代码整洁之道--去除代码的坏味道

1. 神秘命名(Mysterious Name)
  • 整洁代码最重要的一环就是好的名字,所以我们要深思熟虑如何给函数、模块、变量和类命令,使它们能清晰地表明自己的功能和用法。
  • 命名是编程中最难的两件事之一。正因为如此,改名可能是最常用的重构手法,包括改变函数声明变量改名字段改名等。
  • 改名不仅仅是修改名字而已。如果你想不出一个好名字,说明背后很可能隐藏着更深的设计问题。为一个恼人的名字所付出的纠结,常常能推动我们对代码进行精简。

函数改名

function circum(radius) { ... }

function circumference(radius) { ... }

变量改名

let a = height * width;

let area = height * width;

字段改名

class Organization {
    get name() { ... }
}

class Organization {
    get title() { ... }
}
2. 重复代码(Duplicated Code)
  • 如果你在一个以上的地点看到相同的代码结构,那么可以肯定:设法将它们合而为一,程序会变得更好。一旦有重复代码存在,阅读这些重复代码时就必须加倍仔细,留意其间细微的差异。如果要修改重复代码,必须找出所有的副本来修改。
  • 同一个类的两个函数含有相同的表达式,需要做的就是提炼函数提炼出重复的代码,然后两个地方调用该代码
  • 如果重复函数只是相似而不是完全相同,尝试用移动语句重组代码顺序,把相似的部分放在一起以便提炼
  • 如果重复代码段位于同一个超类的不同子类中,可以使用函数上移来避免在两个子类之间相互调用

提炼函数

function printOwing(invoice) {
    printBanner();
    let outstanding = calculateOutstanding();
    
    //print details
    console.log(`name:${invoice.customer}`);
    console.log(`amount:${outstanding}`);
}

function printOwing(invoice) {
    printBanner();
    let outstanding = calculateOutstanding();
    printDetails(invoice,outstanding);
}
function printDetails(invoice,outstanding) {
    console.log(`name:${invoice.customer}`);
    console.log(`amount:${outstanding}`);   
}

移动语句 (合并重复的代码片段)

const pricingPlan = retrievePricingPlan();
const order = retreiveOrder();
let charge;
const chargePerUnit = pricingPlan.unit;

const pricingPlan = retrievePricingPlan();
const chargePerUnit = pricingPlan.unit;
const order = retreiveOrder();
let charge;

函数上移

class Employee { ... }

class Salesman extends Employee {
    get name() { ... }
}

class Engineer extends Employee {
    get name() { ... }
}

class Employee { 
    get name() { ... }
}

class Salesman extends Employee { ... }

class Engineer extends Employee { ... }
3. 过长函数(Long Function)
  • 据我们的经验,活得最长、最好的程序,其中的函数都比较短。初次接触到这种代码库会感觉程序里满是无穷无尽的委托调用。但和这样的程序共处几年之后,就会明白这些小函数的价值所在。间接性带来的好处——更好的阐释力、更易于分享、更多的选择——都是由小函数来支持的。
  • 我们应该更积极地分解函数。我们遵循这样一条原则:每当感觉需要以注释来说明点什么的时候,我们就需要说明的东西写进一个独立函数中,并以其用途命名。
  • 百分之九十九的场合里,要把函数变短,只需要使用 提炼函数。找到函数中适合集中在一起的部分,将它们提炼出来一个新函数
  • 如果函数内有大量的参数和临时变量,可以经常运用以查询取代临时变量来消除这些临时元素。引入参数对象保持对象完整则可以将过长的参数列表变得更简洁一些。
  • 如果这么做了,仍然有太多临时变量和参数,那就应该使出我们的杀手锏——以命令取代函数
  • 如何确定该提炼哪一段代码呢?一个很好的技巧是:寻找注释。它们通常能指出代码用途和实现手法之间的语义距离。如果代码前方有一行注释,就是在提醒你:可以将这段代码替换成一个函数,而且可以在注释的基础上给这个函数命名。就算只有行一代码,如果它需要以注释来说明,那也值得将它提炼到独立函数中去。
  • 条件表达式,对于庞大的 switch 语句,其中的每个分支都应该变成独立的函数调用。如果有多个 switch 语句基于同一个条件进行分支选择,就应该使用以多态取代条件表达式
  • 循环,应该将循环和循环内的代码提炼到一个独立的函数中。如果你发现提炼出的循环很难命名,可能是因为其中做了几件不同的事。如果是这种情况,请勇敢地使用拆分循环将其拆分成各自独立的任务。

提炼函数

