【读书笔记】《重构_改善既有代码的设计》24种代码的坏味道

24中代码的坏味道

标题:【读书笔记】《重构_改善既有代码的设计》24种代码的坏味道

时间:2024.01.11

作者:耿鬼不会笑

24种代码的坏味道

1.神秘命名(Mysterious Name)

描述:改名不仅仅是修改名字而已。如果你想不出一个好名字,说明背后很可能潜藏着更深的设计问题。

优化:

(1)改变函数声明

(2)变量改名

(3)字段改名

样例:

public static double countOrder(Order order) {
    double basePrice = order.getQuantity() * order.getItemPrice();
    double quantityDiscount = Math.max(0, order.getQuantity() - 500) * order.getItemPrice() * 0.05;
    double shipping = Math.min(basePrice * 0.1, 100);
    return basePrice - quantityDiscount + shipping;
}

说明: 函数名 countOrder 的第一感觉不太清晰,无法确认函数的作用?统计订单?订单商品数量吗?还是统计什么?但是通过函数的实现可以确认,这是个统计订单总价格的函数。

修改:

public static double getPrice(Order order) {
	...
}

2.重复代码(Repeat Code)

描述:一旦有重复代码存在,阅读这些重复的代码时你就必须加倍仔细,留意其间细微的差异。如果要修改重复代码,你必须找出所有的副本来修改。最单纯的重复代码就是“同一个类的两个函数含有相同的表达式”

优化:

(1)提炼函数

(2)如果重复代码只是相似而不是完全相同,请首先尝试用移动语句重组代码顺序,把相似的部分放在一起以便提炼

(3)如果重复的代码段位于同一个超类的不同子类中,可以使用函数上移来避免在两个子类之间互相调用

样例:

public String renderPerson(Person person) {
    List<String> result = new ArrayList<>();
    result.add("<p>" + person.getName() + "</p>");
    result.add("<p>title: " + person.getPhoto().getTitle() + "</p>");
    result.add(emitPhotoData(person.getPhoto()));
    return String.join("\n", result);
}
public String photoDiv(Photo photo) {
    List<String> result = new ArrayList<>();
    result.add("<div>");
    result.add("<p>title: " + photo.getTitle() + "</p>");
    result.add(emitPhotoData(photo));
    result.add("</div>");
    return String.join("\n", result);
}

public String emitPhotoData(Photo aPhoto) {
    List<String> result = new ArrayList<>();
    result.add("<p>location: " + aPhoto.getLocation() + "</p>");
    result.add("<p>date: " + aPhoto.getDate() + "</p>");
    return String.join("\n", result);
}

renderPerson方法 和 photoDiv 中有一个同样的实现,那就是渲染 photo.title 的部分。这一部分的逻辑总是在执行 emitPhotoData 函数的前面,这是一段重复代码。

修改:

public String renderPerson(Person person) {
    List<String> result = new ArrayList<>();
    result.add("<p>" + person.getName() + "</p>");
    result.add(emitPhotoData(person.getPhoto()));
    return String.join("\n", result);
}

public String photoDiv(Photo photo) {
    List<String> result = new ArrayList<>();
    result.add("<div>");
    result.add(emitPhotoData(photo));
    result.add("</div>");
    return String.join("\n", result);
}

public String emitPhotoData(Photo aPhoto) {
    List<String> result = new ArrayList<>();
    result.add("<p>title: " + aPhoto.getTitle() + "</p>");
    result.add("<p>location: " + aPhoto.getLocation() + "</p>");
    result.add("<p>date: " + aPhoto.getDate() + "</p>");
    return String.join("\n", result);
}

3.过长函数(Long Function)

描述:函数越长,就越难理解。如果你需要花时间浏览一段代码才能弄清它到底在干什么,那么就应该将其提炼到一个函数中,并根据它所做的事为其命名。让小函数易于理解的关键还是在于良好的命名。如果你能给函数起个好名字,阅读代码的人就可以通过名字了解函数的作用,根本不必去看其中写了些什么,这可以节约大量的时间。条件表达式和循环常常也是提炼的信号。

优化:

(1)提炼函数

(2)如果提炼函数时会把许多参数传递给被提炼出来的新函数,可以经常运用以查询取代临时变量来消除这些临时元素

(3)引入参数对象保持对象完整则可以简化过长的参数列表

(4)仍然有太多临时变量和参数时,应该考虑使用以命令取代函数

(5)使用分解条件表达式处理条件表达式

(6)对于庞大的switch语句,其中的每个分支都应该通过 提炼函数变成独立的函数调用

(7)如果有多个switch语句基于同一个条件 进行分支选择,就应该使用以多态取代条件表达式

(8)对于循环,应该将循环和循环内的代码提炼到一个独立的函数中

(9)如果提炼出的循环很难命名,可能是因为其中做了几件不同的事,此时应使用拆分循环将其拆分成各自独立的任务

样例:

    public static void printOwing(Invoice invoice) {
        double outstanding = 0;
        System.out.println("***********************");
        System.out.println("**** Customer Owes ****");
        System.out.println("***********************");

        // Calculate outstanding
        List<Order> orders = invoice.getOrders();
        for (Order o : orders) {
            outstanding += o.getAmount();
        }

        // Record due date
        Date today = new Date();
        Date dueDate = new Date(today.getYear(), today.getMonth(), today.getDate() + 30);
        invoice.setDueDate(dueDate);

        // Print details
        System.out.println("name: " + invoice.getCustomer());
        System.out.println("amount: " + outstanding);
        System.out.println("due: " + invoice.getDueDate());
    }

函数体过长,进行函数的拆分,重构后的 printOwing 函数,简单的四行代码,清晰的描述了函数所做的事情

