美文网首页程序员
TypeScript 代码整洁之道 - 函数

TypeScript 代码整洁之道 - 函数

作者: 小校有来有去 | 来源:发表于2019-03-01 23:04 被阅读19次

    将 Clean Code 的概念适用到 TypeScript,灵感来自 clean-code-javascript
    原文地址: clean-code-typescript
    中文地址: clean-code-typescript

    简介

    image

    这不是一份 TypeScript 设计规范,而是将 Robert C. Martin 的软件工程著作 《Clean Code》 适用到 TypeScript,指导读者使用 TypeScript 编写易读、可复用和易重构的软件。

    函数

    参数越少越好 (理想情况不超过2个)

    限制参数个数,这样函数测试会更容易。超过三个参数会导致测试复杂度激增,需要测试众多不同参数的组合场景。
    理想情况,只有一两个参数。如果有两个以上的参数,那么您的函数可能就太过复杂了。

    如果需要很多参数,请您考虑使用对象。为了使函数的属性更清晰,可以使用解构,它有以下优点:

    1. 当有人查看函数签名时,会立即清楚使用了哪些属性。

    2. 解构对传递给函数的参数对象做深拷贝,这可预防副作用。(注意:不会克隆从参数对象中解构的对象和数组)

    3. TypeScript 会对未使用的属性显示警告。

    反例:

    
    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
    
    });
    
    

    通过 TypeScript 的类型别名,可以进一步提高可读性。

    
    type MenuOptions = {title: string, body: string, buttonText: string, cancellable: boolean};
    
    function createMenu(options: MenuOptions) {
    
      // ...
    
    }
    
    createMenu({
    
      title: 'Foo',
    
      body: 'Bar',
    
      buttonText: 'Baz',
    
      cancellable: true
    
    });
    
    

    只做一件事

    这是目前软件工程中最重要的规则。如果函数做不止一件事,它就更难组合、测试以及理解。反之,函数只有一个行为,它就更易于重构、代码就更清晰。如果您只从本指南中了解到这一点,那么您就领先多数程序员了。

    反例:

    
    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;
    
    }
    
    

    删除重复代码

    重复乃万恶之源!重复意味着如果要修改某个逻辑,需要修改多处代码:cry:。
    想象一下,如果你经营一家餐厅,要记录你的库存:所有的西红柿、洋葱、大蒜、香料等等。如果要维护多个库存列表,那是多么痛苦的事!

    存在重复代码,是因为有两个或两个以上很近似的功能,只有一点不同,但是这点不同迫使你用多个独立的函数来做很多几乎相同的事情。删除重复代码,则意味着创建一个抽象,该抽象仅用一个函数/模块/类就可以处理这组不同的东西。

    合理的抽象至关重要,这就是为什么您应该遵循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 = developer.calculateExpectedSalary();
    
        const experience = developer.getExperience();
    
        const extra = employee.getExtraDetails();
    
        const data = {
    
          expectedSalary,
    
          experience,
    
          extra,
    
        };
    
        render(data);
    
      });
    
    }
    
    

    有时,在重复代码和引入不必要的抽象而增加的复杂性之间,需要做权衡。当来自不同领域的两个不同模块,它们的实现看起来相似,复制也是可以接受的,并且比抽取公共代码要好一点。因为抽取公共代码会导致两个模块产生间接的依赖关系。

    使用Object.assign解构来设置默认对象

    反例:

    
    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;
    
    }
    
    const menuConfig = {
    
      title: null,
    
      body: 'Bar',
    
      buttonText: null,
    
      cancellable: true
    
    };
    
    createMenu(menuConfig);
    
    

    正例:

    
    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' });
    
    

    或者,您可以使用默认值的解构:

    
    type MenuConfig = {title?: string, body?: string, buttonText?: string, cancellable?: boolean};
    
    function createMenu({title = 'Foo', body = 'Bar', buttonText = 'Baz', cancellable = true}: MenuConfig) {
    
      // ...
    
    }
    
    createMenu({ body: 'Bar' });
    
    

    为了避免副作用,不允许显式传递undefinednull值。参见 TypeScript 编译器的--strictnullcheck选项。

    不要使用Flag参数

    Flag参数告诉用户这个函数做了不止一件事。如果函数使用布尔值实现不同的代码逻辑路径,则考虑将其拆分。

    反例:

    
    function createFile(name:string, temp:boolean) {
    
      if (temp) {
    
        fs.create(`./temp/${name}`);
    
      } else {
    
        fs.create(name);
    
      }
    
    }
    
    

    正例:

    
    function createFile(name:string) {
    
      fs.create(name);
    
    }
    
    function createTempFile(name:string) {
    
      fs.create(`./temp/${name}`);
    
    }
    
    

    避免副作用 (part1)

    当函数产生除了“一个输入一个输出”之外的行为时,称该函数产生了副作用。比如写文件、修改全局变量或将你的钱全转给了一个陌生人等。

    在某些情况下,程序需要一些副作用。如先前例子中的写文件,这时应该将这些功能集中在一起,不要用多个函数/类修改某个文件。用且只用一个 service 完成这一需求。

    重点是要规避常见陷阱,比如,在无结构对象之间共享状态、使用可变数据类型,以及不确定副作用发生的位置。如果你能做到这点,你才可能笑到最后!

    反例:

    
    // Global variable referenced by following function.
    
    // If we had another function that used this name, now it'd be an array and it could break it.
    
    let name = 'Robert C. Martin';
    
    function toBase64() {
    
      name = btoa(name);
    
    }
    
    toBase64(); // produces side effects to `name` variable
    
    console.log(name); // expected to print 'Robert C. Martin' but instead 'Um9iZXJ0IEMuIE1hcnRpbg=='
    
    

    正例:

    
    // Global variable referenced by following function.
    
    // If we had another function that used this name, now it'd be an array and it could break it.
    
    const name = 'Robert C. Martin';
    
    function toBase64(text:string):string {
    
      return btoa(text);
    
    }
    
    const encodedName = toBase64(name);
    
    console.log(name);
    
    

    避免副作用 (part2)

    在 JavaScript 中,原类型是值传递,对象、数组是引用传递。

    有这样一种情况,如果您的函数修改了购物车数组,用来添加购买的商品,那么其他使用该cart数组的函数都将受此添加操作的影响。想象一个糟糕的情况:

    用户点击“购买”按钮,该按钮调用purchase函数,函数请求网络并将cart数组发送到服务器。由于网络连接不好,购买功能必须不断重试请求。恰巧在网络请求开始前,用户不小心点击了某个不想要的项目上的“Add to Cart”按钮,该怎么办?而此时网络请求开始,那么purchase函数将发送意外添加的项,因为它引用了一个购物车数组,addItemToCart函数修改了该数组,添加了不需要的项。

    一个很好的解决方案是addItemToCart总是克隆cart,编辑它,并返回克隆。这确保引用购物车的其他函数不会受到任何更改的影响。

    注意两点:

    1. 在某些情况下,可能确实想要修改输入对象,这种情况非常少见。且大多数可以重构,确保没副作用!(见纯函数)

    2. 性能方面,克隆大对象代价确实比较大。还好有一些很好的库,它提供了一些高效快速的方法,且不像手动克隆对象和数组那样占用大量内存。

    反例:

    
    function addItemToCart(cart: CartItem[], item:Item):void {
    
      cart.push({ item, date: Date.now() });
    
    };
    
    

    正例:

    
    function addItemToCart(cart: CartItem[], item:Item):CartItem[] {
    
      return [...cart, { item, date: Date.now() }];
    
    };
    
    

    不要写全局函数

    在 JavaScript 中污染全局的做法非常糟糕,这可能导致和其他库冲突,而调用你的 API 的用户在实际环境中得到一个 exception 前对这一情况是一无所知的。

    考虑这样一个例子:如果想要扩展 JavaScript 的 Array,使其拥有一个可以显示两个数组之间差异的 diff方法,该怎么做呢?可以将新函数写入Array.prototype ,但它可能与另一个尝试做同样事情的库冲突。如果另一个库只是使用diff来查找数组的第一个元素和最后一个元素之间的区别呢?

    更好的做法是扩展Array,实现对应的函数功能。

    反例:

    
    declare global {
    
      interface Array<T> {
    
        diff(other: T[]): Array<T>;
    
      }
    
    }
    
    if (!Array.prototype.diff){
    
      Array.prototype.diff = function <T>(other: T[]): T[] {
    
        const hash = new Set(other);
    
        return this.filter(elem => !hash.has(elem));
    
      };
    
    }
    
    

    正例:

    
    class MyArray<T> extends Array<T> {
    
      diff(other: T[]): T[] {
    
        const hash = new Set(other);
    
        return this.filter(elem => !hash.has(elem));
    
      };
    
    }
    
    

    函数式编程优于命令式编程

    尽量使用函数式编程!

    反例:

    
    const contributions = [
    
      {
    
        name: 'Uncle Bobby',
    
        linesOfCode: 500
    
      }, {
    
        name: 'Suzie Q',
    
        linesOfCode: 1500
    
      }, {
    
        name: 'Jimmy Gosling',
    
        linesOfCode: 150
    
      }, {
    
        name: 'Gracie Hopper',
    
        linesOfCode: 1000
    
      }
    
    ];
    
    let totalOutput = 0;
    
    for (let i = 0; i < contributions.length; i++) {
    
      totalOutput += contributions[i].linesOfCode;
    
    }
    
    

    正例:

    
    const contributions = [
    
      {
    
        name: 'Uncle Bobby',
    
        linesOfCode: 500
    
      }, {
    
        name: 'Suzie Q',
    
        linesOfCode: 1500
    
      }, {
    
        name: 'Jimmy Gosling',
    
        linesOfCode: 150
    
      }, {
    
        name: 'Gracie Hopper',
    
        linesOfCode: 1000
    
      }
    
    ];
    
    const totalOutput = contributions
    
      .reduce((totalLines, output) => totalLines + output.linesOfCode, 0)
    
    

    封装判断条件

    反例:

    
    if (subscription.isTrial || account.balance > 0) {
    
      // ...
    
    }
    
    

    正例:

    
    function canActivateService(subscription: Subscription, account: Account) {
    
      return subscription.isTrial || account.balance > 0
    
    }
    
    if (canActivateService(subscription, account)) {
    
      // ...
    
    }
    
    

    避免“否定”的判断

    反例:

    
    function isEmailNotUsed(email: string) {
    
      // ...
    
    }
    
    if (isEmailNotUsed(email)) {
    
      // ...
    
    }
    
    

    正例:

    
    function isEmailUsed(email) {
    
      // ...
    
    }
    
    if (!isEmailUsed(node)) {
    
      // ...
    
    }
    
    

    避免判断条件

    这看起来似乎不太可能完成啊。大多数人听到后第一反应是,“没有if语句怎么实现功能呢?” 在多数情况下,可以使用多态性来实现相同的功能。接下来的问题是 “为什么要这么做?” 原因就是之前提到的:函数只做一件事。

    反例:

    
    class Airplane {
    
      private type: string;
    
      // ...
    
      getCruisingAltitude() {
    
        switch (this.type) {
    
          case '777':
    
            return this.getMaxAltitude() - this.getPassengerCount();
    
          case 'Air Force One':
    
            return this.getMaxAltitude();
    
          case 'Cessna':
    
            return this.getMaxAltitude() - this.getFuelExpenditure();
    
          default:
    
            throw new Error('Unknown airplane type.');
    
        }
    
      }
    
    }
    
    

    正例:

    
    class Airplane {
    
      // ...
    
    }
    
    class Boeing777 extends Airplane {
    
      // ...
    
      getCruisingAltitude() {
    
        return this.getMaxAltitude() - this.getPassengerCount();
    
      }
    
    }
    
    class AirForceOne extends Airplane {
    
      // ...
    
      getCruisingAltitude() {
    
        return this.getMaxAltitude();
    
      }
    
    }
    
    class Cessna extends Airplane {
    
      // ...
    
      getCruisingAltitude() {
    
        return this.getMaxAltitude() - this.getFuelExpenditure();
    
      }
    
    }
    
    

    避免类型检查

    TypeScript 是 JavaScript 的一个严格的语法超集,具有静态类型检查的特性。所以指定变量、参数和返回值的类型,以充分利用此特性,能让重构更容易。

    反例:

    
    function travelToTexas(vehicle: Bicycle | Car) {
    
      if (vehicle instanceof Bicycle) {
    
        vehicle.pedal(this.currentLocation, new Location('texas'));
    
      } else if (vehicle instanceof Car) {
    
        vehicle.drive(this.currentLocation, new Location('texas'));
    
      }
    
    }
    
    

    正例:

    
    type Vehicle = Bicycle | Car;
    
    function travelToTexas(vehicle: Vehicle) {
    
      vehicle.move(this.currentLocation, new Location('texas'));
    
    }
    
    

    不要过度优化

    现代浏览器在运行时进行大量的底层优化。很多时候,你做优化只是在浪费时间。有些优秀资源可以帮助定位哪里需要优化,找到并修复它。

    反例:

    
    // On old browsers, each iteration with uncached `list.length` would be costly
    
    // because of `list.length` recomputation. In modern browsers, this is optimized.
    
    for (let i = 0, len = list.length; i < len; i++) {
    
      // ...
    
    }
    
    

    正例:

    
    for (let i = 0; i < list.length; i++) {
    
      // ...
    
    }
    
    

    删除无用代码

    无用代码和重复代码一样无需保留。如果没有地方调用它,请删除!如果仍然需要它,可以查看版本历史。

    反例:

    
    function oldRequestModule(url: string) {
    
      // ...
    
    }
    
    function requestModule(url: string) {
    
      // ...
    
    }
    
    const req = requestModule;
    
    inventoryTracker('apples', req, 'www.inventory-awesome.io');
    
    

    正例:

    
    function requestModule(url: string) {
    
      // ...
    
    }
    
    const req = requestModule;
    
    inventoryTracker('apples', req, 'www.inventory-awesome.io');
    
    

    使用迭代器和生成器

    像使用流一样处理数据集合时,请使用生成器和迭代器。

    理由如下:

    • 将调用者与生成器实现解耦,在某种意义上,调用者决定要访问多少项。
    • 延迟执行,按需使用。
    • 内置支持使用for-of语法进行迭代
    • 允许实现优化的迭代器模式

    反例:

    function fibonacci(n: number): number[] {
      if (n === 1) return [0];
      if (n === 2) return [0, 1];
    
      const items: number[] = [0, 1];
      while (items.length < n) {
        items.push(items[items.length - 2] + items[items.length - 1]);
      }
    
      return items;
    }
    
    function print(n: number) {
      fibonacci(n).forEach(fib => console.log(fib));
    }
    
    // Print first 10 Fibonacci numbers.
    print(10);
    

    正例:

    // Generates an infinite stream of Fibonacci numbers.
    // The generator doesn't keep the array of all numbers.
    function* fibonacci(): IterableIterator<number> {
      let [a, b] = [0, 1];
    
      while (true) {
        yield a;
        [a, b] = [b, a + b];
      }
    }
    
    function print(n: number) {
      let i = 0;
      for (const fib in fibonacci()) {
        if (i++ === n) break;  
        console.log(fib);
      }  
    }
    
    // Print first 10 Fibonacci numbers.
    print(10);
    

    有些库通过链接“map”、“slice”、“forEach”等方法,达到与原生数组类似的方式处理迭代。参见 itiriri 里面有一些使用迭代器的高级操作示例(或异步迭代的操作 itiriri-async)。

    import itiriri from 'itiriri';
    
    function* fibonacci(): IterableIterator<number> {
      let [a, b] = [0, 1];
     
      while (true) {
        yield a;
        [a, b] = [b, a + b];
      }
    }
    
    itiriri(fibonacci())
      .take(10)
      .forEach(fib => console.log(fib));
    

    上一章:TypeScript 代码整洁之道 - 变量
    下一章:TypeScript 代码整洁之道 - 对象和数据结构

    相关文章

      网友评论

        本文标题:TypeScript 代码整洁之道 - 函数

        本文链接:https://www.haomeiwen.com/subject/jhyluqtx.html