• TS代码整洁之道(下)


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

    maxueming|2022-10


    3. 对象和数据结构

    3.1 使用 getters 和 setters

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

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

    反例:

    1. class BankAccount {
    2. balance: number = 0;
    3. // ...
    4. }
    5. const value = 100;
    6. const account = new BankAccount();
    7. if (value < 0) {
    8. throw new Error('Cannot set negative balance.');
    9. }
    10. account.balance = value;

    正例:

    1. class BankAccount {
    2. private accountBalance: number = 0;
    3. get balance(): number {
    4. return this.accountBalance;
    5. }
    6. set balance(value: number) {
    7. if (value < 0) {
    8. throw new Error('Cannot set negative balance.');
    9. }
    10. this.accountBalance = value;
    11. }
    12. // ...
    13. }
    14. const account = new BankAccount();
    15. account.balance = 100;

    3.2 让对象拥有 private/protected 成员

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

    反例:

    1. class Circle {
    2. radius: number;
    3. constructor(radius: number) {
    4. this.radius = radius;
    5. }
    6. surface() {
    7. return Math.PI * this.radius * this.radius;
    8. }
    9. }

    正例:

    1. class Circle {
    2. constructor(private readonly radius: number) {
    3. }
    4. surface(){
    5. return Math.PI * this.radius * this.radius;
    6. }
    7. }

    3.3 不变性

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

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

    反例:

    1. interface Config {
    2. host: string;
    3. port: string;
    4. db: string;
    5. }

    正例:

    1. interface Config {
    2. readonly host: string;
    3. readonly port: string;
    4. readonly db: string;
    5. }

    3.4 类型 vs 接口

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

    示例:

    1. interface Shape {
    2. }
    3. class Circle implements Shape {
    4. // ...
    5. }
    6. class Square implements Shape {
    7. // ...
    8. }

    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,第一次将设计模式提升到理论高度,并将之规范化。

    反例:

    1. class User {
    2. constructor(
    3. private readonly name: string,
    4. private readonly id: string) {
    5. }
    6. // ...
    7. }
    8. // 用户工作信息并不是一类用户,这种继承有问题
    9. class UserJob extends User {
    10. constructor(
    11. name: string,
    12. id: string,
    13. private readonly company: string,
    14. private readonly salary: number) {
    15. super(name, id);
    16. }
    17. // ...
    18. }

    正例:

    1. class User {
    2. private job: UserJob; // 使用组合
    3. constructor(
    4. private readonly name: string,
    5. private readonly id: string) {
    6. }
    7. setJob(company: string, salary: number): User {
    8. this.job = new UserJob(company, salary);
    9. return this;
    10. }
    11. // ...
    12. }
    13. class UserJob {
    14. constructor(
    15. public readonly company: string,
    16. public readonly salary: number) {
    17. }
    18. // ...
    19. }

    4.2 链式调用

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

    反例:

    1. class QueryBuilder {
    2. private collection: string;
    3. private pageNumber: number = 1;
    4. private itemsPerPage: number = 100;
    5. private orderByFields: string[] = [];
    6. from(collection: string): void {
    7. this.collection = collection;
    8. }
    9. page(number: number, itemsPerPage: number = 100): void {
    10. this.pageNumber = number;
    11. this.itemsPerPage = itemsPerPage;
    12. }
    13. orderBy(...fields: string[]): void {
    14. this.orderByFields = fields;
    15. }
    16. build(): Query {
    17. // ...
    18. }
    19. }
    20. // ...
    21. const query = new QueryBuilder();
    22. query.from('users');
    23. query.page(1, 100);
    24. query.orderBy('firstName', 'lastName');
    25. const query = queryBuilder.build();

    正例:

    1. class QueryBuilder {
    2. private collection: string;
    3. private pageNumber: number = 1;
    4. private itemsPerPage: number = 100;
    5. private orderByFields: string[] = [];
    6. from(collection: string): this {
    7. this.collection = collection;
    8. return this;
    9. }
    10. page(number: number, itemsPerPage: number = 100): this {
    11. this.pageNumber = number;
    12. this.itemsPerPage = itemsPerPage;
    13. return this;
    14. }
    15. orderBy(...fields: string[]): this {
    16. this.orderByFields = fields;
    17. return this;
    18. }
    19. build(): Query {
    20. // ...
    21. }
    22. }
    23. // ... 链式调用
    24. const query = new QueryBuilder()
    25. .from('users')
    26. .page(1, 100)
    27. .orderBy('firstName', 'lastName')
    28. .build();

    4.3 SOLID 原则

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

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

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

    反例:

    1. class UserSettings {
    2. constructor(private readonly user: User) {
    3. }
    4. changeSettings(settings: UserSettings) {
    5. if (this.verifyCredentials()) {
    6. // ...
    7. }
    8. }
    9. verifyCredentials() {
    10. // 和 UserSettings 没有关系的逻辑
    11. // ...
    12. }
    13. }

    正例:

    1. class UserAuth {
    2. constructor(private readonly user: User) {
    3. }
    4. verifyCredentials() {
    5. // ...
    6. }
    7. }
    8. class UserSettings {
    9. private readonly auth: UserAuth;
    10. constructor(private readonly user: User) {
    11. this.auth = new UserAuth(user);
    12. }
    13. changeSettings(settings: UserSettings) {
    14. if (this.auth.verifyCredentials()) {
    15. // ...
    16. }
    17. }
    18. }

    4.3.2 开闭原则(Open Closed Principle)

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

    反例:

    1. class AjaxAdapter extends Adapter {
    2. constructor() {
    3. super();
    4. }
    5. // ...
    6. }
    7. class NodeAdapter extends Adapter {
    8. constructor() {
    9. super();
    10. }
    11. // ...
    12. }
    13. class HttpRequester {
    14. constructor(private readonly adapter: Adapter) {}
    15. async fetch<T>(url: string): Promise<T> {
    16. // 对于不同的 adapter 都要做不同处理,如果新增一类 adapter 需要在此处新增处理逻辑
    17. if (this.adapter instanceof AjaxAdapter) {
    18. const response = await makeAjaxCall<T>(url);
    19. } else if (this.adapter instanceof NodeAdapter) {
    20. const response = await makeHttpCall<T>(url);
    21. }
    22. }
    23. }
    24. function makeAjaxCall<T>(url: string): Promise<T> {
    25. // 请求并返回 promise
    26. }
    27. function makeHttpCall<T>(url: string): Promise<T> {
    28. // 请求并返回 promise
    29. }

    正例:

    1. abstract class Adapter {
    2. abstract async request<T>(url: string): Promise<T>;
    3. }
    4. class AjaxAdapter extends Adapter {
    5. // ...
    6. async request<T>(url: string): Promise<T>{
    7. // 请求并返回 promise
    8. }
    9. }
    10. class NodeAdapter extends Adapter {
    11. //...
    12. async request<T>(url: string): Promise<T>{
    13. // 请求并返回 promise
    14. }
    15. }
    16. class HttpRequester {
    17. constructor(private readonly adapter: Adapter) {}
    18. // 新增一类 adapter,此处代码不需要做任何处理
    19. async fetch<T>(url: string): Promise<T> {
    20. const response = await this.adapter.request<T>(url);
    21. }
    22. }

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

    听起来有点懵?是的!

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

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

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

    反例:

    1. class Rectangle {
    2. constructor(
    3. protected width: number = 0,
    4. protected height: number = 0) {
    5. }
    6. setWidth(width: number) {
    7. this.width = width;
    8. }
    9. setHeight(height: number) {
    10. this.height = height;
    11. }
    12. getArea(): number {
    13. return this.width * this.height;
    14. }
    15. }
    16. class Square extends Rectangle {
    17. setWidth(width: number) {
    18. this.width = width;
    19. this.height = width;
    20. }
    21. setHeight(height: number) {
    22. this.width = height;
    23. this.height = height;
    24. }
    25. }
    26. function renderLargeRectangles(rectangles: Rectangle[]) {
    27. rectangles.forEach((rectangle) => {
    28. rectangle.setWidth(4);
    29. rectangle.setHeight(5);
    30. const area = rectangle.getArea(); // 当传入的是 Square 时,返回了 25,应该是 20。没有遵守里氏替换原则
    31. // ...
    32. });
    33. }
    34. const rectangles = [new Rectangle(), new Rectangle(), new Square()];
    35. renderLargeRectangles(rectangles);

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

    正例:

    1. abstract class Shape {
    2. abstract getArea(): number; // 抽象
    3. }
    4. class Rectangle extends Shape {
    5. constructor(
    6. private readonly width = 0,
    7. private readonly height = 0) {
    8. super();
    9. }
    10. getArea(): number {
    11. return this.width * this.height;
    12. }
    13. }
    14. class Square extends Shape {
    15. constructor(private readonly length: number) {
    16. super();
    17. }
    18. getArea(): number {
    19. return this.length * this.length;
    20. }
    21. }
    22. function renderLargeShapes(shapes: Shape[]) {
    23. shapes.forEach((shape) => {
    24. const area = shape.getArea();
    25. // ...
    26. });
    27. }
    28. const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
    29. renderLargeShapes(shapes);

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

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

    反例:

    1. interface ISmartPrinter {
    2. print();
    3. fax();
    4. scan();
    5. }
    6. class AllInOnePrinter implements ISmartPrinter {
    7. print() {
    8. // ...
    9. }
    10. fax() {
    11. // ...
    12. }
    13. scan() {
    14. // ...
    15. }
    16. }
    17. class EconomicPrinter implements ISmartPrinter {
    18. print() {
    19. // ...
    20. }
    21. fax() {
    22. throw new Error('Fax not supported.');
    23. }
    24. scan() {
    25. throw new Error('Scan not supported.');
    26. }
    27. }

    正例:

    1. interface IPrinter {
    2. print();
    3. }
    4. interface IFax {
    5. fax();
    6. }
    7. interface IScanner {
    8. scan();
    9. }
    10. class AllInOnePrinter implements IPrinter, IFax, IScanner {
    11. print() {
    12. // ...
    13. }
    14. fax() {
    15. // ...
    16. }
    17. scan() {
    18. // ...
    19. }
    20. }
    21. class EconomicPrinter implements IPrinter {
    22. print() {
    23. // ...
    24. }
    25. }

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

    这个原则有两个要点:

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

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

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

    反例:

    1. import { readFile as readFileCb } from 'fs';
    2. import { promisify } from 'util';
    3. const readFile = promisify(readFileCb);
    4. type ReportData = {
    5. // ..
    6. }
    7. class XmlFormatter {
    8. parse<T>(content: string): T {
    9. // 转换 XML 字符串
    10. }
    11. }
    12. class ReportReader {
    13. // 这里已经对具体的实现 XmlFormatter 产生了依赖,实际上只需要依赖方法:parse
    14. private readonly formatter = new XmlFormatter();
    15. async read(path: string): Promise<ReportData> {
    16. const text = await readFile(path, 'UTF8');
    17. return this.formatter.parse<ReportData>(text);
    18. }
    19. }
    20. // ...
    21. const reader = new ReportReader();
    22. await report = await reader.read('report.xml');

    正例:

    1. import { readFile as readFileCb } from 'fs';
    2. import { promisify } from 'util';
    3. const readFile = promisify(readFileCb);
    4. type ReportData = {
    5. // ..
    6. }
    7. interface Formatter {
    8. parse<T>(content: string): T;
    9. }
    10. class XmlFormatter implements Formatter {
    11. parse<T>(content: string): T {
    12. // 转换 XML 字符串
    13. }
    14. }
    15. class JsonFormatter implements Formatter {
    16. parse<T>(content: string): T {
    17. // 转换 Json 字符串
    18. }
    19. }
    20. class ReportReader {
    21. // 只依赖了抽象,也就是接口 Formatter,而非它的具体实现 XmlFormatter 或 JsonFormatter
    22. constructor(private readonly formatter: Formatter){}
    23. async read(path: string): Promise<ReportData> {
    24. const text = await readFile(path, 'UTF8');
    25. return this.formatter.parse<ReportData>(text);
    26. }
    27. }
    28. const reader = new ReportReader(new XmlFormatter());
    29. await report = await reader.read('report.xml');
    30. const reader = new ReportReader(new JsonFormatter());
    31. await report = await reader.read('report.json');

    5. 并发

    5.1 用 Promises 替代回调

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

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

    反例:

    1. import { get } from 'request';
    2. import { writeFile } from 'fs';
    3. // 多层嵌套
    4. function downloadPage(url: string, saveTo: string, callback: (error: Error, content?: string) => void){
    5. get(url, (error, response) => {
    6. if (error) {
    7. callback(error);
    8. } else {
    9. writeFile(saveTo, response.body, (error) => {
    10. if (error) {
    11. callback(error);
    12. } else {
    13. callback(null, response.body);
    14. }
    15. });
    16. }
    17. })
    18. }
    19. downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html', (error, content) => {
    20. if (error) {
    21. console.error(error);
    22. } else {
    23. console.log(content);
    24. }
    25. });

    正例:

    1. import { get } from 'request';
    2. import { writeFile } from 'fs';
    3. import { promisify } from 'util';
    4. const write = promisify(writeFile);
    5. function downloadPage(url: string, saveTo: string): Promise<string> {
    6. return get(url).then(response => write(saveTo, response))
    7. }
    8. downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html')
    9. .then(content => console.log(content))
    10. .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 关键字上的代码执行。

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

    1. import { get } from 'request';
    2. import { writeFile } from 'fs';
    3. import { promisify } from 'util';
    4. const write = promisify(writeFile);
    5. async function downloadPage(url: string, saveTo: string): Promise<string> {
    6. const response = await get(url);
    7. await write(saveTo, response);
    8. return response;
    9. }
    10. try {
    11. const content = await downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html');
    12. console.log(content);
    13. } catch (error) {
    14. console.error(error);
    15. }

    6. 错误处理

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

    6.1 抛出 Error 或 使用 reject

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

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

    反例:

    1. function calculateTotal(items: Item[]): number {
    2. throw 'Not implemented.';
    3. }
    4. function get(): Promise<Item[]> {
    5. return Promise.reject('Not implemented.');
    6. }

    正例:

    1. function calculateTotal(items: Item[]): number {
    2. throw new Error('Not implemented.');
    3. }
    4. function get(): Promise<Item[]> {
    5. return Promise.reject(new Error('Not implemented.'));
    6. }

    或者:

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

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

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

    1. type Failable<R, E> = {
    2. isError: true;
    3. error: E;
    4. } | {
    5. isError: false;
    6. value: R;
    7. }
    8. function calculateTotal(items: Item[]): Failable<number, 'empty'> {
    9. if (items.length === 0) {
    10. return { isError: true, error: 'empty' };
    11. }
    12. // ...
    13. return { isError: false, value: 42 };
    14. }

    详细解释请参考原文

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

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

    反例:

    1. try {
    2. throwError();
    3. } catch (error) {
    4. // 打印到控制台,或则直接忽略。
    5. // ignore error
    6. }

    正例:

    1. import { logger } from './logging'
    2. try {
    3. throwError();
    4. } catch (error) {
    5. logger.log(error);
    6. }

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

    1. import { logger } from './logging'
    2. getUser()
    3. .then((user: User) => {
    4. return sendEmail(user.email, 'Welcome!');
    5. }).catch((error) => {
    6. logger.log(error);
    7. });

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

    1. try {
    2. const user = await getUser();
    3. await sendEmail(user.email, 'Welcome!');
    4. } catch (error) {
    5. logger.log(error);
    6. }

    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 单独测试每一个逻辑

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

    反例:

    1. import { assert } from 'chai';
    2. describe('AwesomeDate', () => {
    3. it('handles date boundaries', () => {
    4. let date: AwesomeDate;
    5. date = new AwesomeDate('2/1/2016');
    6. date.addDays(28);
    7. assert.equal('02/29/2016', date);
    8. date = new AwesomeDate('2/1/2015');
    9. date.addDays(28);
    10. assert.equal('03/01/2015', date);
    11. });
    12. });

    正例:

    1. import { assert } from 'chai';
    2. describe('AwesomeDate', () => {
    3. it('handles leap year', () => {
    4. const date = new AwesomeDate('2/1/2016');
    5. date.addDays(28);
    6. assert.equal('02/29/2016', date);
    7. });
    8. it('handles non-leap year', () => {
    9. const date = new AwesomeDate('2/1/2015');
    10. date.addDays(28);
    11. assert.equal('03/01/2015', date);
    12. });
    13. });

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

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

    反例:

    1. describe('Calendar', () => {
    2. it('throws', () => {
    3. // ...
    4. });
    5. });

    正例:

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

    8. 格式化与注释

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

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

    8.1 大小写一致

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

    反例:

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

    正例:

    1. const DAYS_IN_WEEK = 7;
    2. const DAYS_IN_MONTH = 30;
    3. const USERS = ['张三', '李四', '王五', '赵六'];
    4. const BOOKS = ['Clean Code', 'TypeScript in Action'];
    5. function addRecord() {}
    6. function removeRecord() {}
    7. class User {}
    8. 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 应该是支持格式化导入语句的。

    反例:

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

    正例:

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

    8.4 路径映射(路径别名)

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

    反例:

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

    正例:

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

    tsconfig.json 配置:

    1. "compilerOptions": {
    2. ...
    3. "baseUrl": "src",
    4. "paths": {
    5. "@services": ["services/*"]
    6. }
    7. ...
    8. }

    8.5 合理的注释

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

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

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

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

    反例:

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

    正例:

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

    8.6 使用版本控制

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

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

    8.7 避免使用注释标记位置

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

    反例:

    1. // User class
    2. class User {
    3. id: number;
    4. address: Address;
    5. // public methods
    6. public getInfo(): string {
    7. // ...
    8. }
    9. // private methods
    10. private getAddress(): string {
    11. // ...
    12. }
    13. };

    8.8 TODO 注释

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

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

  • 相关阅读:
    提交SCI论文修改稿时标注修改文字颜色
    解决 tesserocr报错 Failed to init API, possibly an invalid tessdata path : ./
    java-net-php-python-jspm家教信息管理系统(2)计算机毕业设计程序
    C++ opencv模板匹配
    rust是否可以用于8051单片机开发工作?
    古代汉语(王力版)笔记 绪论
    使用kubeadm搭建高可用集群-k8s相关组件及1.16版本的安装部署
    进程之间的通信(管道详解)
    TCP沾包问题
    LeetCode-102.题: 二叉树的层序遍历(原创)
  • 原文地址:https://blog.csdn.net/weixin_44828588/article/details/127559084