修改:

	public static void printBanner() {
        System.out.println("***********************");
        System.out.println("**** Customer Owes ****");
        System.out.println("***********************");
    }

    public static double calculateOutstanding(Invoice invoice) {
        double outstanding = 0;
        List<Order> orders = invoice.getOrders();
        for (Order o : orders) {
            outstanding += o.getAmount();
        }
        return outstanding;
    }

    public static void recordDueDate(Invoice invoice) {
        Date today = new Date();
        Date dueDate = new Date(today.getYear(), today.getMonth(), today.getDate() + 30);
        invoice.setDueDate(dueDate);
    }

    public static void printDetails(Invoice invoice, double outstanding) {
        System.out.println("name: " + invoice.getCustomer());
        System.out.println("amount: " + outstanding);
        System.out.println("due: " + invoice.getDueDate());
    }

    public static void printOwing(Invoice invoice) {
        printBanner();
        double outstanding = calculateOutstanding(invoice);
        recordDueDate(invoice);
        printDetails(invoice, outstanding);
    }

4.过长参数列表(Long Parameter List)

描述:过长的参数列表本身令人迷惑

优化:

(1)如果可以向某个参数发起查询而获得另一个参数的值,那么就可以使用以查询取代参数去掉这第二个参数

(2)如果正在从现有的数据结构中抽出很多数据项,就可以考虑使用保持对象完整手法,直接传入原来的数据结构

(3)如果有几项参数总是同时出现,可以用引入参数对象将其 合并成一个对象

(4)如果某个参数被用作区分函数行为的标记(flag),可以使用 移除标记参数

(5)如果多个函数有同样的几个参数,可以使用函数组合成类,将这些共同的参数变成这个类的字段

样例:

public class PriceRangeFilter {

    public static List<Product> filterPriceRange(List<Product> products, double min, double max, boolean isOutside) {
        if (isOutside) {
            return products.stream()
                .filter(product -> product.getPrice() < min || product.getPrice() > max)
                .collect(Collectors.toList());
        } else {
            return products.stream()
                .filter(product -> product.getPrice() > min && product.getPrice() < max)
                .collect(Collectors.toList());
        }
    }

    @Data
    static class Product {
        private double price;
    }
    
    public static List<Product> outsidePriceProducts(){
        filterPriceRange(
                List.of(new Product(/* ... */), new Product(/* ... */)),
                1.0,
                10.0,
                true
        )
    }
    
    public static List<Product> insidePriceProducts(){ 
        filterPriceRange(
                List.of(new Product(/* ... */), new Product(/* ... */)),
                5.0,
                8.0,
                false
        )
    }
}

filterPriceRange 是过滤商品的函数,仔细看的话会发现,主要比对的是 product.price 字段和传入的参数 minmax 之间的大小对比关系。如果 isOutSidetrue 的话,则过滤出价格区间之外的商品,否则过滤出价格区间之内的商品。isOutSide 作为标记参数,因为它们让人难以理解到底有哪些函数可以调用、应该怎么调用。使用这样的函数,我还得弄清标记参数有哪些可用的值。尽管priceOutSideRangepriceInsideRange 的函数命名已经足够清晰,但是内部对 range 范围的判定还是需要花费一定时间理解,而 range 作为我们刚识别出来的一种结构,可以进行重构。

修改:

public class PriceRangeFilter {

    public static List<Product> filterPriceOutsideRange(List<Product> products, Range range) {
        return products.stream()
                .filter(product -> range.outside(product.getPrice()))
                .collect(Collectors.toList());
    }
    
    public static List<Product> filterPriceInsideRange(List<Product> products, Range range) {
        return products.stream()
                .filter(product -> range.inside(product.getPrice()))
                .collect(Collectors.toList());
    }

    @Data
    public static class Range {
        private double min;
        private double max;
        public boolean outside(double num) {
            return num < min || num > max;
        }
        public boolean inside(double num) {
            return num > min && num < max;
        }
    }
	
    @Data
    public static class Product {
        private double price;
    }
    
    public static List<Product> outsidePriceProducts(){
        filterPriceRange(
                List.of(new Product(/* ... */), new Product(/* ... */)),
                new Range(1, 10)
        )
    }
    
    public static List<Product> insidePriceProducts(){ 
        filterPriceRange(
                List.of(new Product(/* ... */), new Product(/* ... */)),
                new Range(5, 8)
        )
    }
}

5.全局数据(Global Data)

描述:全局数据的问题在于,从代码库的任何一个角落都可以修改它,而且没有任何机 制可以探测出到底哪段代码做出了修改。全局数据最显而易见的形式就是全局变量,但类变量和单例(singleton)也有这样的问题。

优化:

(1)封装变量,把全局数据用一个函数包装起来,对于修改它的地方,控制对它的访问。

(2)最好将这个函数(及其封装的数据)搬移到一个类或模块中,只允许模块内的代码使用它,从而尽量控制其作用域。

样例:

public class Global {
    public static String platform = "pc";
    public static String token = "";
}

这个 Global.java 用来提供全局数据的,不能修改为别的值,不然后序的判断逻辑中就会报错,而且没有任何机制可以探测出到底哪段代码做出了修改,将其用一个函数包装起来,至少看见修改它的地方,并开始控制对它的访问。

修改:

public class Global {
    private static String platform = "pc";
    private static String token = "";
    
    public static String getPlatform() {
        return platform;
    }
    public static String getToken() {
        return token;
    }
    public static void setPlatform(String newPlatform) {
        platform = newPlatform;
    }
    public static void setToken(String newToken) {
        token = newToken;
    }
}

6.可变数据(Mutable Data)

描述:在一处更新数据,却没有意识到软件中的另一处期望着完全不同的数据,于是一个功能失效了,且要找出故障原因就会更加困难

优化:

(1)用封装变量来确保所有数据更新操作都通过很少几个函数来进行,使其更容易监控和演进

(2)如果一个变量在不同时候被用于存储不同的东西, 可以使用拆分变量将其拆分为各自不同用途的变量,从而避免危险的更新操作

(3)使用移动语句提炼函数尽量把逻辑从处理更新操作的代码中搬移出来,将没有副作用的代码与执行数据更新操作的代码分开

(4)设计API时,可以使用将查询函数和修改函数分离确保调用者不会调到有副作用的代码,除非他们真的需要更新数据

(5)尽早使用移除设值函数——把设值函数的使用者找出来,缩小变量作用域

(6)如果可变数据的值能在其他地方计算出来,则使用以查询取代派生变量消除这种坏味道

