代码整洁之道

普通人读的懂的代码, 才是好代码 --- 沃滋基梭德

概述篇

  1. 代码可读性 代码必须是对人类是可读的。不要考虑计算机如何处理它,因为会有许多工具来转换我们的代码(编译器)。因此,最重要的是,代码将是人类可读的,因为你在开发代码时,最长的工作就是阅读代码,而不是写代码。

const users = [{ id: 1, name: "Carlos Caballero", memberSince: "1997–04–20", favoriteLanguageProgramming: ["JavaScript", "C", "Java"] }, { id: 2, name: "Antonio Villena", memberSince: "2014–08–15", favoriteLanguageProgramming: ["Go", "Python", "JavaScript"] }, { id: 3, name: "Jesús Segado", memberSice: "2015–03–15", favoriteLanguageProgramming: ["PHP", "JAVA", "JavaScript"] } ];

/***********************/

const users = [
    { id: 1, name: "Carlos Caballero", memberSince: "1997–04–20", favoriteLanguageProgramming: ["JavaScript", "C", "Java"] },
    { id: 2, name: "Antonio Villena", memberSince: "2014–08–15", favoriteLanguageProgramming: ["Go", "Python", "JavaScript"] },
    { id: 3, name: "Jesús Segado", memberSice: "2015–03–15", favoriteLanguageProgramming: ["PHP", "JAVA", "JavaScript"] },
    ];


/***********************/

const users = [{
    id: 1,
    name: "Carlos Caballero",
    memberSince: "1997–04–20",
    favoriteLanguageProgramming: ["JavaScript", "C", "Java"],
},
{
    id: 2,
    name: "Antonio Villena",
    memberSince: "2014–08–15",
    favoriteLanguageProgramming: ["Go", "Python", "JavaScript"],
},
{
    id: 3,
    name: "Jesús Segado",
    memberSince: "2015–03–15",
    favoriteLanguageProgramming: ["PHP", "JAVA", "JavaScript"],
}];


  1. 使用英语编写代码

const benutzer = {
     id: 1,
     name: "John Smith",
     mitgliedVon: "1997–04–20",
    };

    Gehaltserhöhung(benutzer, 1000); 

    /***********************/

    const użytkownik = {
     id: 1,
     imię: "John Smith",
     członekZ: "1997–04–20",
    };
    wzrostWynagrodzeń(użytkownik, 1000);

    /***********************/

    const user = {
     id: 1,
     name: "John Smith",
     memberSince: "1997–04–20",
    };
    increaseSalary(user, 1000);
  1. 团队协作 可以通过配置一个标准的 .editorconfig 文件

  • 用空格还是 Tab 来格式化代码

  • 在函数的名称旁边还是在下一行写大括号

  • 是否在语句的结尾处放一个分号

root = true

    [*]
    end_of_line = lf
    insert_final_newline = true

    [*.{js,py}]
    charset = utf-8

    [*.py]
    indent_style = space
    indent_size = 4

    [Makefile]
    indent_style = tab

    [*.js]
    indent_style = space
    indent_size = 2

    [{package.json,.travis.yml}]
    indent_style = space
    indent_size = 2

有一个在业界广泛使用的工具被称为 Prettier,它能够根据 linter 的规则实时改变我们的代码格式(IDE的插件)。

变量篇

  1. 变量名要名副其实 变量的名称必须能够描述出该变量的作用和用途。

  2. 变量名可以读出来

在选择变量的名称时,另一个错误的行为是删除一个词中某些字母,使用这些缩略语读起来很困难。首先,我们是在用英语编码,而且不是所有的开发者都是讲英语的。因此,我们为什么要把它的名字减少 3 或 4 个字符?这有什么好处呢?代码会被工具(转译器包括其他语言的编译器)操作,最终正确地完成格式化(使用 Prettier)。因此,把不能发音的名字放在一起,只能让我们更费力地去推断变量的用途。

  1. 不要在名称中使用变量的类型 在变量名称中使用数据类型作为前缀是一个很古老的做法,现在让我们反思一下这个问题。

变量名称中必须要用类型作为前缀吗? 每个公司和工程都有各自的前缀规范,那如何去学习和书写这种代码呢? 如果我在变量的名称中使用一种编程语言的类型系统,为什么要用它呢? 当变量的数据类型发生变化时,比如把 Array 修改为 Map,这种情况怎么处理? 这个前缀能给我们带来什么?它是可以发音的吗?

const aCountries = [];
const sName = "";
const dAmount = 3.2;

