maxueming|2022-10
3.1 使用 getters 和 setters
TypeScript 支持 getter/setter 语法。使用 getter 和 setter 从对象中访问数据比简单地在对象上查找属性要好。原因如下:
反例:
- 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.1 组合大于继承
“四人帮”在《设计模式》中指出:尽可能使用组合而不是继承。如果你默认倾向于继承,那么考虑下组合是否能更好的解决问题。
何时使用继承?需要因地制宜:
设计模式四人帮,又称 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.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.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);
- }
测试至关重要。如果没有测试或用例数量不足,那么每次提交代码时都无法确保不引入问题。
怎样才算是足够的测试?一般情况下都会按照覆盖率来衡量,拥有 100% 的分支覆盖率当然最好。这一切都要基于好的测试框架以及覆盖率工具,如:istanbul。
没有理由不编写测试。有很多优秀的 JS 测试框架都支持 TypeScript,找个合适的,然后为每个新特性/模块编写测试。如果你习惯于测试驱动开发(TDD),那就太好了,重点是确保在开发任何特性或重构现有特性之前,代码覆盖率已经达到要求。
7.1 TDD(测试驱动开发)
TDD 是敏捷开发中的一项核心实践和技术,也是一种设计方法论。TDD 的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。TDD 是 XP(Extreme Programming)的核心实践。它的主要推动者是 Kent Beck。
TDD 三定律:
7.2 F.I.R.S.T. 准则
为了写出有效的测试代码,应遵循以下准则:
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', () => {
- // ...
- });
- });
格式化是让代码整洁的一个简单却又重要手段(我在项目组见过,有多年工作经验的老司机也未对代码格式化),但是,格式定义却没有什么硬性规定。争论那种格式更好都是徒劳,浪费时间,在格式化上这点上,最重要的就是要统一,项目或公司级的统一格式规范。确实,很多国内外公司都有自己的代码格式规范。
另外,可以使用工具帮助处理格式。例如,静态分析工具 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 {}
命名规则推荐:
8.2 把函数和被调函数放在一起
两个函数如果有调用关系,那么把调用者放在被调用者的上方。我们阅读代码时,就会更自然、更顺畅。
8.3 组织导入
对 import 语句进行合理的排序和分组,这样可以快速查看当前代码的依赖关系,应遵循以下规则:
Import 语句应该按字母顺序排列和分组。
删除未使用的导入语句。
命名导入必须按字母顺序(例如:import {A, B, C} from 'foo';)。
导入源必须在组中按字母顺序排列。 例如:import * as foo from 'a'; import * as bar from 'b';
导入组用空行隔开。
组内按照如下排序:
import 'reflect-metadata';)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 注释并不是坏代码的借口,要尽快处理掉。