(7)如果变量作用域只有几行代码,即使其中的数据可变,也不是什么大问题; 但随着变量作用域的扩展,风险也随之增大。用函数组合成类或者 函数组合成变换来限制需要对变量进行修改的代码量

(8)如果一个变量在其内部结构中包含了数据,通常最好不要直接修改其中的数据,而是用将引用对象改为值对象令其直接替换整个数据结构

(9)如果要更新一个数据结构,可以就返回一份新的数据副本,旧的数据仍保持不变

样例:

    public static Map<K, V> merge(Map<K, V> target, Map<K, V> source) {
        for (Map.Entry<K, V> entry : source.entrySet()) {
            target.put(entry.getKey(), entry.getValue());
        }
        return target;
    }

对源对象进行了修改调整,从而影响了源对象的值,当使用到源对象时,可能会因为取到错误的数据

修改:

    public static Map<K, V> merge(Map<K, V> target, Map<K, V> source) {
        Map<K, V> mergedMap = new HashMap<>(target);
        mergedMap.putAll(source);
        return mergedMap;
    }

7.发散式变化(Divergent Change)

说明:如果某个模块经常因为不同的原因在不同的方向上发生变化,发散式变化就出现了。例如:如果新加入一个数据库,我必须修改这3个函数;如果新出现一种金融工具,我必须修改这4个函数。这就是发散式变化的征兆。数据库交互和金融逻辑处理是两个不同的上下文,将它们分别搬移到各自独立的模块中。每当要对某个上下文做修改时,我们只需要理 解这个上下文,而不必操心另一个。

优化:

(1)如果发生变化的两个方向自然地形成了先后次序(比如说,先从数据库取出数据,再对其进行金融逻辑处理),就可以用拆分阶段将两者分开,两者之间通过一个清晰的数据结构进行沟通。

(2)如果两个方向之间有更多的来回调用,就应该先创建适当的模块,然后用搬移函数把处理逻辑分开。

(3)如果函数内部混合了两类处理逻辑,应该先用提炼函数将其分开,然后再做搬移。

(4)如果模块是以类的形式定义的,就可以用提炼类来做拆分。

样例:

    public static double getPrice(Order order) {
        double basePrice = order.getQuantity() * order.getItemPrice();
        double quantityDiscount = Math.max(0, order.getQuantity() - 500) * order.getItemPrice() * 0.05;
        double shipping = Math.min(basePrice * 0.1, 100);

        return basePrice - quantityDiscount + shipping;
    }

这个函数的职责就是计算基础价格 - 数量折扣 + 运费。如果基础价格计算规则改变,需要修改这个函数,如果折扣规则发生改变也需要修改这个函数,同理,运费计算规则也会引发它的改变。

优化:

    public static double calculateBasePrice(Order order) {
        return order.getQuantity() * order.getItemPrice();
    }
    public static double calculateDiscount(Order order) {
        return Math.max(0, order.getQuantity() - 500) * order.getItemPrice() * 0.05;
    }
    public static double calculateShipping(double basePrice) {
        return Math.min(basePrice * 0.1, 100);
    }

    public static double getPrice(Order order) {
        double basePrice = calculateBasePrice(order);
        double discount = calculateDiscount(order);
        double shipping = calculateShipping(basePrice);

        return basePrice - discount + shipping;
    }

8.霰弹式修改(Shotgun Surgery)

说明:霰弹式修改类似于发散式变化,但又恰恰相反。如果每遇到某种变化,都必须在许多不同的类内做出许多小修改,这就是霰弹式修改。如果需要修改的代码散布四处,你不但很难找到它们,也很容易错过某个重要的修改。

优化:

(1)使用搬移函数搬移字段把所有需要修改的代码放进同一个模块里。

(2)如果有很多函数都在操作相似的数据,可以使用函数组合成类

(3)如果有些函数的功能是转化或者充实数据结构,可以使用函数组合成变换

(4)如果一些函数的输出可以组合后提供给一段专门使用 这些计算结果的逻辑,这种时候常常用得上拆分阶段

(5)使用与内联(inline)相关的重构,如内联函数或是内联类,把本不该分散的逻辑拽回一处。

// File Reading
@Data
public static class Reading {
    private String customer;
    private int quantity;
    private int month;
    private int year;
}

public static Reading acquireReading() {
    return new Reading("ivan",10,5,2017);
}

// File 1
Reading aReading1 = acquireReading();
double baseCharge1 = baseRate(aReading1.getMonth(), aReading1.getYear()) * aReading1.getQuantity();

// File 2
Reading aReading2 = acquireReading();
double base2 = baseRate(aReading2.getMonth(), aReading2.getYear()) * aReading2.getQuantity();
double taxableCharge = Math.max(0, base2 - taxThreshold(aReading2.getYear()));
public static double taxThreshold(int year) {
    return ...;
}

// File 3
Reading aReading3 = acquireReading();
double basicChargeAmount = calculateBaseCharge(aReading3);
public static double calculateBaseCharge(Reading aReading) {
    return baseRate(aReading.getMonth(), aReading.getYear()) * aReading.getQuantity();
}

如果 reading 的部分逻辑发生了改变,对这部分逻辑的修改需要跨越好几个文件调整。

@Data
public class Reading {
    private String customer;
    private int quantity;
    private int month;
    private int year;

    public double getBaseRate(int month,int year) {
        return ...;
    }
    public double getBaseCharge() {
        return getBaseRate(this.month, this.year) * getQuantity();
    }
    public double getTaxableCharge() {
        return Math.max(0, getBaseCharge() - getTaxThreshold());
    }
    public double getTaxThreshold() {
        return ...; 
    }

    public static void main(String[] args) {
        Reading reading = new Reading();
        reading.setCustomer("ivan");
        reading.setQuantity(10);
        reading.setMonth(5);
        reading.setYear(2017);

        double baseCharge = reading.getBaseCharge();
        double taxableCharge = reading.getTaxableCharge();
    }
}

9.依恋情节(Feature Envy)

说明:一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于在自己所处模块内部的交流

优化:

(1)使用搬移函数把相关代码移过去

