TS代码整洁之道(下)

TS代码整洁之道——"净"

maxueming|2022-10


3. 对象和数据结构

3.1 使用 getters 和 setters

TypeScript 支持 getter/setter 语法。使用 getter 和 setter 从对象中访问数据比简单地在对象上查找属性要好。原因如下:

  • 当需要在获取对象属性之前做一些事情时,不必在代码中查找并修改每一处调用。
  • 执行 set 时添加验证更简单。
  • 封装内部表示。
  • 更容易添加日志和错误处理。
  • 可以延迟加载对象的属性,比如从服务器获取它。

反例:

class BankAccount {
  balance: number = 0;
  // ...
}

const value = 100;
const account = new BankAccount();

if (value < 0) {
  throw new Error('Cannot set negative balance.');
}

account.balance = value;

正例:

class BankAccount {
  private accountBalance: number = 0;
  get balance(): number {
    return this.accountBalance;
  }

  set balance(value: number) {
    if (value < 0) {
      throw new Error('Cannot set negative balance.');
    }
    this.accountBalance = value;
  }

  // ...
}

const account = new BankAccount();
account.balance = 100;

3.2 让对象拥有 private/protected 成员

TypeScript 类成员支持 public(默认)、protected 以及 private 的访问限制。

反例:

class Circle {
  radius: number;

  constructor(radius: number) {
    this.radius = radius;
  }

  surface() {
    return Math.PI * this.radius * this.radius;
  }
}

正例:

class Circle {
  constructor(private readonly radius: number) {
  }

  surface(){
    return Math.PI * this.radius * this.radius;
  }
}

3.3 不变性

TypeScript 类型系统允许将接口、类上的单个属性设置为只读,能以函数的方式运行。

还有个高级场景,可以使用内置类型 Readonly,它接受类型 T 并使用映射类型将其所有属性标记为只读。

反例:

interface Config {
  host: string;
  port: string;
  db: string;
}

正例:

interface Config {
  readonly host: string;
  readonly port: string;
  readonly db: string;
}

3.4 类型 vs 接口

定义组合类型,交叉类型和原始类型,请使用 type。如果需要扩展或实现,请使用 interface。然而,没有最好,只有是否适合。type 和 interface 区别,详细参考 Stack Overflow 上的解答 。

示例:

interface Shape {

}

class Circle implements Shape {
  // ...
}

class Square implements Shape {
  // ...
}

4. 类的设计以及 SOLID 原则

首先,类一定要小、小、小!重要的事情说三遍!类的大小是由它的职责来度量的,按照单一职责原则,类要小。

另外,好的设计要高内聚低耦合

  • 内聚:定义类成员之间相互关联的程度。理想情况下,高内聚类的每个方法都应该使用类中的所有字段,实际这不可能也不可取。但依然提倡高内聚。
  • 耦合:指的是两个类之间的关联程度。如果其中一个类的更改不影响另一个类,则称为低耦合类。

这些都是老生常谈的原则,这里不举例说明了。

4.1 组合大于继承

“四人帮”在《设计模式》中指出:尽可能使用组合而不是继承。如果你默认倾向于继承,那么考虑下组合是否能更好的解决问题。

何时使用继承?需要因地制宜:

  • 继承代表的是 is-a 关系,而不是 has-a 关系(人 -> 动物 vs. 用户 -> 用户详情)。
  • 可复用基类的代码 。
  • 希望通过更改基类对派生类进行全局更改。

设计模式四人帮,又称 Gang of Four,即 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 四人的《设计模式》,原名 Design Patterns: Elements of Reusable Object-Oriented Software,第一次将设计模式提升到理论高度,并将之规范化。

反例:

class User {
  constructor(
    private readonly name: string, 
    private readonly id: string) {
  }

  // ...
}

// 用户工作信息并不是一类用户,这种继承有问题
class UserJob extends User {
  constructor(
    name: string, 
    id: string,
    private readonly company: string, 
    private readonly salary: number) {
    super(name, id);
  }