const countries = [];
const name = "";
const amount = 3.2;

  1. 对同一类型的变量使用相同的词汇表 对同一类型的数据使用相同的词汇表。如果我们需要检索一个用户或客户的信息,我们不能以不同的方式称呼用户或客户。也就是说,不能有时称其为 user,有时称其为 customer,甚至是 client 这个词。更不可 取的是,在变量名称上额外再加一个后缀。 下面代码就是很好的示例,同一个概念,有三个不同的定义。必须自始至终使用统一的命名,不管是 user、customer 还是 client,只能用同一个。

getUserInfo();
getClientData();
getCustomerRecord(); 

getUser();
  1. 不要添加不需要的上下文 在变量名称中没有必要添加类或包的相关上下文。 在变量名称中添加上下文是很常见的,这样可以知道这个变量位于哪个工作区。

const Car = {
    carMake: 'Honda',
    carModel: 'Accord',
    carColor: 'Blue',
}

function paintCar(car){
    car.carColor = 'red'
}

const Car = {
    make: 'Honda',
    model: 'Accord',
    color: 'Blue'
}
function paint(car){
    car.color = 'Red'
}
  1. 不要使用魔法数字和字符串 在编写代码时,不应该在源代码中直接使用数字或文本字符串,这些通常也被称为魔法数字.这个数字是什么意思?必须要解释这个数字吗?这让我们不得不思考业务逻辑之外的事情。

这些魔法数字或字符串必须存储在常量中,通过对常量的名称来表达出它们的用途。在业务逻辑层面上,对于那些有意义的数字或文本字符串,如果没有一个确切的名字就会引起噪音。

在业务逻辑层面上,对于那些有意义的数字或文本字符串,如果没有一个确切的名字就会引起噪音。

// what the heck is 86400000 for?
setTimeout(blastOff, 864000000);
user.rol = 'Administrator';

const MILLISECONDS_IN_A_DAY = 'Administrator';

setTimeout(blastOff, MILLISECONDS_IN_A_DAY);
user.rol = ADMINISTRATOR_ROL

复杂判断

  1. 不要使用标记 (flag) 作为函数参数 这样会破坏函数功能的单一性,我们必须创建两个函数来实现各自对应的逻辑功能,而不是使用一个函数来实现两个逻辑功能,因为他们是不同的功能。

// Dirty
function book(customer, isPremium) {
  // ...
  if (isPremium) {
    premiumLogic();
  } else {
    regularLogic();
  }
}

// Clean (Declarative way)
function bookPremium (customer) {
  premiumLogic();
}

function bookRegular (customer) {
  retularLogic();
}
  1. 封装判断条件 条件封装在具有明确语义的函数中

if (platform.state === 'fetching' && isEmpty(cart)) {
    // ...
}


function showLoading(platform, cart) {
    return platform.state === 'fetching' && isEmpty(cart);
}

if (showLoading(platform, cart)) {
    // ...
}
  1. 用卫语句替换嵌套的条件语句

function getPayAmount() {
    let result;
    if (isDead){
        result = deadAmount();
    } else {
        if (isSeparated){
            result = separatedAmount();
        } else {
            if (isRetired){
                result = retiredAmount();
            }else{
                result = normalPayAmount();
            }
        }
    }
    return result;
}


function getPayAmount() {
    if (isDead) return deadAmount();
    if (isSeparated) return separatedAmount();
    if (isRetired) return retiredAmount();
    return normalPayAmount();
}
  1. 空对象 不断检查对象是否为null, 并根据该检查判断是否显示默认操作。这种模式称为空对象模式,我们默认封装了空对象行为的对象

class Dog {
  sound() {
    return 'bark';
  }
}

['dog', null].map((animal) => {
  if(animal !== null) { 
    sound(); 
  }
 });

class Dog {
  sound() {
    return 'bark';
  }
}

class NullAnimal {
  sound() {
    return null;
  }
}

function getAnimal(type) {
  return type === 'dog' ? new Dog() : new NullAnimal();
}

['dog', null].map((animal) => getAnimal(animal).sound());
// Returns ["bark", null]
  1. 使用多态删除条件 通过判断对象的类型去定义对象的方法,但是这里的条件语句被滥用了,在这种场景下,我们可以通过类的继承,为每个特定类型创建一个类,利用多态来避免使用条件判断。