(2)如果函数中只有一部分受这种依恋之苦,这时候应该使用提炼函数把这一部分提炼到独立的函数中,再使用搬移函数

(3)如果一个函数往往会用到几个模块的功能,首先要判断哪个模块拥有的此函数使用的数据最多,然后就把这个函数和那些数据摆在一起。若先以提炼函数将这个函数分解为数个较小的函数并分别置放于不同地点,上述步骤也就比较容易完成了。

@Data
public class Account {
    private String name;
    private AccountType type;

    public int getLoanAmount() {
        if (getType().getType().equals("vip")) {
            return 20000;
        } else {
            return 10000;
        }
    }

    public static void main(String[] args) {
        AccountType vipType = new AccountType("vip");
        Account account = new Account();
        account.setName("John Doe");
        account.setType(vipType);

        int loanAmount = account.getLoanAmount();
    }
}

@Data
public class AccountType {
    private String type;
}

这段代码是账户 Account 和账户类型 AccountType,如果账户的类型是 vip,贷款额度 loanAmount 就有 20000,否则就只有 10000。在获取贷款额度时,Account 内部的 loanAmount 方法和另一个类 AccountType 的内部数据交流格外频繁,远胜于在自己所处模块内部的交流,这就是依恋情结的典型情况。

@Data
public class Account {
    private String name;
    private AccountType type;
    public int getLoanAmount() {
        return getType().getLoanAmount();
    }

    public static void main(String[] args) {
        AccountType vipType = new AccountType("vip");
        Account account = new Account();
        account.setName("John Doe");
        account.setType(vipType);

        int loanAmount = account.getLoanAmount();
    }
}

@Data
public class AccountType {
    private String type;
    public int getLoanAmount() {
        if (getType().equals("vip")) {
            return 20000;
        } else {
            return 10000;
        }
    }
}

10.数据泥团(Data Clumps)

说明:

两个类中相同的字段、许多函数签名中相同的参数。这些总是绑在一起出现的数据真应该拥有属于它们自己的对象。

优化:

(1)运用提炼类将它们提炼到一个独立对象中。

(2)对于函数签名,运用引入参数对象保持对象完整为它瘦身。

@Data
public class Person {
    private String name;
    private String officeAreaCode;
    private String officeNumber;

    public String getTelephoneNumber() {
        return "(" + getOfficeAreaCode() + ") " + getOfficeNumber();
    }

    public static void main(String[] args) {
        Person person = new Person();
        person.setName("jack");
        person.setOfficeAreaCode("+86");
        person.setOfficeNumber("18726182811");

        System.out.println("person's name is " + person.getName() + ", telephoneNumber is " + person.getTelephoneNumber());
        // person's name is jack, telephoneNumber is (+86) 18726182811
    }
}

这个 Person 类记录了用户的名字(name),电话区号(officeAreaCode)和电话号码(officeNumber),如果我把 officeNumber 字段删除,那 officeAreaCode 就失去了意义。这说明这两个字段总是一起出现的,除了 Person 类,其他用到电话号码的地方也是会出现这两个字段的组合

@Data
public class Person {
    private String name;
    private TelephoneNumber telephoneNumber;
    
    public String getTelephoneNumber() {
        return telephoneNumber.toString();
    }
    public String getOfficeAreaCode() {
        return telephoneNumber.getAreaCode();
    }
    public void setOfficeAreaCode(String arg) {
        this.telephoneNumber = new TelephoneNumber(arg, getOfficeNumber());
    }
    public String getOfficeNumber() {
        return telephoneNumber.getNumber();
    }
    public void setOfficeNumber(String arg) {
        this.telephoneNumber = new TelephoneNumber(getOfficeAreaCode(), arg);
    }

    public static void main(String[] args) {
        //
        Person person = new Person("John");
        person.setOfficeAreaCode("+86");
        person.setOfficeNumber("18726182811");

        System.out.println("Person's name is " + person.getName() + ", telephoneNumber is " + person.getTelephoneNumber());
        // Person's name is John, telephoneNumber is (+86) 18726182811
    }
}

@Data
public class TelephoneNumber {
    private String areaCode;
    private String number;
    @Override
    public String toString() {
        return "(" + getAreaCode() + ") " + getNumber();
    }
}

11.基本类型偏执(Primitive Obsession)

说明:很多程序员不愿意 创建对自己的问题域有用的基本类型,如钱、坐标、范围等。于是,我们看到了把钱当作普通数字来计算的情况、计算物理量时无视单位(如把英寸与毫米相 加)的情况以及大量类似if (a < upper && a > lower)这样的代码。

优化:

(1)以对象取代基本类型将原本单独存在的数据值替换为对象。

(2)如果想要替换的数据值是 控制条件行为的类型码,则可以运用以子类取代类型码加上以多态取代 条件表达式的组合将它换掉。

(3)如果有一组总是同时出现的基本类型数据,这就是数据泥团的征兆,应该运用提炼类引入参数对象来处理。

@Data
public class Product {
    private String name;
    private String price;

    public String getPrice() {
        return getPriceCount() + " " + getPriceSuffix();
    }
    public double getPriceCount() {
        return Double.parseDouble(this.price.substring(1));
    }

    public String getPriceUnit() {
        switch (this.price.charAt(0)) {
            case '¥':
                return "cny";
            case '$':
                return "usd";
            case 'k':
                return "hkd";
            default:
                throw new UnsupportedOperationException("Unsupported unit");
        }
    }

    public double getPriceCnyCount() {
        switch (this.getPriceUnit()) {
            case "cny":
                return this.getPriceCount();
            case "usd":
                return this.getPriceCount() * 7;
            case "hkd":
                return this.getPriceCount() * 0.8;
            default:
                throw new UnsupportedOperationException("Unsupported unit");
        }
    }

    public String getPriceSuffix() {
        switch (this.getPriceUnit()) {
            case "cny":
                return "元";
            case "usd":
                return "美元";
            case "hkd":
                return "港币";
            default:
                throw new UnsupportedOperationException("Unsupported unit");
        }
    }
}