function printOwing(invoice) {
    printBanner();
    let outstanding = calculateOutstanding();
    
    //print details
    console.log(`name:${invoice.customer}`);
    console.log(`amount:${outstanding}`);
}

function printOwing(invoice) {
    printBanner();
    let outstanding = calculateOutstanding();
    printDetails(invoice,outstanding);
}
function printDetails(invoice,outstanding) {
    console.log(`name:${invoice.customer}`);
    console.log(`amount:${outstanding}`);   
}

以查询取代临时变量

const basePrice = this._quantitiy * this._itemPrice;
if (basePrice > 100) {
    return basePrice * 0.5;
}else {
    return basePrice * 0.98;
}

get basePrice(){
    this._quantitiy * this._itemPrice;
}
...

if (this.basePrice() > 100) {
    return this.basePrice() * 0.5;
}else {
    return this.basePrice() * 0.98;
}

引入参数对象:

function amountInvoiced(startDate,endDate) { ... }
function amountReceived(startDate,endDate) { ... }
function amountOverdue(startDate,endDate) { ... }

function amountInvoiced(dateRange) { ... }
function amountReceived(dateRange) { ... }
function amountOverdue(dateRange) { ... }

保持对象完整

const low = aRoom.daysTempRange.low;
const high = aRoom.daysTempRange.high;
if (aPlan.withinRange(low,high))

if (aPlan.withinRange(aRoom.daysTempRange))

以命令取代函

function score(candidate,medicalExam,scoringGuide) {
    let result = 0;
    let healthLevel = 0;
    //long body code;
}

class Scorer {
    constructor(candidate,medicalExam,scoringGuide) {
        this.candidate = candidate;
        this.medicalExam = medicalExam;
        this.scoringGuide = scoringGuide;
    }
    execute (){
        this.result = 0;
        this.healthLevel = 0;
        //long body code;
    }
}

多态取代条件表达式

switch (brid.type) {
    case "EuropeanSwallow" :
        return "average";
    case "AfricanSwallow" :
        return (bird.numberOfCoconuts > 2) ? "tired" : "average" ;
    case "NorwegianBlueParrot":
        return (bird.voltage > 100) ? "scorched" : "beautiful" ;
    default:
}

class EuropeanSwallow {
    get plumage() {
        return "average";
    }
}
class AfricanSwallow {
    get plumage() {
        return (bird.numberOfCoconuts > 2) ? "tired" : "average" ;
    }
}
class NorwegianBlueParrot {
    get plumage(){
        return (bird.voltage > 100) ? "scorched" : "beautiful" ;
    }
}

拆分循环

let averageAge = 0;
let totalSalary = 0;
for (People p : people) {
    averageAge += p.age;
    totalSalary += p.salary;
}
averageAge = averageAge / people.length;

let totalSalary = 0;
for (People p : people) {
    totalSalary = p.salary;
}

let averageAge = 0;
for (People p : people) {
    averageAge += p.age;
}
averageAge /= people.length;
  1. 过长参数列表(Long Parameter List)
  • 如果可以向某个参数发起查询而获得另一个参数的值,那么就可以使用查询替代参数
  • 如果发现自己正从现有的数据结构中抽出很多数据项,就直接传入原来的数据结构保持对象完整
  • 如果有几项参数总是同时出现,可以将其合并成一个对象引入参数对象
  • 如果某个参数被作区分函数行为的标记(boolean flag),移除标记参数
  • 如果多个函数有同样的几个参数,引入一类就尤为有意义。可以使用函数组合成为,将这些共同的参数变成这个类的字段。

移除标记参数

function setDimension(name, value) {
    if (name === "height") {
        this.height = value;
        return ;
    }
    if (name === "width") {
        this.width = value;
        return;
    }
}

function setHeight(value) { this.height = value; }
function setWidth(value) { this.width = value; }

函数组合成为

function base(aReading) { ... }
function taxableCharge(aReading) { ... }
function calculateBaseCharge(aReading) { ... }

class Reading {
    function base() { ... }
    function taxableCharge() { ... }
    function calculateBaseCharge() { ... }
}
  1. 全局数据(Global Data)
  • 全局数据是最刺鼻的坏味道之一。全局数据的问题在于,从代码库的任何一个角落都可以修改它,而且没有任何机制可以探测出到底哪段代码做出了修改。全局数据最显而易见的形式就是全局变量,但类变量和单例也有这样的问题。
  • 首要的防御手段就是封装变量,控制对它的访问权限,然后最后将这个函数(及其封装的数据)搬移到一个类或模块中,只允许模块内的代码使用它,从而尽量控制其作用域。
  • 有少量的全局变量或许无妨,但数量越多,处理的难度就会指数上长。即便只是少量的数据,我们也要将它封装起来,这是在软件演进过程中应对变化的关键所在。