function Auto() {
}
Auto.prototype.getProperty = function () {
    switch (type) {
        case BIKE:
            return getBaseProperty();
        case CAR:
            return getBaseProperty() - getLoadFactor();
        case BUS:
            return (isNailed) ? 
            0 : 
            getBaseProperty(voltage);
    }
    throw new Exception("Should be unreachable");
};



abstract class Auto { 
    abstract getProperty();
}

class Bike extends Auto {
    getProperty() {
        return getBaseProperty();
    }
}
class Car extends Auto {
    getProperty() {
        return getBaseProperty() - getLoadFactor();
    }
}
class Bus extends Auto {
    getProperty() {
        return (isNailed) ? 
                0 : 
                getBaseProperty(voltage);
    }
}
// Somewhere in client code
speed = auto.getProperty();

  1. 使用策略模式/命令模式移除条件


函数

  1. 使用默认参数去代替短路操作或条件赋值

    在大多数编程语言中,函数的参数支持设置默认值。这就意味着我们可以在代码中避免使用短路操作和条件赋值。

function setName(name) {
    const newName = name || 'Juan Palomo';
}

function setName(name  = 'Juan Palomo') {
    // ...
}
  1. 函数参数(理想情况下不多于 2 个)

当一个函数有很多参数时,可以把这些参数组合在一起构成一个对象。我们需要避免使用多个基础类型 (如字符串、数字、布尔值等) 作为函数的入参,而是要使用抽象级别更高的对象作为入参。这样我们会更接近业务逻辑,并且更加远离底层实现。

function newBurger(name, price, ingredients, vegan) {
    // ...
}

function newBurger(burger) {
    // ...
} 

function newBurger({ name, price, ingredients, vegan }) {
    // ...
}

const burger = {
    name: 'Chicken',
    price: 1.25,
    ingredients: ['chicken'],
    vegan: false,
};
newBurger(burger);

第一个例子中,我们实现了一个生产汉堡的函数,它有 4 个参数。这些参数是固定的,并且必须按照这个顺序传参,这会给我们带来很多的限制。这样的函数在使用的时候不是很灵活。

第二个例子中,最大的改进就是使用一个对象来作为参数,只需要传入一个 burger 对象就可以生产出一个 “新汉堡”。通过这种方式,我们将汉堡的基本属性整合到 1 个对象里面。

在第三个例子中,我们对传入的对象进行解构赋值,让对象的属性在函数体中可访问到,但是实际上我们传入的仅仅是一个参数,这使得这个函数有了更大的灵活性。

  1. 避免副作用 - 全局变量 副作用是未来麻烦的根源。虽然从定义上来说副作用不一定是有害的,但是如果在项目中没有节制的引起副作用,代码出错的可能性就会大大提高。 不惜一切代价避免副作用,并且确保函数是可以被测试到的。

let fruits = 'Banana Apple';

function splitFruits() {
    fruits = fruits.split(' ');
}

splitFruits();

console.log(fruits); // ['Banana', 'Apple'];

改进后

function splitFruits(fruits) {
    return fruits.split(' ');
}

const fruits = 'Banana Apple';
const newFruits = splitFruits(fruits);

console.log(fruits); // 'Banana Apple';
console.log(newFruits); // ['Banana', 'Apple'];

  1. 避免副作用 - 可变对象 是直接修改对象本身,如果你一直从事计算机相关的工作,你会知道 JavaScript 自诞生以来就是支持对象可变的,目前许多库都在尽量避免使用可变对象。

数组的方法一般被分为两部分: 一部分是会对数组本身进行修改的方法,例如: push、pop、sort,另一部分是不会对数组本身产生修改的方法例如:filter、reduce 、map 等。

const addItemToCart = (cart, item) => {
    cart.push({ item, date: Date.now() });
};

//无副作用
const addItemToCart = (cart, item) => {
    return [...cart, {
                item, 
                date: Date.now(),
            }];
};
  1. 函数应该只做一件事

function emailCustomers(customers) {
    customers.forEach((customer) => {
        const customerRecord = database.find(customer);
        if (customerRecord.isActive()) {
            email(client);
        }
    });
}

function emailActiveCustomers(customers) {
    customers
        .filter(isActiveCustomer)
        .forEach(email);
    }

function isActiveCustomer(customer) {
    const customerRecord = database.find(customer);
    return customerRecord.isActive();
}
  1. 函数应该只是有一个抽象级别 每个函数应仅具有单个抽象级别。