  // ...
}

正例:

class User {
  private job: UserJob; // 使用组合
  constructor(
    private readonly name: string, 
    private readonly id: string) {
  }

  setJob(company: string, salary: number): User {
    this.job = new UserJob(company, salary);
    return this;
  }

  // ...
}

class UserJob {
  constructor(
    public readonly company: string, 
    public readonly salary: number) {
  }

  // ...
}

4.2 链式调用

非常有用且表达力非常好的一种写法,代码也看起来更简洁。

反例:

class QueryBuilder {
  private collection: string;
  private pageNumber: number = 1;
  private itemsPerPage: number = 100;
  private orderByFields: string[] = [];

  from(collection: string): void {
    this.collection = collection;
  }

  page(number: number, itemsPerPage: number = 100): void {
    this.pageNumber = number;
    this.itemsPerPage = itemsPerPage;
  }

  orderBy(...fields: string[]): void {
    this.orderByFields = fields;
  }

  build(): Query {
    // ...
  }
}

// ...
const query = new QueryBuilder();
query.from('users');
query.page(1, 100);
query.orderBy('firstName', 'lastName');
const query = queryBuilder.build();

正例:

class QueryBuilder {
  private collection: string;
  private pageNumber: number = 1;
  private itemsPerPage: number = 100;
  private orderByFields: string[] = [];

  from(collection: string): this {
    this.collection = collection;
    return this;
  }

  page(number: number, itemsPerPage: number = 100): this {
    this.pageNumber = number;
    this.itemsPerPage = itemsPerPage;
    return this;
  }

  orderBy(...fields: string[]): this {
    this.orderByFields = fields;
    return this;
  }

  build(): Query {
    // ...
  }
}

// ... 链式调用
const query = new QueryBuilder()
  .from('users')
  .page(1, 100)
  .orderBy('firstName', 'lastName')
  .build();

4.3 SOLID 原则

4.3.1 单一职责原则(Single Responsibility Principle)

类更改的原因不应该超过一个。

如果把很多不相关的功能都放在一个类中,看起来似乎很方便。却导致可能有很多原因去修改它,应该尽量减少修改类的次数。且修改了其中一处很难确定对其他依赖模块的影响。

反例:

class UserSettings {
  constructor(private readonly user: User) {
  }

  changeSettings(settings: UserSettings) {
    if (this.verifyCredentials()) {
      // ...
    }
  }

  verifyCredentials() { 
    // 和 UserSettings 没有关系的逻辑
    // ... 
  }

}

正例:

class UserAuth {
  constructor(private readonly user: User) {
  }

  verifyCredentials() {
    // ...
  }
}

class UserSettings {
  private readonly auth: UserAuth;
  constructor(private readonly user: User) {
    this.auth = new UserAuth(user);
  }

  changeSettings(settings: UserSettings) {
    if (this.auth.verifyCredentials()) {
      // ...
    }
  }
}

4.3.2 开闭原则(Open Closed Principle)

正如 Bertrand Meyer 所说,“软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。”换句话说,就是允许在不更改现有代码的情况下添加新功能。

反例:

class AjaxAdapter extends Adapter {
  constructor() {
    super();
  }

  // ...
}

class NodeAdapter extends Adapter {
  constructor() {
    super();
  }

  // ...
}

class HttpRequester {

  constructor(private readonly adapter: Adapter) {}

  async fetch<T>(url: string): Promise<T> {
     // 对于不同的 adapter 都要做不同处理,如果新增一类 adapter 需要在此处新增处理逻辑
    if (this.adapter instanceof AjaxAdapter) {
      const response = await makeAjaxCall<T>(url);
    } else if (this.adapter instanceof NodeAdapter) {
      const response = await makeHttpCall<T>(url);
    }
  }
}

function makeAjaxCall<T>(url: string): Promise<T> {
  // 请求并返回 promise
}

function makeHttpCall<T>(url: string): Promise<T> {
  // 请求并返回 promise
}

正例:

abstract class Adapter {
  abstract async request<T>(url: string): Promise<T>;
}

class AjaxAdapter extends Adapter {
  // ...
  async request<T>(url: string): Promise<T>{
    // 请求并返回 promise
  }
}

class NodeAdapter extends Adapter {
  //...
  async request<T>(url: string): Promise<T>{
    // 请求并返回 promise
  }
}

class HttpRequester {
  constructor(private readonly adapter: Adapter) {}

  // 新增一类 adapter,此处代码不需要做任何处理
  async fetch<T>(url: string): Promise<T> {
    const response = await this.adapter.request<T>(url);
  }
}

4.3.3 里氏替换原则(Liskov Substitution Principle)

听起来有点懵?是的!

这个原则的定义是:“如果 S 是 T 的一个子类型,那么类型 T 的对象可以被替换为类型 S 的对象,而不会改变程序的正确性”。这听起来似乎还是有点懵了。

讲人话就是,如果有一个父类和一个子类,那么父类和子类可以互换使用,而代码不会出现问题。

看下经典的正方形矩形的例子。从数学上讲,正方形是矩形,但是如果通过继承使用 is-a 关系对其建模,很快就会遇到麻烦。

反例:

class Rectangle {
  constructor(
    protected width: number = 0, 
    protected height: number = 0) {
  }

  setWidth(width: number) {
    this.width = width;
  }

  setHeight(height: number) {
    this.height = height;
  }

  getArea(): number {
    return this.width * this.height;
  }
}

class Square extends Rectangle {
  setWidth(width: number) {
    this.width = width;
    this.height = width;
  }

  setHeight(height: number) {
    this.width = height;
    this.height = height;
  }
}

function renderLargeRectangles(rectangles: Rectangle[]) {
  rectangles.forEach((rectangle) => {
    rectangle.setWidth(4);
    rectangle.setHeight(5);
    const area = rectangle.getArea(); // 当传入的是 Square 时,返回了 25,应该是 20。没有遵守里氏替换原则
    // ...
  });
}

const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);

子类必须实现父类的抽象方法,但最好不要重写父类的非抽象方法。

正例:

abstract class Shape {
  abstract getArea(): number;  // 抽象
}

class Rectangle extends Shape {
  constructor(
    private readonly width = 0, 
    private readonly height = 0) {
    super();
  }

  getArea(): number {
    return this.width * this.height;
  }
}

class Square extends Shape {
  constructor(private readonly length: number) {
    super();
  }

  getArea(): number {
    return this.length * this.length;
  }

}

function renderLargeShapes(shapes: Shape[]) {
  shapes.forEach((shape) => {
    const area = shape.getArea();
    // ...
  });
}

const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);

4.3.4 接口隔离原则(Interface Segregation Principle)

“要设计小并且具体的接口,而非大而全!”这一原则与单一责任原则密切相关。试想,如果一个接口是一个大而全的抽象,那么实现这个接口就会成为一种负担,因为需要实现一些不需要的方法。

反例:

interface ISmartPrinter {
  print();
  fax();
  scan();
}

class AllInOnePrinter implements ISmartPrinter {
  print() {
    // ...
  }  

  fax() {
    // ...
  }

  scan() {
    // ...
  }
}

class EconomicPrinter implements ISmartPrinter {
  print() {
    // ...
  }  

  fax() {
    throw new Error('Fax not supported.');
  }

  scan() {
    throw new Error('Scan not supported.');
  }
}

正例:

interface IPrinter {
  print();
}

interface IFax {
  fax();
}

interface IScanner {
  scan();
}

class AllInOnePrinter implements IPrinter, IFax, IScanner {

  print() {
    // ...
  }  

  fax() {
    // ...
  }

  scan() {
    // ...
  }
}

class EconomicPrinter implements IPrinter {
  print() {
    // ...
  }
}

4.3.5 依赖倒置原则(Dependency Inversion Principle)

