共计 10153 个字符,预计需要花费 26 分钟才能阅读完成。
原文链接适用于 TypeScript 的 Clean Code(简洁代码)概念启发来自于 clean-code-javascript
介绍
软件工程原理,来自于 Robert C.Martin’s 的《Clean Code》一书,适用于 Typescript.clean-code-typescript 不是一个风格指南,它是一个在 TypeScript 中生成可读的、可重用的和可重构的软件的指南。
并非每个原则都需要我们严格的遵循,原则越少越容易得到大家的认同。这里仅仅有一些准则,但这些准则是 Clean Code 作者多年经验的总结。
我们的软件工程仅仅只有 50 多年的历史,并且我们还在不断的学习。当软件架构与架构一样古老时,说不定我们还会有更难遵循的规则。但是现在,现在,让这些指南作为评估您和您的团队生成的 TypeScript 代码质量的试金石。
还有一件事:了解这些规范并不会让你立即就成为一位更好的开发人员,按照这个规范工作也并不会意味着你再未来多年内,不会犯错。每一段代码一开始都是一个初稿,就像黏土被塑造成最终形状前一样。最后,我们和我们的同行一起审查,然后凿掉不完美的地方。不要因为需要改进初稿而退缩,一起打败代码吧!
变量
使用具有意义的变量名称
已这种方式提供名称,能够让读者很轻松的知道这个变量提供了什么。Bad
function between<T>(a1: T, a2: T, a3: T): boolean {
return a2 <= a1 && a1 <= a3;
}
Good
function between<T>(value: T, left: T, right: T): boolean {
return left <= value && value <= right;
}
使用可读的变量名称
如果这个变量名称不能发音,你将会像个白痴一样和别人讨论这个变量。Bad
type DtaRcrd102 = {
genymdhms: Data;
modymdhms: Data;
pszqint: number;
}
Good
type Customer = {
generationTimestamp: Date;
modificationTimestamp: Date;
recordId: number;
}
对用相同类型的变量使用相同的词汇
Bad
function getUserInfo(): User;
function getUserDetails(): User;
function getUserData(): User;
Good
function getUser(): User;
使用可搜索的名称
我们会阅读比我们写的代码更多的代码。我们编写的代码是可读的、可搜索的,这一点很重要。对有意义的变量不进行命名最终将会导致我们的程序更难理解。确保你的变量名称是可搜索的。像 TSLint 这样的工具可以帮助识别未命名的常量。Bad
// What the heck is 86400000 for?
setTimeout(restart, 86400000);
Good
// Declare them as capitalized named constants.
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
setTimeout(restart, MILLISECONDS_IN_A_DAY);
使用带有解释性的变量
Bad
declare const users: Map<string, User>;
for (const keyValue of users) {
// iterate through users map
}
Good
declare const users: Map<string, User>;
for (const [id, user] of users) {
// iterate through users map
}
避免心理映射
显式永远优于隐式。清晰表达才是王道!Bad
const u = getUser();
const s = getSubscription();
const t = charge(u, s);
Good
const user = getUser();
const subscription = getSubscription();
const transaction = charge(user, subscription);
不要添加不需要的上下文
如果你的类 / 类型 / 对象的名称已经告诉了你一些信息,不要在变量中再重复这些信息。Bad
type Car = {
carMake: string;
carModel: string;
carColor: string;
}
function print(car: Car): void {
console.log(`${car.carMake} ${car.carModel} (${car.carColor})`);
}
Good
type Car = {
make: string;
model: string;
color: string;
}
function print(car: Car): void {
console.log(`${car.make} ${car.model} (${car.color})`);
}
使用默认参数而不是判断条件
默认参数通常比短路更清晰。Bad
function loadPages(count?: number) {
const loadCount = count !== undefined ? count : 10;
// …
}
Good
function loadPages(count: number = 10) {
// …
}
函数
函数参数(2 个或者更少)
限制函数的参数数量这一点非常的重要,更少的参数意味着更加容易测试。超过 3 个的参数会使你必须为多种可能编写多个单独的变量来进行测试,这是一件非常痛苦的事情。只有一个或两个参数是一个理想情况,如果可能的话,我们尽量避免 3 个参数。应该整合除此之外的任何东西。通常,如果你的函数有两个以上的参数,那么你的函数可能试图做过多的事情了。如果不是,大多数情况下,你可以通过更高级别的对象来作为参数。如果你发现你需要大象的参数,请尝试使用对象。为了明确函数所期望的属性,你能够使用 destructuring 语句。这有游一些优点:
当有人查看函数签名的时候,他会立刻知道正在使用的属性
destructuring 还会克隆传递给函数的参数对象的指定原始值。这有助于防止副作用。注意:解构对象中的 object 和 array 不会被克隆
TypeScript 会警告您未使用的属性,如果没有 destructuring,这将是不可能的。
Bad
function createMenu(title: string, body: string, buttonText: string, cancellable: boolean) {
// …
}
createMenu(‘Foo’, ‘Bar’, ‘Baz’, true);
Good
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
});
函数应该只做一件事
这是迄今为止软件工程中最重要的规则。当一个函数做更多的事情时,他们更难编写,测试和推理。当你的函数只执行一个操作时,你的函数将更容易重构,并且你的代码也更加清晰。Bad
function emailClients(clients: Client) {
clients.forEach((client) => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
Good
function emailClients(clients: Client) {
clients.filter(isActiveClient).forEach(email);
}
function isActiveClient(client: Client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}
函数名称应该说明它们的作用
Bad
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);
Good
function addMonthToDate(date: Date, month: number): Date {
// …
}
const date = new Date();
addMonthToDate(date, 1);
函数应该只是一个抽象级别
当你有多个抽象级别时,你的函数通常做得太多了. 拆分功能可以实现可重用性和更轻松的测试。Bad
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…
});
}
Good
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 原则. 糟糕的抽象比重复代码更加糟糕,所以请更加小心。说完这个,如果你能做出很好的抽象,那就做吧!不要重复自己,否则你会发现自己在想要改变一件事的时候更新多个地方。Bad
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);
});
}
Good
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);
});
}
你应该对重复代码持批评态度。有时候,你需要在重复代码和引入不必要的抽象带来的复杂度之间权衡。当来自于不同模块的两个实现看起来很相似但又存在于不同的域当中,重复代码是可以被接受的,并且它优于提取一个公共代码。在这种情况下,提取公共代码,将会引入两个模块之间的间接依赖关系。
设置默认对象和对象属性或解构(destructuring)
Bad
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’})
Good
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’});
通过显式传入 undefined 或 null 值来避免任何副作用和意外行为, 你可以告诉 TypeScript 不允许这样做。TypeScript 选项 –strictNullChecks
不要使用标志作为函数参数
标志会告诉你的用户这个函数不止做一件事,但函数应该只做一件事。如果你的函数按照布尔值来判断执行不同的代码,请将你的函数拆分。Bad
function createFile(name: string, temp: boolean) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}
Good
function createTempFile(name: string) {
createFile(`./temp/${name}`);
}
function createFile(name: string) {
fs.create(name);
}
避免副作用(1)
如果一个函数除了取参数并返回一个或多个值以外还执行其他任何操作,则这个函数产生了副作用。这个副作用可能是写一个文件,修改一些全局变量,或者将所有资金都给了一个陌生人。现在,你确实需要副作用。就像前面的例子,你可能需要写入文件。不要有多个函数和类来写入同一个特定的文件,而是应该提供一个唯一的服务来做这个事情。Bad
// Global variable referenced by following function.
let name = ‘Robert C. Martin’;
function toBase64() {
name = btoa(name);
}
toBase64();
// If we had another function that used this name, now it’d be a Base64 value
console.log(name); // expected to print ‘Robert C. Martin’ but instead ‘Um9iZXJ0IEMuIE1hcnRpbg==’
Good
const name = ‘Robert C. Martin’;
function toBase64(text: string): string {
return btoa(text);
}
const encodedName = toBase64(name);
console.log(name);
避免副作用(2)
在 JavaScript 中,值、对象、数组是通过传递引用来传递的。对于对象和数组,如果你的函数直接去改变你的数组,如例子,直接添加物品到当前购物车下,其他使用这个购物车的函数的功能将会受到影响。这可能很好,但也可能很糟糕,让我们想象一个糟糕的情况:用户点击了“购买”按钮,按钮调用了购买函数,该函数将购物车数组发送到服务器。由于网络连接不好,购买函数必须重试。现在,如果在此期间用户在网络请求开始之前意外地点击了他们实际上不想要的项目上的“添加到购物车”按钮,该怎么办?如果这发生在网络请求之前,则购买功能将会意外的添加一个项目,因为 addItemToCart 函数通过对购物车的引用,将不需要的项目添加进入了购物车。一个很好的解决方案是,addItemCart 始终克隆购物车,并对克隆的购物车进行修改并返回。这样可以确保其他使用购物车的函数不会受到任何更改的影响。有两个需要注意的地方:1. 在某些情况下,你可能确实需要修改输入的对象,但如果你使用本指南进行编程时,你会发现这种情况非常少。大多数情况下都可以重构为没有副作用的函数。(参考纯函数)2. 克隆大对象对性能上的影响是非常严重的。幸运的是,这在实际开发中并不是一个大问题,因为有很好的库让这种编程方法变快,而不用手动克隆以占用大量的内存。Bad
function addItemToCart(cart: CartItem[], item: Item): void {
cart.push({item, date: Date.now() });
};
Good
function addItemToCart(cart: CartItem[], item: Item): CartItem[] {
return […cart, { item, date: Date.now() }];
};
不要写全局函数
污染全局是一个非常糟糕的做法,因为这样做你可能会和其他库冲突,并且你的 api 用户在生产中获得异常之前,都不会很好的去处理。让我们考虑一个例子:如果你想扩展 JavaScript 的原生 Array 方法以获得一个可以显示两个数组之间差异的 diff 方法,该怎么办?你可以写一个新的函数到 Array.prototype 中,但是他可能会与另外一个试图做同样事情的库发生冲突。如果那个其他库只是使用 diff 来找到数组的第一个和最后一个元素之间的区别怎么办?这就是为什么使用类并简单地扩展 Array 比全局函数更好的原因。Bad
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));
};
}
good
class MyArray<T> extends Array<T> {
diff(other: T[]): T[] {
const hash = new Set(other);
return this.filter(elem => !hash.has(elem));
};
}
多使用函数式编程而不是命令式编程
尽可能多的使用这种编程方式 Bad
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;
}
Good
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);
封装条件语句
Bad
if (subscription.isTrial || account.balance > 0) {
// …
}
Good
function canActivateService(subscription: Subscription, account: Account) {
return subscription.isTrial || account.balance > 0
}
if (canActivateService(subscription, account)) {
// …
}
避免负条件
Bad
function isEmailNotUsed(email: string): boolean {
// …
}
if (isEmailNotUsed(email)) {
// …
}
Good
function isEmailUsed(email): boolean {
// …
}
if (!isEmailUsed(node)) {
// …
}