封装变量

let defaultOwner = {firstName: "Martin", lastName: "Fowler"};

let defalutOwnerData = {firstName: "Martin", lastName: "Fowler"};
function defaultOwner() { return defaultOwnerData; }
function setDefaultOwner(arg) { this.defaultOwnerData = art; }
  1. 可变数据(Mutable Data)
  • 可用封装变量来确保所有数据更新操作都通过很少几个函数来进行,使其更容易监控和演进。
  • 如果一个变量在不同时间被用于存储不同的东西,可以使用拆分变量将其拆分为各自不同用途的变量
  • 使用移动语句提炼函数尽量把逻辑从处理更新操作的代码中搬移出来,将没有副作用的代码与执行数据更新操作的代码分开
  • 设计 API 时,将查询和修改函数分离确保调用者不会调用到有副作用的代码
  • 尽早使用移除设置函数,帮我们发现缩小变量作用域的机会
  • 如果可变数据的值能在其人地方计算出来,这就是一个特别刺鼻的坏味道。它不仅会造成困扰、bug、和加班,而且毫无必要。消除这种坏味道的办法很简单,使用以查询取代派生变量即可

拆分变量

let temp = 2 * (height + width);
console.log(temp);
temp = height * width;
console.log(temp);

const perimeter = 2 * (height + width);
console.log(perimerter)
const area = height * width;
console.log(area);

将查询和修改函数分离

function getTotalOutstandingAndSendBill() {
    const result = customer.invoice.reduce((total,each)=>each.amount+total,0);
    sendBill();
    return result;
}

function totalOutstanding() {
    return customer.invoice.reduce((total,each)=>each.amount+total,0);
}
function sendBill() {
    emailGateway.send(formatBill(customer));
}

移除设置函数

class Person {
    get name() { ... }
    set name(aString) { ... }
}

class Person {
    get name() { ... }
}

以查询取代派生变量

get discountedTotal() { return this.discountedTotal; }
set discount(aNumber) {
    const old = this.discount;
    this.dicount = aNumber;
    this.discountedTotal += old - aNumber;
    
}

get discountedTotal() { return this.baseTotal - this.discount; }
set dicount(aNumber) { return this.dicount = aNumber; }
  1. 发散式变化(Divergent Change)
  • 如果某个模块经常因为不同的原因在不同的方向上发生变化,发散式变化就出现了
  • 如果发生变化的两个方向自然地形成了先后次序,可以用拆分阶段将两者分开,两者之间通过一个清晰的数据结构进行沟通
  • 如果两个方向之间有更多的来回调用,就应该先创建适当的模块,然后用搬移函数把处理逻辑分开
  • 如果函数内部混合了两类处理逻辑,应该先用提炼函数将其分开,然后再做搬移
  • 如果模块以类的形式定义的,就可以用提炼类来做拆分

拆分阶段:

const orderData = orderString.split(/\s+/);
const productPrice = priceList[orderData[0].split("-")[1]];
const orderPrice = parseInt(orderData[1]) * productPrice;

const orderRecord = parseOrder(order);
const orderPrice = price(orderRecord,priceList);
function parseOrder(aString) {
    const value = aString.split(/\s+/);
    return ({
        productID: value[0].split("-")[1],
        quantity: parseInt(values[1]);
    });
}
function price(order,priceList) {
    return order.quantity * priceList[order.productID];
}

搬移函数:

class Account {
    get overdraftCharge() { ... }
}

class AccountType {
    get overdraftCharge() { ... }
}

提炼类:

class Person {
    get officeAreaCode() { return this.officeAreaCode; }
    get officeNumber() { return this.officeNumber; }
}

class Person {
    get officeAreaCode() { return this.telephoneNumber.areaCode; }
    get officeNumber() { return this.telephoneNumber.number; }
}
class TelephoneNumber {
    get areaCode() { return this.areaCode; }
    get number() { return this.number; }
}
  1. 霰弹式修改(Shotgun Surgery)
  • 霰弹式修改类似于发散式变化,但又恰恰相反。如果每遇到某种变化,你都必须在许多不同的类的内做出许多小修改,所面临的坏味道就是散弹式修改。如果需要修改的代码散布四处,不但很难找到它们,也很容易错过某个重要的修改
  • 应该使用搬移函数搬移字段把所有需要修改的代码放进同一个模块里
  • 如果有很多函数都在操作相似的数据,可以使用函数组合成类
  • 如果有些函数功能是转化或者充实数据结构,可以使用函数组合成变换
  • 如果一些函数的输出可以组合后提供给一段专门使用这些计算结果的逻辑,这种时候常常用得上拆分阶段
  • 面对霰弹式修改,一个常用的策略就是使用与内联相关的重构——如内联函数或是内联类——把本不该分散的逻辑拽回一处。