这个原则有两个要点:

  • 高层模块不应该依赖于低层模块,两者都应该依赖于抽象。
  • 抽象不依赖实现,实现应依赖抽象。

一开始这难以理解,但是如果你使用过 Angular,你就会看到以依赖注入(DI)的方式实现了这一原则。虽然概念不同,但是 DIP 阻止高级模块了解其低级模块的实现细节,这样做的一个巨大好处是减少了模块之间的耦合。模块间的高耦合非常麻烦,它让代码难以重构。

DIP 通常是通过使用控制反转(IoC)容器来实现的。比如:TypeScript 的 IoC 容器 InversifyJs

反例:

import { readFile as readFileCb } from 'fs';
import { promisify } from 'util';

const readFile = promisify(readFileCb);

type ReportData = {
  // ..
}

class XmlFormatter {
  parse<T>(content: string): T {
    // 转换 XML 字符串
  }
}

class ReportReader {
  // 这里已经对具体的实现 XmlFormatter 产生了依赖,实际上只需要依赖方法:parse
  private readonly formatter = new XmlFormatter();
  async read(path: string): Promise<ReportData> {
    const text = await readFile(path, 'UTF8');
    return this.formatter.parse<ReportData>(text);
  }

}
// ...

const reader = new ReportReader();
await report = await reader.read('report.xml');

正例:

import { readFile as readFileCb } from 'fs';
import { promisify } from 'util';

const readFile = promisify(readFileCb);

type ReportData = {
  // ..
}

interface Formatter {
  parse<T>(content: string): T;
}

class XmlFormatter implements Formatter {
  parse<T>(content: string): T {
    //  转换 XML 字符串
  }
}

class JsonFormatter implements Formatter {
  parse<T>(content: string): T {
    // 转换 Json 字符串
  }
}

class ReportReader {
  // 只依赖了抽象,也就是接口 Formatter,而非它的具体实现 XmlFormatter 或 JsonFormatter
  constructor(private readonly formatter: Formatter){}

  async read(path: string): Promise<ReportData> {
    const text = await readFile(path, 'UTF8');
    return this.formatter.parse<ReportData>(text);
  }
}

const reader = new ReportReader(new XmlFormatter());
await report = await reader.read('report.xml');

const reader = new ReportReader(new JsonFormatter());
await report = await reader.read('report.json');

5. 并发

5.1 用 Promises 替代回调

回调不够整洁而且会导致过多的嵌套(回调地狱)。

有些工具使用回调的方式将现有函数转换为 promise 对象:

反例:

import { get } from 'request';
import { writeFile } from 'fs';
// 多层嵌套
function downloadPage(url: string, saveTo: string, callback: (error: Error, content?: string) => void){
  get(url, (error, response) => {
    if (error) {
      callback(error);
    } else {
      writeFile(saveTo, response.body, (error) => {
        if (error) {
          callback(error);
        } else {
          callback(null, response.body);
        }
      });
    }
  })
}

downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html', (error, content) => {
  if (error) {
    console.error(error);
  } else {
    console.log(content);
  }
});

正例:

import { get } from 'request';
import { writeFile } from 'fs';
import { promisify } from 'util';

const write = promisify(writeFile);
function downloadPage(url: string, saveTo: string): Promise<string> {
  return get(url).then(response => write(saveTo, response))
}

downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html')
  .then(content => console.log(content))
  .catch(error => console.error(error));  

同时,Promise 提供了一些辅助方法,能让代码更简洁:

方法 描述
Promise.resolve(value) 返回一个传入值解析后的 promise。
Promise.reject(error) 返回一个带有拒绝原因的 promise。
Promise.all(promises) 返回一个新的 promise,传入数组中的每个 promise 都执行完成后返回的 promise 才算完成,或第一个 promise 拒绝而拒绝。
Promise.race(promises) 返回一个新的 promise,传入数组中的某个 promise 解决或拒绝,返回的 promise 就会解决或拒绝。

Promise.all 在并行运行任务时尤其有用,Promise.race 让为 Promise 更容易实现超时。