这个 Product(产品)类,price 字段作为一个基本类型,在 Product 类中被各种转换计算,然后输出不同的格式,Product 类需要关心 price 的每一个细节。在这里,应当为price 创建一个属于它自己的基本类型 Price。同时,重构 Product 类,将原有跟 price 相关的逻辑,使用中间人委托来调用。

@Data
public class Product {
    private String name;
    private Price price;
}

@Data
public class Price {
    private double count;
    private String unit;
    private double cnyCount;
    private String suffix;
}

12.重复的 switch(Repeated switch)

说明:很多语言支持更复杂的switch语句,而不只是根据基本类型值来做条件判 断。因此,我们现在更关注重复的switch:在不同的地方反复使用同样的switch 逻辑(可能是以switch/case语句的形式,也可能是以连续的if/else语句的形 式)。重复的switch的问题在于:每当你想增加一个选择分支时,必须找到所有 的switch,并逐一更新。

优化:

(1)以多态取代条件表达式

样例:

@Data
public class Product {
    private String name;
    private String price;

    public String getPrice() {
        return getPriceCount() + " " + getPriceSuffix();
    }
    public double getPriceCount() {
        return Double.parseDouble(this.price.substring(1));
    }

    public String getPriceUnit() {
        switch (this.price.charAt(0)) {
            case '¥':
                return "cny";
            case '$':
                return "usd";
            case 'k':
                return "hkd";
            default:
                throw new UnsupportedOperationException("Unsupported unit");
        }
    }

    public double getPriceCnyCount() {
        switch (this.getPriceUnit()) {
            case "cny":
                return this.getPriceCount();
            case "usd":
                return this.getPriceCount() * 7;
            case "hkd":
                return this.getPriceCount() * 0.8;
            default:
                throw new UnsupportedOperationException("Unsupported unit");
        }
    }

    public String getPriceSuffix() {
        switch (this.getPriceUnit()) {
            case "cny":
                return "元";
            case "usd":
                return "美元";
            case "hkd":
                return "港币";
            default:
                throw new UnsupportedOperationException("Unsupported unit");
        }
    }
}

创建一个工厂函数,同时将 Product 类的实例方法也使用工厂函数创建

优化:

// Price.java
@Data
public abstract class Price {
    protected String value;
    public double getCount() {
        return Double.parseDouble(this.value.substring(1));
    }
}

// CnyPrice.java
@Data
public class CnyPrice extends Price {
    public CnyPrice(String value) {
        super(value);
    }
    @Override
    public String getUnit() {
        return "cny";
    }
    @Override
    public double getCnyCount() {
        return getCount();
    }
    @Override
    public String getSuffix() {
        return "元";
    }
}

// UsdPrice.java
@Data
public class UsdPrice extends Price {
    public UsdPrice(String value) {
        super(value);
    }
    @Override
    public String getUnit() {
        return "usd";
    }
    @Override
    public double getCnyCount() {
        return getCount() * 7;
    }
    @Override
    public String getSuffix() {
        return "美元";
    }
}

// HkdPrice.java
@Data
public class HkdPrice extends Price {
    public HkdPrice(String value) {
        super(value);
    }
    @Override
    public String getUnit() {
        return "hkd";
    }
    @Override
    public double getCnyCount() {
        return getCount() * 0.8;
    }
    @Override
    public String getSuffix() {
        return "港币";
    }
}

// PriceFactory.java
public class PriceFactory {
    
    public static Price createPrice(String value) {
        switch (value.charAt(0)) {
            case '¥':
                return new CnyPrice(value);
            case '$':
                return new UsdPrice(value);
            case 'k':
                return new HkdPrice(value);
            default:
                throw new UnsupportedOperationException("Unsupported unit");
        }
    }

    public static void main(String[] args) {
        // Example usage
        Price cnyPrice = PriceFactory.createPrice("¥50.0");
        System.out.println("CNY Price: " + cnyPrice.toString());
        System.out.println("CNY Count: " + cnyPrice.getCnyCount());
    }
}

13.循环语句(Loop)

说明:如今循环已经有点儿过时,如今,如今越来越多的编程语言都提供了更好的语言结构来处理迭代过程,管道操作(如filter和map)可以帮助我们更快地看清被处理的元素以及处理它们的动作。

优化:

(1)以管道取代循环

样例:

    public static List<CityAreaCodeData> acquireCityAreaCodeData(String input, String country) {
        String[] lines = input.split("\n");
        boolean firstLine = true;
        List<CityAreaCodeData> result = new ArrayList<>();
        for (String line : lines) {
            if (firstLine) {
                firstLine = false;
                continue;
            }
            if (line.trim().isEmpty()) {
                continue;
            }
            String[] record = line.split(",");
            if (record[1].trim().equals(country)) {
                result.add(new CityAreaCodeData(record[0].trim(), record[2].trim()));
            }
        }
        return result;
    }

	@Data
    public static class CityAreaCodeData {
        private String city;
        private String phone;
    }

Java提供了更好的语言结构来处理迭代过程,可以使用stream流来优化代码

修改:

    public static List<CityData> acquireCityData(String input, String country) {
        String[] lines = input.split("\n");
        return Arrays.stream(lines)
                .skip(1)
                .filter(line -> !line.trim().isEmpty())
                .map(line -> line.split(","))
                .filter(record -> record[1].trim().equals(country))
                .map(record -> new CityData(record[0].trim(), record[2].trim()))
                .collect(Collectors.toList());
    }

    @Data
    public static class CityData {
        private String city;
        private String phone;
    }

14.冗赘的元素(Lazy Element)

说明:程序元素(如类和函数)能给代码增加结构,从而支持变化、促进复用,但有时我们真的不需要这层额外的结构。可能有这样一个函数,它的名字就跟实现代码看起来一模一样;也可能有这样一个类, 根本就是一个简单的函数。这可能是因为,起初在编写这个函数时,程序员也许 期望它将来有一天会变大、变复杂,但那一天从未到来;也可能是因为,这个类 原本是有用的,但随着重构的进行越变越小,最后只剩了一个函数。

优化:

(1)内联函数

(2)内联类

(3)如果这个类处于一个继承体系中,可以使用折叠继承体系