搬移字段:

class Customer {
    get plan() { return this.plan; }
    get discountRate() { return this.dicountRate; }
}

class Customer {
    get plan() { return this.plan; }
    get dicountRate() { return this.plan.dicountRate; }
}

函数组合成变换:

function base(aReading) { ... }
function taxableCharge(aReading) { ... }

function enrichReading(argReading) {
    const aReading = this.cloneDeep(argReading);
    aReading.baseCharge = base(aReading);
    aReading.taxableCharge = taxableCharge(aReading);
    return aReading;
}

内联函数:

function getRating(driver) {
    retur moreThanFiveLateDeliveries(deriver) ? 2 : 1;
}
function moreThanFiveLateDeliveries(driver) {
    return diver.numOfLateDeliveries > 5;
}

function getRating(driver){
    return (diver.numOflateDeliveries > 5) ? 2 : 1;
}

内联类 (反射重构:提炼类):

class Person {
    get officeAreaCode() { return this.telephoneNumber.areaCode; }
    get officeNumber() { return this.telephoneNumber.number; }
}
class TelephoneNumber {
    get areaCode() { return this.areaCode; }
    get number() { return this.number; }
}

class Person {
    get officeAreaCode() { return this.officeAreaCode; }
    get officeNumber() { return this.officeNumber; }
}
  1. 依恋情结(Feature Envy)
  • 一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于在自己所处模块内部的交流,这就是依恋情绪的典型情况
  • 一个函数往往用到几个模块的功能,那么它究竟该被置于何处呢?我们的原则是:判断哪个模块拥有的此函数使用的数据最多,然后就把这个函数和那些数据摆在一起。
  1. 数据泥团(Data Clumps)
  • 两个类中相同的字段、许多函数签名中相同的参数。这些总是绑在一起出现的数据应该拥有属于它们自己的对象。
  • 首先找出这些数据以字段形式出现的地方,运用提炼类将它们提炼到一个独立对象中
  • 运用引入参数对象保持对象完整为函数签名瘦身,这么做的直接好处就是可以将很多参数列表缩短,简化函数调用。
  1. 基本类型偏执(Primitive Obsession)
  • 可以运用以对象取代基本类型将原来单独存在的数据的值替换为对象,从而走出传统的洞窟,进入炙手可热的对象世界。
  • 如果想要替换的数据值是控制条件行为的类型码,则可以运用以子类取代类型码加上以多态取代条件表达式的组合将它换掉
  • 如果有一组总是同时出现的基本类型数据,这就是数据泥团的征兆,应该运用提炼类引入参数对象来处理。

以对象取代基本类型

orders.filter(o => "height" === o.priority || "rush" === o.priority);

orders.filter(o => o.priority.higherThan(ne Priority("normal")));

以子类取代类型码

function createEmployee(name, type) {
    return new Employee(name, type);
}

function createEmployee(name, type) {
    switch(type) {
        case "engineer": return new Engineer(name);
        case "salesman": return new Salesman(name);
        case "manager": return new Manager(name);
    }
}
  1. 重复的 switch(Repeated Switches)
  • 如果你跟真正的面向对象布道者交谈,他们很快就会谈到 switch 语句的邪恶。在他们看来,任何 switch 语句都应该用以多态取代条件表达式消除掉。我们甚至还听过这样的观点:所有条件逻辑都应该用多态取代,绝大多数 if 语句都应该被扫进历史的垃圾桶。
  1. 循环语句(Loops)
  • 如今,函数作为一等公民已经得到了广泛的支持,因此我们可以使用以管道取代循环来让这些循环退休。我们发现,管道操作(如 filter 和 map)可以帮助我们更快地看清被处理的元素以及处理它们的动作。

以管道取代循环

const names = [];
for (Object obj : input) {
    if (obj.job === "programmer"){
        names.push(obj.job);
    }
}

const names = input.filter(i => i.job === "programmer")
                   .map(i => i.name);
  1. 冗赘的元素(Lazy Element)
  • 通常只需要使用内联函数或是内联类。如果这个类处于一个继承体系中,可以使用折叠继承体系

折叠继承体系

class Employee { ... }
class Salesman extends Employee { ... }