5.2 用 Async/Await 替代 Promises

使用 async/await 语法,可以编写更简洁、更易理解代码。一个函数使用 async 关键字作为前缀,那么就告诉了 JavaScript 运行时暂停 await 关键字上的代码执行。

上一节中的例子可以继续优化为:

import { get } from 'request';
import { writeFile } from 'fs';
import { promisify } from 'util';

const write = promisify(writeFile);
async function downloadPage(url: string, saveTo: string): Promise<string> {
  const response = await get(url);
  await write(saveTo, response);
  return response;
}

try {
  const content = await downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html');
  console.log(content);
} catch (error) {
  console.error(error);
}

6. 错误处理

抛出错误并非是件坏事,至少在运行时可以识别出错的位置。通常,程序会在控制台中打印堆栈信息。

6.1 抛出 Error 或 使用 reject

JavaScript 和 TypeScript 允许 throw 任何对象,Promise 也可以用任何理由对象拒绝。

代码中的 Error 可以在 catch 中被捕获,所以还是建议使用 throw error,而不是简单的字符串。 Promise 也是同样的道理。

反例:

function calculateTotal(items: Item[]): number {
  throw 'Not implemented.';
}

function get(): Promise<Item[]> {
  return Promise.reject('Not implemented.');
}

正例:

function calculateTotal(items: Item[]): number {
  throw new Error('Not implemented.');
}

function get(): Promise<Item[]> {
  return Promise.reject(new Error('Not implemented.'));
}

或者:

async function get(): Promise<Item[]> {
  throw new Error('Not implemented.');
}

使用 Error 类型的好处是 try/catch/finally 语法支持它,并且隐式地所有错误都具有 stack 属性,该属性对于调试非常有用。

另外,即使不用 throw 语法而是返回自定义错误对象,TypeScript 在这块也很容易。考虑下面的例子:

type Failable<R, E> = {
  isError: true;
  error: E;
} | {
  isError: false;
  value: R;
}

function calculateTotal(items: Item[]): Failable<number, 'empty'> {
  if (items.length === 0) {
    return { isError: true, error: 'empty' };
  }
  // ...
  return { isError: false, value: 42 };
}

详细解释请参考原文

6.2 有始有终,别忘了捕获 Error

绝对不能忽略 Error,或者捕获到 Error 不处理而是打印到控制台(console.log),这样的话这些异常会丢失在控制台日志的汪洋大海中。如果代码写在 try/catch 中,说明那里可能会发生错误,因此应该考虑在错误发生时做一些对应的处理。

反例:

try {
  throwError();
} catch (error) {
  // 打印到控制台,或则直接忽略。
  // ignore error
}

正例:

import { logger } from './logging'

try {
  throwError();
} catch (error) {
  logger.log(error);
}

另外,Promises 的 Error 也要正确处理。如下:

import { logger } from './logging'

getUser()
  .then((user: User) => {
    return sendEmail(user.email, 'Welcome!');
  }).catch((error) => {
    logger.log(error);
  });

或者使用 async/await 时。如下:

try {
  const user = await getUser();
  await sendEmail(user.email, 'Welcome!');
} catch (error) {
  logger.log(error);
}

7. 测试

测试至关重要。如果没有测试或用例数量不足,那么每次提交代码时都无法确保不引入问题。

怎样才算是足够的测试?一般情况下都会按照覆盖率来衡量,拥有 100% 的分支覆盖率当然最好。这一切都要基于好的测试框架以及覆盖率工具,如:istanbul

没有理由不编写测试。有很多优秀的 JS 测试框架都支持 TypeScript,找个合适的,然后为每个新特性/模块编写测试。如果你习惯于测试驱动开发(TDD),那就太好了,重点是确保在开发任何特性或重构现有特性之前,代码覆盖率已经达到要求。

7.1 TDD(测试驱动开发)

TDD 是敏捷开发中的一项核心实践和技术,也是一种设计方法论。TDD 的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。TDD 是 XP(Extreme Programming)的核心实践。它的主要推动者是 Kent Beck。

