ฟังก์ชัน
อาร์กิวเมนต์ของฟังก์ชัน (ถ้าให้ดีควรมีแค่ 2 หรือน้อยกว่า)
การจำกัดจำนวนของพารามิเตอร์เป็นสิ่งที่สำคัญอย่างยิ่ง เพราะว่ามันจะทำให้การทดสอบฟังก์ชันของคุณทำได้ง่ายมาก การที่มีมากกว่า 3 ตัวขึ้นไป จะทำให้เราต้องเขียนเทสเคสที่แตกต่างกันของแต่ละอาร์กิวเมนต์ ซึ่งมันจะจำนวนเทสเคสที่เยอะมาก
การมี 1 หรือ 2 อาร์กิวเมนต์จึงเป็นสิ่งที่ดีที่สุด และเป็นไปได้ไม่ควรถึง 3 อาร์กิวเมนต์ แต่ถ้าจำเป็นต้องมีมากกว่านั้นจริง ๆ ก็ควรหาทางรวมกันให้ได้ ซึ่งโดยปกติถ้าคุณมีมากกว่า 2 อาร์กิวเมนต์ แปลว่าฟังก์ชันที่คุณพยามทำนั้นมันใหญ่เกินไปแล้ว แต่ในกรณีที่ไม่ใช่แบบนั้น การใช้ higher-level object เป็นอาร์กิวเมนต์ก็จะเป็นทางเลือกที่ดีกว่า
ถ้าคุณพบว่าคุณเองต้องการใช้อาร์กิวเมนต์จำนวนมาก ให้ตัดสินใจใช้ object literals
เพื่อให้ชัดเจนว่าฟังก์ชันของคุณต้องการ property อะไรบ้าง คุณควรเลือกใช้ destructuring ซึ่งมีข้อดีดังนี้:
- เมื่อมีคนดูการทำงานของฟังก์ชัน จะรู้ได้ทันทีเลยว่า property อะไรบ้างที่กำลังถูกใช้อยู่
- สามารถใช้เพื่อจำลองเป็นชื่อพารามิเตอร์ได้
- การ Destructuring จะส่งค่าเริ่มต้นของอาร์กิวเมนต์ไปในฟังก์ชันด้วย ทำให้ช่วยป้องกันไม่ให้เกิดปัญหา side effect ได้ ข้อควรจำ: ถ้าเป็น object และ array ที่ถูก destructure ไว้อยู่แล้วในอาร์กิวเมนต์ ค่าเหล่านั้นจะไม่ถูกส่งเข้าฟังก์ชันนั้นด้วย
- TypeScript จะมีการเตือนหากมี property ไหนที่ไม่ได้ใช้ได้ด้วย ซึ่งจะเป็นไม่ได้หากเราไม่มีการใช้ destructuring
👎 ไม่ดี:
function createMenu(title: string, body: string, buttonText: string, cancellable: boolean) {
// ...
}
createMenu('Foo', 'Bar', 'Baz', true);
👍 ดี:
function createMenu(options: { title: string; body: string; buttonText: string; cancellable: boolean }) {
// ...
}
createMenu({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
});
คุณสามารถปรับปรุงให้อ่านง่ายขึ้นได้อีกโดยใช้ type aliases:
type MenuOptions = {
title: string;
body: string;
buttonText: string;
cancellable: boolean;
};
function createMenu(options: MenuOptions) {
// ...
}
createMenu({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
});
ฟังก์ชันนึงควรทำแค่ 1 อย่าง
นี่เป็นกฎที่สำคัญที่สุดในวิศวกรรมซอฟต์แวร์ เมื่อฟังก์ชันนึงทำหลายอย่าง จะทำให้เขียน test case ยาก แต่เมื่อคุณแยกฟังก์ชันเป็นฟังกชั่นเล็ก ๆ ที่ทำงานแค่ 1 อย่าง จะทำให้สามารถปรับปรุงโค้ดได้ง่ายและดูสะอาดตาขึ้นมาก ถ้าคุณไม่ได้ทำอะไรผิดแปลกไปนอกเหนือจากคู่มือนี้ คุณก็จะลำ้หน้า developers คนอื่น ๆ อีกมาก
👎 ไม่ดี:
function emailClients(clients: Client[]) {
clients.forEach((client) => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
👍 ดี:
function emailClients(clients: Client[]) {
clients.filter(isActiveClient).forEach(email);
}
function isActiveClient(client: Client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}
ชื่อฟังก์ชันควรบอกได้เลยว่าทำอะไร
👎 ไม่ดี:
function addToDate(date: Date, month: number): Date {
// ...
}
const date = new Date();
// It's hard to tell from the function name what is added
addToDate(date, 1);
👍 ดี:
function addMonthToDate(date: Date, month: number): Date {
// ...
}
const date = new Date();
addMonthToDate(date, 1);
ฟังก์ชันควรเป็นนามธรรมระดับหนึ่งเท่านั้น
เมื่อคุณมีนามธรรมมากกว่าหนึ่งระดับหน้าที่ ของคุณมักจะทำมากเกินไป การแยกฟังก์ชันนำไปสู่การใช้ซ้ำและการทดสอบที่ง่ายขึ้น
👎 ไม่ดี:
function parseCode(code: string) {
const REGEXES = [
/* ... */
];
const statements = code.split(' ');
const tokens = [];
REGEXES.forEach((regex) => {
statements.forEach((statement) => {
// ...
});
});
const ast = [];
tokens.forEach((token) => {
// lex...
});
ast.forEach((node) => {
// parse...
});
}
👍 ดี:
const REGEXES = [
/* ... */
];
function parseCode(code: string) {
const tokens = tokenize(code);
const syntaxTree = parse(tokens);
syntaxTree.forEach((node) => {
// parse...
});
}
function tokenize(code: string): Token[] {
const statements = code.split(' ');
const tokens: Token[] = [];
REGEXES.forEach((regex) => {
statements.forEach((statement) => {
tokens.push(/* ... */);
});
});
return tokens;
}
function parse(tokens: Token[]): SyntaxTree {
const syntaxTree: SyntaxTree[] = [];
tokens.forEach((token) => {
syntaxTree.push(/* ... */);
});
return syntaxTree;
}
ลดโค้ดที่ซ้ำกัน
พยายามหลีกเลี่ยงไม่ให้มีโค้ดซ้ำกัน การมีโค้ดซ้ำกันเป็นสิ่งที่ไม่ดีเลย เพราะว่าถ้าเราต้องแก้โค้ดนั้นมันก็หมายความว่าเราต้องตามแก้ให้ครบทุกจุดที่ซ้ำกัน
ลองนึกภาพว่าถ้าคุณเป็นเจ้าของร้านอาหารและคุณต้องการติดตามสินค้าคงคลังของคุณ: มะเขือเทศ, หัวหอม, กระเทียม, เครื่องเทศ และอื่น ๆ ทั้งหมดของคุณ ถ้าคุณจดรายการเหล่านั้นไว้หลาย ๆ ที่ เมื่อคุณมีการเสิร์ฟอาหารที่มีมะเขือเทศออกไป คุณก็ต้องไปปรับจำนวนมะเขือเทศในในทุกรายการที่ที่จดไว้ แต่ถ้าคุณจดไว้ที่เดียวคุณก็แค่ปรับรายการแค่ที่เดียว!
บ่อยครั้งที่คุณมีโค้ดที่ซ้ำกันได้ เพราะคุณมีโค้ดที่แตกต่างกันเล็กน้อยมากกว่าสองที่ ส่วนใหญ่จะมีส่วนมีเหมือนกันอยู่เกือบทั้งหมด แต่ส่วนที่ต่างเล็กน้อยนั่นแหละบังคับให้คุณต้องแยกฟังก์ชันออกไปมากกว่าสองที่ ทั้ง ๆ ที่มันทำงานเหมือนกัน การลบโค้ดที่ซ้ำกันคือการสร้างสิ่งที่เป็นนามธรรมที่สามารถจัดสิ่งที่แตกต่างกันทั้งหมดนี้ด้วยการใช้แค่เพียง ฟังก์ชัน/โมดูล/คลาส เดียวเท่านั้น
การทำให้สิ่งที่เป็นนามธรรมถูกต้อง เป็นสิ่งสำคัญนั่นเป็นเหตุผลที่คุณควรปฏิบัติตามหลักการ SOLID นามธรรมที่ไม่ดีอาจแย่กว่าโค้ดที่ซ้ำกันจงระวังไว้ด้วย! บอกไว้เลยว่า ถ้าคุณสามารถสร้างนามธรรมที่ดีได้ก็จงทำ! อย่าทำอะไรซ้ำซากจำเจ มิฉะนั้นคุณจะพบว่าตัวเองต้องคอยตามไปแก้ไขโค้ดหลายที่ ทุกครั้งเวลาที่คุณต้องการแก้ไขโค้ดแค่จุดเดียว
👎 ไม่ดี:
function showDeveloperList(developers: Developer[]) {
developers.forEach((developer) => {
const expectedSalary = developer.calculateExpectedSalary();
const experience = developer.getExperience();
const githubLink = developer.getGithubLink();
const data = {
expectedSalary,
experience,
githubLink
};
render(data);
});
}
function showManagerList(managers: Manager[]) {
managers.forEach((manager) => {
const expectedSalary = manager.calculateExpectedSalary();
const experience = manager.getExperience();
const portfolio = manager.getMBAProjects();
const data = {
expectedSalary,
experience,
portfolio
};
render(data);
});
}
👍 ดี:
class Developer {
// ...
getExtraDetails() {
return {
githubLink: this.githubLink
};
}
}
class Manager {
// ...
getExtraDetails() {
return {
portfolio: this.portfolio
};
}
}
function showEmployeeList(employee: Developer | Manager) {
employee.forEach((employee) => {
const expectedSalary = employee.calculateExpectedSalary();
const experience = employee.getExperience();
const extra = employee.getExtraDetails();
const data = {
expectedSalary,
experience,
extra
};
render(data);
});
}
คุณควรต้องใช้วิจารณญาณเกี่ยวกับเรื่องโค้ดซ้ำ เพราะมีบางครั้งที่ต้องเลือกระหว่างการมีโค้ดที่ซ้ำกันกับการมีที่โค้ดมีความซับซ้อนเพิ่มขิ้นโดยไม่จำเป็น เมื่อมีโค้ดที่เขียนคล้ายกันของโมดูลสองโมดูลที่อยู่กันคนละโดเมน การมีโค้ดซ้ำแบบนั้นก็ถือว่าเป็นเรื่องที่ยอมรับได้ และน่าสนใจกว่าการมีโค้ดพื้นฐานแยกกัน โค้ดพื้นฐานที่แยกออกมาในกรณีนี้แนะนำให้ใช้การพึ่งพาทางอ้อมระหว่างสองโมดูล
ตั้งค่าเริ่มต้นให้อ็อบเจกต์ด้วย Object.assign หรือ destructuring
👎 ไม่ดี:
type MenuConfig = {
title?: string;
body?: string;
buttonText?: string;
cancellable?: boolean;
};
function createMenu(config: MenuConfig) {
config.title = config.title || 'Foo';
config.body = config.body || 'Bar';
config.buttonText = config.buttonText || 'Baz';
config.cancellable = config.cancellable !== undefined ? config.cancellable : true;
// ...
}
createMenu({ body: 'Bar' });
👍 ดี:
type MenuConfig = {
title?: string;
body?: string;
buttonText?: string;
cancellable?: boolean;
};
function createMenu(config: MenuConfig) {
const menuConfig = Object.assign(
{
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
},
config
);
// ...
}
createMenu({ body: 'Bar' });
อีกทางนึง คุณสามารถใช้ destructuring ด้วยค่าเริ่มต้นแบบนี้:
type MenuConfig = {
title?: string;
body?: string;
buttonText?: string;
cancellable?: boolean;
};
function createMenu({ title = 'Foo', body = 'Bar', buttonText = 'Baz', cancellable = true }: MenuConfig) {
// ...
}
createMenu({ body: 'Bar' });
เพื่อหลีกเลี่ยงการเกิด side effects และ unexpected behavior โดยการผ่านค่าให้ชัดเจนไปเลย เช่น undefined
หรือ null
คุณสามารถบอก TypeScript compiler ให้ปิดมัน
โดยดูการตั้งค่า TypeScript ที่ --strictNullChecks
อย่าใช้ flags เป็นพารามิเตอร์ของฟังก์ชัน
Flags จะไว้บอกผู้ใช้ของคุณว่าฟังก์ชันนี้ทำมากกว่าหนึ่งอย่าง ฟังก์ชันควรทำแค่อย่างเดียว แยกฟังก์ชันของคุณออกมา ถ้าพวกเขากำลังไล่หาโค้ดผิดที่จากโค้ดใช้ boolean ที่แตกต่างกัน
👎 ไม่ดี:
function createFile(name: string, temp: boolean) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}
👍 ดี:
function createTempFile(name: string) {
createFile(`./temp/${name}`);
}
function createFile(name: string) {
fs.create(name);
}