Clean Code TypeScript: Function

Savepong · Jun 01, 2021

ฟังก์ชัน

อาร์กิวเมนต์ของฟังก์ชัน (ถ้าให้ดีควรมีแค่ 2 หรือน้อยกว่า)

การจำกัดจำนวนของพารามิเตอร์เป็นสิ่งที่สำคัญอย่างยิ่ง เพราะว่ามันจะทำให้การทดสอบฟังก์ชันของคุณทำได้ง่ายมาก การที่มีมากกว่า 3 ตัวขึ้นไป จะทำให้เราต้องเขียนเทสเคสที่แตกต่างกันของแต่ละอาร์กิวเมนต์ ซึ่งมันจะจำนวนเทสเคสที่เยอะมาก

การมี 1 หรือ 2 อาร์กิวเมนต์จึงเป็นสิ่งที่ดีที่สุด และเป็นไปได้ไม่ควรถึง 3 อาร์กิวเมนต์ แต่ถ้าจำเป็นต้องมีมากกว่านั้นจริง ๆ ก็ควรหาทางรวมกันให้ได้ ซึ่งโดยปกติถ้าคุณมีมากกว่า 2 อาร์กิวเมนต์ แปลว่าฟังก์ชันที่คุณพยามทำนั้นมันใหญ่เกินไปแล้ว แต่ในกรณีที่ไม่ใช่แบบนั้น การใช้ higher-level object เป็นอาร์กิวเมนต์ก็จะเป็นทางเลือกที่ดีกว่า

ถ้าคุณพบว่าคุณเองต้องการใช้อาร์กิวเมนต์จำนวนมาก ให้ตัดสินใจใช้ object literals

เพื่อให้ชัดเจนว่าฟังก์ชันของคุณต้องการ property อะไรบ้าง คุณควรเลือกใช้ destructuring ซึ่งมีข้อดีดังนี้:

  1. เมื่อมีคนดูการทำงานของฟังก์ชัน จะรู้ได้ทันทีเลยว่า property อะไรบ้างที่กำลังถูกใช้อยู่
  2. สามารถใช้เพื่อจำลองเป็นชื่อพารามิเตอร์ได้
  3. การ Destructuring จะส่งค่าเริ่มต้นของอาร์กิวเมนต์ไปในฟังก์ชันด้วย ทำให้ช่วยป้องกันไม่ให้เกิดปัญหา side effect ได้ ข้อควรจำ: ถ้าเป็น object และ array ที่ถูก destructure ไว้อยู่แล้วในอาร์กิวเมนต์ ค่าเหล่านั้นจะไม่ถูกส่งเข้าฟังก์ชันนั้นด้วย
  4. 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);
}
TypeScriptClean codeBest PracticeFunction
Senior Software Engineer

Pongsiri Chuaychoonoo

Senior Software Engineer at Refinitiv, an LSEG Business, CPO & Co-founder of Code Passion, More than 10 years experience, Expertise in web and mobile app development, Always keep in touch and catch up the new technologies.

Webring