TDD 三定律:

  • 在编写不能通过的单元测试前,不可编写生产代码。
  • 只可编写刚好无法通过的单元测试,不能编译也算不过。
  • 只可编写刚好足以通过当前失败测试的生产代码。

7.2 F.I.R.S.T. 准则

为了写出有效的测试代码,应遵循以下准则:

  • 快速(Fast),测试应该快(及时反馈出业务代码的问题)。
  • 独立(Independent),每个测试流程应该独立。
  • 可重复(Repeatable),测试应该在任何环境上都能重复通过。
  • 自我验证(Self-Validating),测试结果应该明确通过或者失败
  • 及时(Timely),测试代码应该在产品代码之前编写。

7.3 单独测试每一个逻辑

测试代码和业务代码一样,也要遵循单一职责原则,每个单元测试只有一个断言。

反例:

import { assert } from 'chai';
describe('AwesomeDate', () => {
  it('handles date boundaries', () => {
    let date: AwesomeDate;
    date = new AwesomeDate('2/1/2016');
    date.addDays(28);
    assert.equal('02/29/2016', date);

    date = new AwesomeDate('2/1/2015');
    date.addDays(28);
    assert.equal('03/01/2015', date);
  });
});

正例:

import { assert } from 'chai';
describe('AwesomeDate', () => {
  it('handles leap year', () => {
    const date = new AwesomeDate('2/1/2016');
    date.addDays(28);
    assert.equal('02/29/2016', date);
  });

  it('handles non-leap year', () => {
    const date = new AwesomeDate('2/1/2015');
    date.addDays(28);
    assert.equal('03/01/2015', date);
  });
});

7.4 测试用例名称应该显示它的意图

同样,也要取一个有意义的用例名字。如果用例出错,根据用例名就可以判断代码大概的问题。在命名上,我们曾在 Java 的 UT 中使用中文来描述用例名,效果也是挺好的。如果是英文,也可以使用 should *** when *** 这样的命名格式,总之,表达清楚即可。

反例:

describe('Calendar', () => {
  it('throws', () => {
    // ...
  });
});

正例:

describe('Calendar', () => {
  it('should throw error when format is invalid', () => {
    // ...
  });
});

8. 格式化与注释

格式化是让代码整洁的一个简单却又重要手段(我在项目组见过,有多年工作经验的老司机也未对代码格式化),但是,格式定义却没有什么硬性规定。争论那种格式更好都是徒劳,浪费时间,在格式化上这点上,最重要的就是要统一,项目或公司级的统一格式规范。确实,很多国内外公司都有自己的代码格式规范。

另外,可以使用工具帮助处理格式。例如,静态分析工具 TSLint,项目中使用可以参考以下 TSLint 配置:

8.1 大小写一致

这是一个主观性的规则,主要看我们怎么选。关键是无论怎么选,都要保持一致

反例:

const DAYS_IN_WEEK = 7;
const daysInMonth = 30;
const users = ['张三', '李四', '王五', '赵六'];
const Books = ['Clean Code', 'TypeScript in Action'];
function addRecord() {}
function remove_record() {}
class user {}
class Book {}

正例:

const DAYS_IN_WEEK = 7;
const DAYS_IN_MONTH = 30;
const USERS = ['张三', '李四', '王五', '赵六'];
const BOOKS = ['Clean Code', 'TypeScript in Action'];
function addRecord() {}
function removeRecord() {}
class User {}
class Book {}

命名规则推荐:

  • 类名、接口名、类型名和命名空间名最好使用帕斯卡命名(Pascal)。
  • 变量、函数和类成员使用驼峰式命名(Camel)。

8.2 把函数和被调函数放在一起

两个函数如果有调用关系,那么把调用者放在被调用者的上方。我们阅读代码时,就会更自然、更顺畅。

8.3 组织导入