样例:

    public List<String[]> reportLines(Customer aCustomer) {
        List<String[]> lines = new ArrayList<>();
        gatherCustomerData(lines, aCustomer);
        return lines;
    }

    private void gatherCustomerData(List<String[]> out, Customer aCustomer) {
        out.add(new String[]{"name", aCustomer.getName()});
        out.add(new String[]{"location", aCustomer.getLocation()});
    }

gatherCustomerData函数显得有点多余,函数逻辑可以直接与reportLines函数合并

优化:

    public List<String[]> reportLines(Customer aCustomer) {
        List<String[]> lines = new ArrayList<>();
        lines.add(new String[]{"name", aCustomer.getName()});
        lines.add(new String[]{"location", aCustomer.getLocation()});
        return lines;
    }
    public List<String[]> reportLines(Customer aCustomer) {
        return Arrays.asList(
            new String[]{"name", aCustomer.getName()},
            new String[]{"location", aCustomer.getLocation()}
        );
    }

15.夸夸其谈通用性(Speculative Generality)

说明:当有人说“噢,我想我们总有一天需要做这事”,并因而企图以各式各样的钩子和特殊情况来处理一些 非必要的事情,这种坏味道就出现了。这么做的结果往往造成系统更难理解和维护。如果所有装置都会被用到,就值得那么做;如果用不到,就不值得。用不上的装置只会挡你的路。

优化:

(1)如果你的某个抽象类其实没有太大作用,请运用折叠继承体系

(2)不必要的委托可运用内联函数内联类除掉。

(3)如果函数的某些参数未被用上,可以用改变函数声明去掉这些参数。如果有并非真正需要、 只是为不知远在何处的将来而塞进去的参数,也应该用改变函数声明去掉。

(4)如果函数或类的唯一用户是测试用例,可以先删掉测试用例,然后移除死代码

@Data
public class TrackingInformation {
    private String shippingCompany;
    private String trackingNumber;
}

@Data
public class Shipment {
    private TrackingInformation trackingInformation;
}

说明:这个关于这两个物流的类,而 TrackingInformation 记录物流公司和物流单号,而 Shipment 只是使用 TrackingInformation 管理物流信息,并没有其他任何额外的工作。为什么用一个额外的 TrackingInformation 来管理物流信息,而不是直接用 Shipment 来管理呢?因为 Shipment 可能还会有其他的职责。但N年已经过去了,它还没有出现其他的职责。

优化:

@Data
public class Shipment {
    private String shippingCompany;
    private String trackingNumber;
}

16.临时字段(Temporary Field)

说明:当某个类其内部某个字段仅为某种特定情况而设。这样的代码难以理解,因为你通常认为对象在所有时候都需要它的所有字段。在字段未被使用的情况下也很难猜测当初设置它的目的。

优化:

(1)使用提炼类为将其收拢到一个地方,然后用搬移函数把所有和这些字段相关的代码放到一起统一管理。

(2)也许你还可以使用引入特例在“变量不合法”的情况下创建一个替代对象,从而避免写出条件式代码。

样例:

@Data
public class Site {
    private Customer customer;
}

@Data
public class Customer {
    private String name;
    private BillingPlan billingPlan;
    private PaymentHistory paymentHistory;
}

@Data
public class BillingPlan {
	......
}

@Data
public class PaymentHistory {
    private int weeksDelinquentInLastYear;
}

public class Main {
    public static void main(String[] args) {
        
        //initial
        Site site = xxx ;

        // Client 1
        Customer aCustomer = site.getCustomer();
        String customerName = (aCustomer == null) ? "occupant" : aCustomer.getName();
        System.out.println("Client 1: " + customerName);

        // Client 2
        BillingPlan plan = (aCustomer == null) ? Registry.getBillingPlans().get("basic") : aCustomer.getBillingPlan();
        System.out.println("Client 2: " + plan);

        // Client 3
        if (aCustomer != null) {
            BillingPlan newPlan = new BillingPlan();
            aCustomer.setBillingPlan(newPlan);
        }

        // Client 4
        int weeksDelinquent = (aCustomer == null) ? 0 : aCustomer.getPaymentHistory().getWeeksDelinquentInLastYear();
        System.out.println("Client 4: " + weeksDelinquent);
    }
}

这一段代码是,我们的线下商城服务点,在老客户搬走新客户还没搬进来的时候,会出现暂时没有客户的情况。在每个查询客户信息的地方,都需要判断这个服务点有没有客户,然后再根据判断来获取有效信息。aCustomer === 'unknown' 这是个特例情况,在这个特例情况下,就会使用到很多临时字段,或者说是特殊值字段。这种重复的判断不仅会来重复代码的问题,也会非常影响核心逻辑的代码可读性,造成理解的困难。这里,要把所有的重复判断逻辑都移除掉,保持核心逻辑代码的纯粹性。然后,要把这些临时字段收拢到一个地方,进行统一管理。

优化:

public class NullCustomer extends Customer {
    public NullCustomer() {
        super(new CustomerData("occupant", new BillingPlan(0, 0), new PaymentHistory(0)));
    }
}
// Initial
Site site = (customer==null) ? new Site(new NullCustomer()) : new Site(new Customer(customer));

// Client 1
Customer aCustomer = site.getCustomer();
String customerName = aCustomer.getName();

// Client 2
BillingPlan plan = aCustomer.getBillingPlan();

// Client 3
......

// Client 4
int weeksDelinquent = aCustomer.getPaymentHistory().getWeeksDelinquentInLastYear();

17.过长的消息链(Message Chains)

说明:如果你看到用户向一个对象请求另一个对象,然后再向后者请求另一个对 象,然后再请求另一个对象……这就是消息链。在实际代码中你看到的可能是一 长串取值函数或一长串临时变量。采取这种方式,意味客户端代码将与查找过程 中的导航结构紧密耦合。一旦对象间的关系发生任何变化,客户端就不得不做出 相应修改。

优化:

(1)隐藏委托关系

(2)用提炼函数把使用该对象的代码提炼到一个独立 的函数中,再运用搬移函数把这个函数推入消息链

样例:

const result = a(b(c(1, d(f()))));

优化:

const result = goodNameFunc();