function parseBetterJSAlternative(code) {
    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 tokenize(code) {    
    const statements = code.split(' ');
    const tokens = [];
    REGEXES.forEach((REGEX) => {
        statements.forEach((statement) => {
            tokens.push( /* ... */ );
        });
    });
    return tokens;
}
function lexer(tokens) {
    const ast = [];
    tokens.forEach((token) => ast.push( /* */ ));
    return ast;
}
function parseBetterJSAlternative(code) {
    const tokens = tokenize(code);
    const ast = lexer(tokens);
    ast.forEach((node) => // parse...);
}
  1. 优先考虑函数式编程而不是命令式编程 函数式编程相对于命令式编程相比的另一个特点是代码更具可读性。 函数式编程在这方面就具有很大的优势;但对于那些使用命令式编程学习并开始解决问题的初级程序员来说,他们很难使用这种编程范式,因为它改变了他们的工作习惯。但是在这个行业中,我们必须适应变化,况且目前使用函数式编程的场景越来越多。

const items = [{
    name: 'Coffe',
    price: 500
  }, {
    name: 'Ham',
    price: 1500
  }, {
    name: 'Bread',
    price: 150
  }, {
    name: 'Donuts',
    price: 1000
  }
];

let total = 0;
for (let i = 0; i < items.length; i++) {
  total += items[i].price;
}

// 优化后
const total = items
  .map(({ price }) => price)
  .reduce((total, price) => total + price);
  1. 函数链式调用

class Car {
    constructor({ make, model, color } = car) {
        this.make = make;
        this.model = model;
        this.color = color;
    }
    setMake(make) {
        this.make = make;
    }
    setModel(model) {
        this.model = model;
    }
    setColor(color) {
        this.color = color;
    }
    save() {
        console.log(this.make, this.model, this.color);
    }
}    
const car = new Car({make: 'hoel', model: 'Jetta', color: 'gray'});
car.setColor('red');
car.save();
class Car {
    constructor({ make, model, color } = car){
        this.make = make;
        this.model = model;
        this.color = color;
    }
    setMake(make) {
        this.make = make;
        return this;
    }
    setModel(model) {
        this.model = model;
        return this;
    }
    setColor(color) {
        this.color = color;
        return this;
    }
    save() {
        console.log(this.make, this.model, this.color);
        return this;
    }
}
const car = new Car({make: 'hoel', model: 'Jetta', color: 'gray'});
.setColor('red')
.save();

重构篇

重构很重要的原因:

  1. 改进软件/应用程序的设计

  2. 使软件更容易理解

  3. 发现 bug

  4. 修复现有的旧数据库

  5. 为用户提供更好的一致性

    重构不是银弹,但它是一种有价值的武器,可以帮助你控制好代码和项目 (软件/应用)。它是一个科学的过程,对现有的代码进行改造,使代码可读性更高、更好理解和更整洁。而且,使添加新功能、构建大型应用程序以及发现和修复 bug 变得非常便捷。

撒密码是由明文和密文两个字母表构成的,密文字母表是将明文字母表向左或向右移动一个固定位置得到的。例如,这个恺撒密码,使用的偏移量为 6,相当于右移 6 位

Plain:
    ABCDEFGHIJKLMNOPQRSTUVWXYZ

Cipher:
   GHIJKLMNOPQRSTUVWXYZABCDEF



重构建议: 无论我们如何重构代码,都要使用自动化测试来帮助我们验证有没有 “破坏” 代码。

console.assert(cipher
('Hello World',1) === 'Ifmmp!Xpsme',
`${cipher('Hello World', 1)} === 'Ifmmp!Xpsme'`,
);

console.assert(decipher(cipher
('Hello World',3),3) === 'Hello World',
`${decipher(cipher('Hello World', 3), 3)} === 'Hello World'`,);
  1. 魔法数字 通过定义语义化的变量来移除代码中出现的魔法数字

const NUMBER_LETTERS = 26; 
const LETTER ={
    a: 65,
    z: 90,
    A: 97,
    Z: 122
}
  1. 从 if-else 中提取相似代码 下一步是将代码中重复的代码提取到函数中。具体来说,if 控制结构体中的赋值代码在整个代码中是重复的,我们可以提取这些赋值代码。

  2. 避免使用 else 下一步是避免使用 else 控制结构。避免使用它是很容易的,我们只需要在循环开始之前将代码从 else 移到变量 character,并作为它的默认值。

  3. 合并 IF 逻辑

  4. 简化算法逻辑

  5. 封装条件

  6. 移除 if-else 控制结构

  7. 变量命名

最后更新于

这有帮助吗?