对 import 语句进行合理的排序和分组,这样可以快速查看当前代码的依赖关系,应遵循以下规则:

  • Import 语句应该按字母顺序排列和分组。

  • 删除未使用的导入语句。

  • 命名导入必须按字母顺序(例如:import {A, B, C} from 'foo';)。

  • 导入源必须在组中按字母顺序排列。 例如:import * as foo from 'a'; import * as bar from 'b';

  • 导入组用空行隔开。

  • 组内按照如下排序:

    • Polyfills(例如:import 'reflect-metadata';
    • Node 内置模块(例如:import fs from 'fs';
    • 外部模块(例如:import { query } from 'itiriri';
    • 内部模块(例如:import { UserService } from 'src/services/userService';
    • 父目录中的模块(例如:import foo from '../foo'; import qux from '../../foo/qux';
    • 来自相同或兄弟目录的模块(例如:import bar from './bar'; import baz from './bar/baz';

一些 IDE 应该是支持格式化导入语句的。

反例:

import { TypeDefinition } from '../types/typeDefinition';
import { AttributeTypes } from '../model/attribute';
import { ApiCredentials, Adapters } from './common/api/authorization';
import fs from 'fs';
import { ConfigPlugin } from './plugins/config/configPlugin';
import { BindingScopeEnum, Container } from 'inversify';
import 'reflect-metadata';

正例:

import 'reflect-metadata';

import fs from 'fs';
import { BindingScopeEnum, Container } from 'inversify';

import { AttributeTypes } from '../model/attribute';
import { TypeDefinition } from '../types/typeDefinition';

import { ApiCredentials, Adapters } from './common/api/authorization';
import { ConfigPlugin } from './plugins/config/configPlugin';

8.4 路径映射(路径别名)

为了创建简洁的导入语句,可以在 tsconfig.json 中设置编译器选项的 paths 和 baseUrl 属性,这样可以避免导入时使用较长的相对路径。

反例:

import { UserService } from '../../../services/UserService';

正例:

import { UserService } from '@services/UserService';

tsconfig.json 配置:

  "compilerOptions": {
    ...
    "baseUrl": "src",
    "paths": {
      "@services": ["services/*"]
    }
    ...
  }

8.5 合理的注释

在好格式化基础之上,我们要考虑合理的运用注释。好的代码并非不需要注释,合理的注释会帮助理解代码。

在这个问题上,应该争议最大。大概一类认为:代码自解释,另一类则是需要详尽的注释。个人认为只是需要找到一个平衡点即可。对他人来说,我们的代码不可能完整做到自解释,甚至对一段时间后的自己都不行。而合理的注释,只需要在一些关键点上做到点睛即可。

不要注释坏代码,重写吧!——Brian W. Kernighan and P. J. Plaugher

当然对一些烂代码还是要及时重构的。

反例:

// 检查订阅是否处于激活状态
if (subscription.endDate > Date.now) {  }

正例:

// 不需要注释,通过抽取成变量或者函数,通过变量名或者函数名来说明含义。
const isSubscriptionActive = subscription.endDate > Date.now;
if (isSubscriptionActive) { /* ... */ }

8.6 使用版本控制

使用版本控制,而非注释,这样做的好处:

  • 删除注释掉的代码而无需担心。
  • 使用提交日志来替代注释。如,使用 git log 来获取历史提交信息,避免日志中出现日记式注释。

8.7 避免使用注释标记位置

这样的注释干扰正常阅读代码。要让代码结构化,函数和变量要有合适的缩进和格式。

反例:


// User class

class User {

  id: number;
  address: Address;

  
  // public methods
  
  public getInfo(): string {
    // ...
  }

  
  // private methods
  
  private getAddress(): string {
    // ...
  }
};

8.8 TODO 注释

在 Code Review 时会常常会留下很多 // TODO 注释,多数 IDE 都对这类注释提供了支持,一般可快速浏览整个 TODO 列表。

但是,TODO 注释并不是坏代码的借口,要尽快处理掉。

猜你喜欢

转载自blog.csdn.net/weixin_44828588/article/details/127559084