class Employee { ... }
  1. 夸夸其谈通用性(Speculative Generality)
  • 当有人说“我想我们总有一天需要做这事”,并因而企图以各式各样的钩子和特殊情况来处理一些非必要的事情,这种坏味道就出现了。这么做的结果往往造成系统更难理解和维护。
  1. 临时字段(Temporary Field)
  • 有时你会看到这样的类:其内部的某个字段仅为某种特定情况而设。这样的代码让人不易理解,因为你通常认为对象在所有时间都需要它的所有字段。在字段未被使用的情况下猜测当初设置它的目的,会让人发疯。
  • 使用引入特例在变量不合法的情况下创建一个替代对象,从而避免写出条件式代码。
    引入特例
if (aCustomer === "unknown") customerName = "occupant"; 

class UnknownCustomer {
    get name() { return "occupant" };
}
  1. 过长的消息链(Message Chains)
  • 如果看到用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求别一个对象……这就是消息链。在实际代码中你看到的可能是一长串取值函数或一长串临时变量。采取这种方式,意味客户端代码将与查找过程中的导航结构紧密耦合。一旦对象间的关系发生任何变化,客户端就不得不做出相应修改。
  1. 中间人(Middle Man)
  • 对象基本特征之一就是封装——对外部世界隐藏其内部细节。封装往往伴随着委托。
  • 但是人们可能过度运用委托。如果某个类的接口有一半的函数都委托给其它类,这样就是过度运用。这时应用使用移除中间人,直接和真正的对象打交道。如果这样“不干实事”的函数只有少数几个,可以运用内联函数把它们放进调用端如果这些中间人还有其他行为,可以运用以委托取代超类或者以委托取代子类把它变成真正的对象,这样既可以扩展原对象的行业,又不必负担那么多的委托动作。
    移除中间人
manager = aPerson.mananger;
class Person {
    get manager() { return this.department.manager; }
}

manager = aPerson.department.manager;

以委托取代超类

class List { ... }
class Stack extends List { ... }

class Stack {
    construtor() {
        this._storage = new List();
    }
}
class List { ... }

以委托取代子类

class Order {
    get daysToShip() {
        return this._warehouse.daysToShip;
    }
}
class PriorityOrder extends Order {
    get daysToShip() {
        return this._priorityPlan.daysToShip;
    }
}

class Order {
    get daysToShip() {
        return (this._priorityDelegate) 
            ? this._priorityDelegate.daysToShip
            : this._warehouse.daysToShip;                            
    }
}
class PriorityOrderDelegate {
    get daysToShip() {
        return this._priorityPlan.daysToShip;
    }
}
  1. 内幕交易(Insider Trading)
  • 软件开发者喜欢在模块之间建起高墙,极其反感在模块之间大量交换数据,因为这会增加模块的耦合。在实际情况里,一定的数据交换不可避免,但我们必须尽量减少这种情况,并把这种交换都放到明面上来。
  1. 过大的类(Large Class)
  • 如果想利用单个类做太多事情,其内往往就会出现太多字段。一旦如此,重复代码也就接踵而来。
  1. 异曲同工的类(Alternative Classes with Different Interfaces)
  • 使用类的好处之一就在于可以替换:今天用这个类,未来可以换成用另一个类。但只有当两个类的接口一致时,才能做这种替换。
  1. 纯数据类(Data Class)
  • 所谓纯数据类是指:它们拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长物。这样的类只是一种不会说话的数据容器,它们几乎一定被其他类过分细琐的操控着。
  • 这些类早期可能拥有 public 字段,若果真如此,应该在别人注意到它们之前,立刻运用封装记录将它们封装起来。对于那些不该被其他类修改的字段,请运用移除设置函数
  1. 被拒绝的遗赠(Refused Bequest)
  • 子类应该继承超类的函数和数据。如果它们不想或不需要继承,又该怎么办?
  • 按传统说法,这就意味着继承体系设计错误。你需要为这个子类新建一个兄弟类,把所有用不到的函数下推给那个兄弟。你常常会听到这样的建议:所有超类都应该是抽象的。
  • 如果子类复用了超类的行为(实现),却又不愿意支持超类的接口。“被拒绝的遗赠”的坏味道就会变得很浓烈。拒绝继承超类的实现,这一点我们不介意;但如果拒绝支持超类的接口,这就难以接受了。既然不愿意支持超类的接口,就不要糊弄继承体系,应用运用以委托取代子类或者以委托取代超类彻底划清界限。
  1. 注释(Comments)
  • 并不说不该写注释。注释不但不是一种坏味道,事实上它们还是一种香味。之所以提到注释,是因为人们常把它当作“除臭剂”来使用。常常会有这样的情况:你看到一段代码有着长长的注释,然后发现,这些注释之所以存在是因为代码很糟糕。
  • 当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都变得多余。
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值