function goodNameFunc() {
  return a(b(c(1, d(f()))));
}

18.中间人(Middle Man)

说明:人们可能过度运用委托。你也许会看到某个类的接口有一半的函数都委托给其他类,这样就是过度运用。这时应该移除中间人,直接和真正负责的对象打交道。

优化:

(1)移除中间人

(2)如果这样“不干实事”的函数只有少数几个,可以运用内联函数把它们放进调用端。

(3)如果这些中间人还有其他行为,可以运用以委托取代超类或者以委托取代子类把它变成真正的对象,这样你既可以扩展原对象的行为,又不必负担那么多的委托动作。

样例:

@Data
public class Product {
  private String name;
  private Price price;
    
  public String get price() {
    return this.price.toString();
  }
  public String get priceCount() {
    return this.price.count;
  }
  public String get priceUnit() {
    return this.price.unit;
  }
  public String get priceCnyCount() {
    return this.price.cnyCount;
  }
  public String get priceSuffix() {
    return this.price.suffix;
  }
}

现在我要访问 Product 价格相关的信息,都是直接通过 Product 访问,而 Product 负责提供 price 的很多接口。随着 Price 类的新特性越来越多,更多的转发函数就会使人烦躁,而现在已经有点让人烦躁了。此时,这个 Product 类已经快完全变成一个中间人了,那我现在希望调用方应该直接使用 Price 类。

优化:

@Data
public class Product {
  private String name;
  private Price price;
    
  public Price get price() {
    return this.price();
  }
}

19.内幕交易(Insider Trading)

说明:

软件开发者喜欢在模块之间建起高墙,极其反感在模块之间大量交换数据, 因为这会增加模块间的耦合。在实际情况里,一定的数据交换不可避免,但我们 必须尽量减少这种情况,并把这种交换都放到明面上来。

优化:

(1)如果两个模块总是在咖啡机旁边窃窃私语,就应该用搬移函数搬移字段减少它们的私下交流。

(2)如果两个模块有共同的兴趣,可以尝试再 新建一个模块,把这些共用的数据放在一个管理良好的地方;或者用隐藏委托关系,把另一个模块变成两者的中介。

(3)如果子类对超类的了解总是超过后者的主观愿望,请运用以委托取代子类以委托取代超类让它离开继承体系。

样例:

@Data
public class Person {
    private String name;
    private Department department;
}

@Data
public class Department {
    private String code;
    private Person manager;
}

在这个案例里,如果要获取 Person 的部门代码 code 和部门领导 manager 都需要先获取 Person.department。这样一来,调用者需要额外了解 Department 的接口细节,如果 Department 类修改了接口,变化会波及通过 Person 对象使用它的所有客户端。

优化:

@Data
public class Person {
    private String name;
    private Department department;

    public String getDepartmentCode() {
        return department.getCode();
    }
    public void setDepartmentCode(String code) {
        department.setCode(code);
    }
    public Person getManager() {
        return department.getManager();
    }
    public void setManager(Person manager) {
        department.setManager(manager);
    }
}

@Data
public class Department {
    private String code;
    private Person manager;
}

20.过大的类(Large Class)

说明:如果想利用单个类做太多事情,其内往往就会出现太多字段。一旦如此,重复代码也就接踵而至了。

优化:

(1)运用提炼类将几个变量一起提炼至新类内。提炼时应该选择类内彼此相关的变量,将它们放在一起。

(2)如果类内的数个变量有着相同的前缀或后缀,这就意味着有机会把它们提炼到某个组件内。如果这个组件适合作 为一个子类,你会发现提炼超类或者以子类取代类型码(其实就 是提炼子类)往往比较简单。

(3)观察一个大类的使用者,经常能找到如何拆分类的线索。看看使用者是否只 用到了这个类所有功能的一个子集,每个这样的子集都可能拆分成一个独立的 类。一旦识别出一个合适的功能子集,就试用提炼类提炼超类或是以子类取代类型码将其拆分出来。

样例:

@Data
public class Product {
    private String name;
    private String price;

    public String getPrice() {
        return getPriceCount() + " " + getPriceSuffix();
    }
    public double getPriceCount() {
        return Double.parseDouble(this.price.substring(1));
    }

    public String getPriceUnit() {
		...
    }

    public double getPriceCnyCount() {
		...
    }

    public String getPriceSuffix() {
		...
    }
}

Product 类中就发现了三个坏味道:基本类型偏执、重复的 switch、中间人。在解决这三个坏味道的过程中,也把 过大的类 这个问题给解决了。

优化:

@Data
public abstract class Price {
    protected String value;
    public double getCount() {
        return Double.parseDouble(this.value.substring(1));
    }
}
@Data
public class CnyPrice extends Price {
    public CnyPrice(String value) {
        super(value);
    }
    ......
}
@Data
public class UsdPrice extends Price {
    public UsdPrice(String value) {
        super(value);
    }
	......
}
@Data
public class HkdPrice extends Price {
    public HkdPrice(String value) {
        super(value);
    }
	......
}
public class PriceFactory {
    
    public static Price createPrice(String value) {
        switch (value.charAt(0)) {
            case '¥':
                return new CnyPrice(value);
            case '$':
                return new UsdPrice(value);
            case 'k':
                return new HkdPrice(value);
            default:
                throw new UnsupportedOperationException("Unsupported unit");
        }
    }

    public static void main(String[] args) {
        Price cnyPrice = PriceFactory.createPrice("¥50.0");
        System.out.println("CNY Price: " + cnyPrice.toString());
        System.out.println("CNY Count: " + cnyPrice.getCnyCount());
    }
}

21.异曲同工的类(Alternative Classes with Different Interfaces)

说明:

使用类的好处之一就在于可以替换:今天用这个类,未来可以换成用另一个 类。但只有当两个类的接口一致时,才能做这种替换。

优化:

(1)可以用改变函数声明 将函数签名变得一致。但这往往还不够,请反复运用搬移函数将某些行为移入类中,直到两者的协议一致为止。

(2)如果搬移过程造成了重复代码, 或许可运用提炼超类补偿一下。

样例:

@Data
public class Employee {
    private int id;
    private String name;
    private double monthlyCost;

    public double getAnnualCost() {
        return monthlyCost * 12;
    }
}

@Data
public class Department {
    private String name;
    private List<Employee> staff;

    public double getTotalMonthlyCost() {
        return staff.stream().mapToDouble(Employee::getMonthlyCost).sum();
    }
    public int getHeadCount() {
        return staff.size();
    }
    public double getTotalAnnualCost() {
        return getTotalMonthlyCost() * 12;
    }
}

在这个案例中,Employee 类和 Department 都有 name 字段,也都有月度成本 monthlyCost 和年度成本 annualCost 的概念,可以说这两个类其实在做类似的事情。我们可以用提炼超类来组织这种异曲同工的类,来消除重复行为。

优化:

@Data
public class Party {
    private String name;
    public double getMonthlyCost() {
        return 0;
    }
    public double getAnnualCost() {
        return getMonthlyCost() * 12;
    }
}
@Data
public class Employee extends Party {
    private int id;
    private double monthlyCost;
    
    @Override
    public double getMonthlyCost() {
        return monthlyCost;
    }
}
@Data
public class Department extends Party {
    private List<Employee> staff;

    @Override
    public double getMonthlyCost() {
        return staff.stream().mapToDouble(Employee::getMonthlyCost).sum();
    }
}

22.纯数据类(Data Class)

说明:所谓纯数据类是指:它们拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长物。纯数据类常常意味着行为被放在了错误的地方。也就是说,只要把处理数据的行为从客户端搬移到纯数据类里来,就能使情况大为改观。但也有例外情况, 一个最好的例外情况就是,纯数据记录对象被用作函数调用的返回结果,比如使用拆分阶段之后得到的中转数据结构就是这种情况。这种结果数据对象有一个关键的特征:它是不可修改的(至少在拆分阶段的实际操作中是 这样)。不可修改的字段无须封装,使用者可以直接通过字段取得数据,无须通过取值函数。

优化:

(1)如果有public字段,应立刻运用封装记录将它们封装起来。

(2)对于那些不该被其他类修改的字段,请运用移除设值函数

样例:

@Data
public class Category {
    private String name;
    private int level;
}

@Data
public class Product {
    private String name;
    private Category category;
    
    public String getCategory() {
        return category.getLevel() + "." + category.getName();
    }
}

Category 是个纯数据类,像这样的纯数据类,直接使用字面量对象似乎也没什么问题。但是,纯数据类常常意味着行为被放在了错误的地方。比如在 Product 有一个应该属于 Category 的行为,就是转化为字符串,如果把处理数据的行为从其他地方搬移到纯数据类里来,就能使这个纯数据类有存在的意义。

优化:

@Data
public class Category {
    private String name;
    private int level;

    @Override
    public String toString() {
        return level + "." + name;
    }
}

public class Product {
    private String name;
    private Category category;
    
    public String getCategory() {
        return category.toString();
    }
}

23.被拒绝的遗赠(Refuse Bequest)

描述:

子类应该继承超类的函数和数据。但如果它们不想或不需要继承,又该怎么 办呢?它们得到所有礼物,却只从中挑选几样来玩! 按传统说法,这就意味着继承体系设计错误。

优化:

(1)为这个子类新建一个兄弟类,再运用函数下移字段下移把所有用不到的函数下推给那个兄弟。这样一来,超类就只持有所有子类共享的东西。

(2)如果子类复用了超类的行为(实现),却又不愿意支持超类的接口时,应该运用以委托取代子类或者以委托取代超类彻底划清界限。

样例:

@Data
public class Party {
    private String name;
    private List<Employee> staff;
}

@Data
public class Employee extends Party {
    private String id;
    private double monthlyCost;
}

@Data
public class Department extends Party {
    public double getMonthlyCost() {
        return getStaff().stream().mapToDouble(Employee::getMonthlyCost).sum();
    }
    public int getHeadCount() {
        return getStaff().size();
    }
}

Employee 类并不关心 staff 这个字段,这就是 被拒绝的遗赠 。重构手法也很简单,就是把 staff 字段下移到真正需要它的子类 Department

@Data
public class Party {
    private String name;
}

@Data
public class Employee extends Party {
    private String id;
    private double monthlyCost;
}

@Data
public class Department extends Party {
    private List<Employee> staff;
    public double getMonthlyCost() {
        return getStaff().stream().mapToDouble(Employee::getMonthlyCost).sum();
    }
    public int getHeadCount() {
        return getStaff().size();
    }
}

24.注释(Comments)

说明:

注释并不是坏味道,并且属于一种好味道,但是注释的问题在于很多人是经常把它当作“除臭剂”来使用。你经常会看到,一段代码有着长长的注释,然后发现,这些注释之所以存在乃是因为代码很糟糕,创造它的程序员不想管它了。当你感觉需要写注释时,请先尝试重构,试着让所有注释都变得多余。

优化:

(1)如果你需要注释来解释一块代码做了什么,试试提炼函数

(2)如果函 数已经提炼出来,但还是需要注释来解释其行为,试试用改变函数声明为它改名;

(3)如果你需要注释说明某些系统的需求规格,试试引入断言

样例:

public static void main(String[] args) {
    double discountRate = getDiscountRate();
    double base = 10;
    //DiscountRate 作为折扣比率,必须要大于0
    if (discountRate > 0) {
        base = base - discountRate * base;
    }
    System.out.println("Final base after discount: " + base);
}

public static double getDiscountRate() {
    return 0.25;
}

对于discountRate参数,在业务逻辑中必须要保持大于0,当他小于0时,应抛出异常

优化:

public static void main(String[] args) {
    double base = 10;
    double discountRate = getDiscountRate();

    assert discountRate > 0 : "Discount rate should be greater than 0";

    if (discountRate > 0) {
        base = base - discountRate * base;
    }
    System.out.println("Final base after discount: " + base);
}

public static double getDiscountRate() {
    return -0.25;
}

参考文档

重构:改善既有代码的设计(第2版) 马丁·福勒(Martin Fowler)

硬核课堂:重构:改善既有代码的设计

代码中常见的 24 种坏味道及重构手法

  • 21
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值