代码坏味道有24种?我看未必

news2024/10/1 21:40:38

微信公众号:牛奶 Yoka 的小屋
有任何问题。欢迎来撩~
最近更新:2024/08/03

[大家好,我是牛奶。]

我在上一篇文章打开IDEA,程序员思考的永远只有两件事!中,通过代码命名、重复代码、合格方法三个章节,着重讲解了24种代码坏味道最常见的五种:注释、命名、重复代码、过长函数、过长的参数列表。在这篇文章中,我将通过对象数据、对象关系、代码表达力及重复的坏味道四个章节,再详细讲一讲剩下的19种坏味道。

《重构》的大佬Martin Fowler给出24种代码坏味道,但我看未必。我认为有些坏味道犯了“重复坏味道”的问题,所以我把它们汇总到最后一章重复的坏味道中,大家看完其他坏味道再看这些,便可很快理解;有些坏味道只是代码啰嗦了点,并不会对业务逻辑产生什么实质影响,我将他们放在了代码表达力的章节;还有些坏味道根因类似,比如基本类型偏执和重复的switch,所以我把它们放到一起,方便理解。本篇文章的阅读导图如下:

 

561edeaf131729c1cf93cf18bd9c7966.png

代码坏味道

总之,当你看完所有的坏味道,会发现大佬翻来覆去,讲的都是那几类问题。愿此篇文章,能使诸君对坏味道代码的敏感度有所提升!

对象数据定义时有哪些坑?

普通数据可变问题

可变数据是Martin Fowler大佬在《重构》第二版新增的坏味道。这个坏味道很是反直觉,我琢磨了半天,才咂巴出点味道。突然想起句名人名言:

我横竖睡不着,仔细看了半夜,才从字缝里看出字来,整段都写着几个字是“别让数据可变”!——坡迅

什么是可变数据?

任何在赋值之后仍可以修改的变量称为可变数据。例如拥有Getter方法和Setter方法的类变量(Getter方法的返参可变导致变量可变);再或者拥有类似Setter设置变更值方法的变量。(咱就说这两类变量常不常用!)

为啥可变数据很危险?

一句话概括,你不知道数据会在哪里被何人以什么方式修改。下面给一个银行存钱的简单案例,上代码:

import java.math.BigDecimal;

public class BankAccount {
private BigDecimal balance;

public BankAccount(BigDecimal initialBalance) {
this.balance = initialBalance;
  }

// 存款方法
public void deposit(BigDecimal amount) {
balance =  balance.add(amount);
  }

// 取款方法
public void withdraw(BigDecimal amount) {
if (amount.compareTo(balance) < 1) {
balance = balance.subtract(amount);
      } else {
          System.out.println("Insufficient funds");
      }
  }

// 获取余额方法
public BigDecimal getBalance() {
return balance;
  }
}

BankService类:

public class BankService {
 private BankAccount account;

 public BankService(BankAccount account) {
 this.account = account;
    }

 // 服务方法,意外地修改了账户余额
 public void performService() {
 // 假设这里执行了一些操作,需要修改账户余额
 account.deposit(BigDecimal.valueOf(100)); // 意外地给账户增加了100元
 }
}

主函数:

public class Main {
 public static void main(String[] args) {
        BankAccount account = new BankAccount(BigDecimal.valueOf(1000)); // 初始余额1000元
 BankService service = new BankService(account);

 // 显示初始余额
 System.out.println("Initial balance: " + account.getBalance());

 // 执行服务,意外修改了账户余额
 service.performService();

 // 显示修改后的余额,意外地增加了100元
 System.out.println("Balance after service: " + account.getBalance());
    }
}

代码中的银行余额balance为可变变量,该变量会被传递到银行服务类BankService中时,该类可能是另一位同事负责开发,他在执行服务的方法中意外修改了银行余额,最终导致BankAccount类中的可变变量发生了非预期的变化。

数据可变带来的不可控和难定位等风险远比我们想象严重,甚至出现了完全建立在“数据永不改变”概念基础上的软件开发流派——函数式编程。在该编程范式中,如果要更新一个数据结构,就返回一份新的数据副本,旧的数据仍保持不变。

我仔细想了想,发现写过的大部分类中的可变变量,基本都是一次赋值,需要再次赋值都会new出一个新类。所以,针对可变数据,Martin Fowler给出的优化建议是:

  • 将可变数据设置为私有且不可变状态(private final)

  • 将参数赋值优化为使用有参构造函数

  • 必须二次赋值时使用新的数据副本

作者倡导类的创建和类变量的赋值要同时进行,创建类的时候就要初始化变量,自此不可更改,非要更改就重新创建一个新类。

上述银行余额问题,满足优化建议第二点,但不满足第一三点。而银行余额是必须要存取,因此,BankAccount类代码优化后为:

public class BankAccount {
  private final BigDecimal balance;

  public BankAccount(BigDecimal initialBalance) {
      this.balance = initialBalance;
  }

  public BankAccount deposit(BigDecimal amount) {
      if (amount.compareTo(BigDecimal.ZERO) <= 0) {
          throw new IllegalArgumentException("Deposit amount must be positive.");
      }
      return new BankAccount(this.balance.add(amount));
  }

  public BankAccount withdraw(BigDecimal amount) {
      if (amount.compareTo(BigDecimal.ZERO) <= 0 || amount.compareTo(this.balance) > 0) {
          throw new IllegalArgumentException("Invalid withdrawal amount.");
      }
      return new BankAccount(this.balance.subtract(amount));
  }

  public BigDecimal getBalance() {
      return balance;
  }
}

优化后,余额balance成为不可变变量,依然作为构造函数参数不变。同时存取方法返回的是新的实例对象,这样即使在BankService类的performService方法中误操作,最后在主函数中打印account.getBalance()银行余额,依然是1000元,不会平白无故新增100元。大家可以在编辑器中进行简单测试。

新的问题随之而来,如果有个类中有1000个变量,难道往构造函数中传1000个参数么?答案是,不用,用构建器替代构造函数(构建器可直接使用Build注解)。

上代码:

@Data
public class BankAccount {
    public BigDecimal balance;
    public String amount;
}

应优化为

@Builder
public class BankAccount {
  private final BigDecimal balance;
  private final String amount;
}

构建器使用方式:

 BankAccount bankAccount = BankAccount.builder().balance(0.00).amount("1000").build();

使用构建器后,就可以选择性的赋值参数,添加参数也不影响旧代码。提高了代码的扩展性和可维护性。

如果可变数据赋值固定,赋值总是那几个值,比如订单状态等。开源项目Moco作者郑晔大佬还提出一个优化建议:将可变数据的赋值操作封装为一个不带参的类方法,取代Setter方法。比如这种:

原代码:

public void approve(final long bookId) {
...
book.setReviewStatus(ReviewStatus.APPROVED);
...
}

优化后可为:

public void approve(final long bookId) {
...
book.approve();
...
}
class Book {
public void approve() {
  this.reviewStatus = ReviewStatus.APPROVED;
}
}

这样既能表明该操作逻辑,又能避免变量不可控,同时维持了可变数据的可变特性。

全局数据可变问题

明白了可变数据,全局数据的可变问题就很好理解了。全局数据的优点:全局可修改。但其带来优点的同时也带来风险,你很难知道在全局哪处什么时候做了修改,这种BUG定位又极其困难。举个例子,某个类中定义了两个public静态变量:

import java.util.HashMap;
import java.util.Map;

public class ShoppingInfo {
  public static Map<String, Integer> productAndNum = new HashMap<>();
  public static Integer productUpLimit = 10;

}

但是在其他地方又出现令人迷惑的修改:

public class Market {
  private Map<String, String>productAndNum = new HashMap<>();
  public static Double calculateChenGuangMarketPrice(String type, Integer number) {
      Double prices;
      // 迷惑的修改
      productUpLimit = 3;
      productAndNum = new HashMap<>();
}

有些IDEA不会提示带ShoppingInfo类名来使用静态变量(如ShoppingInfo.productUpLimit),如果无意中命名了一个和静态变量同名的变量,极其容易误改静态变量值。因此建议:

  • 全局变量使用全部大写命名

  • 将全局变量和使用全局变量的方法聚合到一个类中

  • 将该类中的全局变量设置关键字static final,保证该变量类中私有且一旦赋值不可被其他地方更改

全局数据的重构优化

一句话,将使用全局变量的代码提取为方法并移动至全局变量对应的类中,之后实例化方法,设置全局变量为私有且不可变。对应的快捷键如下:

快捷键目的
Crtl+Alt+M选中使用全局变量的代码提取方法
F6将提取的方法转移到全局变量对应的类中
Refactor->Convert To Instance Method将移动后的静态方法转变为该类的实例化方法

数据泥团

你常常可以在很多地方看到相同的三四项数据:两个类中相同的字段、许多函数签名中相同的参数。这些总是绑在一起出现的数据真应该拥有属于它们自己的对象。——Robert C. Martin

这个坏味道相对容易,如大佬所言,多个类中数据有重复或者多个方法中的参数有重复,比如联系人姓名,性别,电话这几个参数,完全可以聚合到联系人类中,省、市、区、县等参数完全可以聚合到地址类中。优化方法:引入参数对象,把关联性强的参数聚合到一起。

排查方法:

删掉众多数据中的一项。如果这么做,其他数据有没有因而失去意义?如果它们不再有意义,这就是一个明确信号:你应该为它们产生一个新对象。——Robert C. Martin

趁机复习几个重构优化的快捷键:

refactor的快捷键为:Ctrl+Alt+Shift+T

快捷键功能
Refactor+Extract Delegate提取一个新类
Refactor+Introduce Parameter Object引入一个新的参数对象
Ctrl+Alt+M提取方法
Ctrl+Alt+N内联
F6移动方法
Crtl+Alt+C提炼常量
Alt+Delete安全刪除

临时字段

有时你会看到这样的类:其内部某个字段仅为某种特定情况而设。这样的代码让人不易理解,因为你通常认为对象在所有时候都需要它的所有字段。——Robert C. Martin

Martin大佬认为,特殊情况才使用的类变量属于临时变量,这类变量不应该出现在该类中。简而言之,如果一个字段的使用频率很低,那这个字段及其关联的方法应搞出去,要么字段用方法替换掉,要么单独放到一个类中。比如有个账户类,类中增加余额和减少余额方法使用频率很高,但是账户评估使用频率很低,那就把账户评估相关的方法和字段抽取到一个单独的类中。这个坏味道暗含的原则是,一个类中所有字段的使用频率应该是相同的,不同频的字段不应放到一起。否则会让人难以理解。

举个取代临时字段的例子:

public class GradeBook {
  private List<Double> grades;
  private Double maxGrade; // 临时字段,用于存储最高分

  public GradeBook(List<Double> grades) {
      this.grades = grades;
  }

  public void processGrades() {
      maxGrade = Double.MIN_VALUE; // 初始化最高分为可能的最低值
      for (Double grade : grades) {
          if (grade > maxGrade) {
              maxGrade = grade;
          }
      }
      // 使用 maxGrade 做其他处理...
  }

  public Double getMaxGrade() {
      return maxGrade;
  }
}

maxGrade是临时字段,使用方法优化后:

public class GradeBook {
  private List<Double> grades;

  public GradeBook(List<Double> grades) {
      this.grades = grades;
  }

  public void processGrades() {
      Double maxGrade = findMaxGrade();
      // 使用 maxGrade 做其他处理...
  }

  private Double findMaxGrade() {
      return grades.stream()
                   .max(Double::compare); // 使用 Stream API 找出最高分
  }

  public Double getMaxGrade() {
      return findMaxGrade();
  }
}

上述案例中存储最高分的maxGrade字段使用频率很低,则使用查找最大分数方法对该字段进行代替。

基本类型偏执

这名字起的,好像大家都偏爱基本类型。但其实并非偏爱或者偏执。我觉得它应该称基本类型陷阱更为合适。为何出此言,且听我道来:

当我们面对业务需求中存在价格,坐标等名词时,我们起手就是double price和int[][]Coordinates;这如果是我们在刷算法题集,那这样写完全ok。但在实际业务需求中,价格 ≠ "double price",坐标 ≠ "int[][]Coordinates"。为啥?double类型的范围是-1.79E+308 ~ +1.79E+308,请问价格可以是负数么?int的类型是 -2147483648~2147483647,请问世界坐标系的范围会达到200多万么?

显然不等价,所以我们一定会对这样的类型进行处理,比如获取到对应的价格就会校验是否为负数,如果为负数就抛出异常。

if (price < 0) {
    throw new IllegalArgumentException("Price should be positive");
  }

问题是,难道我们要在所有用到价格的地方进行异常校验么?显然太重复了。最好的办法就是,把价格封装为一个类对象,用对象取代基本类型:

   class Price {

    private long price;

    public Price(final double price) {
      if (price < 0) {
        throw new IllegalArgumentException("Price should be positive");
      }
      this.price = price;
    }
  }

一旦用对象替代,你就可以在对象内为所欲为,比如限定价格范围,比如要求价格必须保留两位小数,比如限定价格的币种等。类对象相对基本类型更接近于实际事物,不断完善类对象的过程,其实就是对现实事物建模的过程。

还有一类使用的基本类型的参数叫类型码,即用来控制走哪一个代码逻辑分支的参数。还记得我在上一篇文章讲的标记参数么?(打开IDEA,程序员思考的永远只有两件事!),标记参数更多指布尔类型参数,类型码则参数类型不定,但两者都用来区分代码逻辑分支。根据SRP原则,本身一个方法就建议只履行一个相关职责。一旦有了类型码或者标记参数,他便存在多个分支履行多个职责的可能。在上一篇文章我建议将标记参数区分的逻辑代码拆分成多个不同的方法。而面对类型码,同样如果每种类型下的处理逻辑相对复杂,那就把各分支的逻辑拆分成各个子类使用子类取代类型码是常见的一种重构手段。在实际编码时,多分支情况具体运行哪一个子类逻辑,可以在工厂模式中进行选择。

这里需要强调的是,子类取代类型码并不能消除条件分支,它只是将分支的逻辑从方法中搬移到子类中,然后在工厂类中选择具体的类,目的是为了更方便扩展和日后维护。举个产品的例子:

优化前的代码:

public class Product {
 public enum ProductType {
 ELECTRONIC, BOOK
 }

 private ProductType type;
 private final double price;

 public Product(ProductType type, double price) {
 this.type = type;
 if (price < 0) {
 throw new IllegalArgumentException("Price cannot be negative");
        }
 this.price = price;
    }

 public double getPrice() {
 return price;
    }

 public void display() {
  // 类型码
 switch (type) { 
 case ELECTRONIC:
                System.out.println("Electronic product with price: " + price);
 break;
 case BOOK:
                System.out.println("Book product with price: " + price);
 break;
 default:
 throw new IllegalStateException("Invalid product type: " + type);
        }
    }
}

优化后的代码:

Price类:

public class Price {
    private final double value;

    public Price(double value) {
        if (value < 0) {
            throw new IllegalArgumentException("Price cannot be negative");
        }
        this.value = value;
    }

    public double getValue() {
        return value;
    }
}

product类:

public interface Product {
    void display();
}

public class ElectronicProduct implements Product {
    private final Price price;

    public ElectronicProduct(Price price) {
        this.price = price;
    }

    @Override
    public void display() {
        System.out.println("Electronic product, price: " + price.getValue());
    }
}

public class BookProduct implements Product {
    private final Price price;

    public BookProduct(Price price) {
        this.price = price;
    }

    @Override
    public void display() {
        System.out.println("Book product, price: " + price.getValue());
    }
}  

产品工厂类:

public class ProductFactory {
  // 根据类型参数决定创建哪种产品
  public Product createProduct(String type, Price price) {
      if ("Electronic".equalsIgnoreCase(type)) {
          return new ElectronicProduct(price);
      } else if ("Book".equalsIgnoreCase(type)) {
          return new BookProduct(price);
      } else {
          throw new IllegalArgumentException("Unknown product type: " + type);
      }
  }
}

上述案例解决了基本类型坏味道涉及到的两个问题。

重复的switch

说到类型码,顺路可以引入另一个坏味道--重复的switch。

重复的switch 的问题在于:每当你想增加一个选择分支时,必须找到所有的 switch,并逐一更新。多态给了我们对抗这种黑暗力量的武器,使我们得到更优雅的代码库。我们甚至还听过这样的观点:所有条件逻辑都应该用多态取代,绝大多数 if 语句都应该被扫进历史的垃圾桶。****——Robert C. Martin

接着用上面产品的原案例解释下,假如在不用子类的情况,需要计算不同产品价格,需要获取产品折扣,需要设定不同产品购买数量限制,那原案例代码列出的每种上述对应方法中,都要用switch语句来区分不同产品,编写不同的逻辑,这样多个方法中就存在多个近乎重复的switch代码。这个坏味道其实本质大同小异,都是在说当代码存在多个条件下的多个场景处理逻辑,尽可能把各个场景使用子类表示,这样,选取哪个子类只需要在工厂中写一次,避免了switch语句在每个动作方法中的重复改动。

当然,并非所有的switch都必须转换成子类,如果有些场景很简单,不用建模也可以解决,那就没必要增加代码的复杂性。

总结

这一章节从对象数据的角度,讲解了可变数据、全局可变数据、数据泥团、临时字段、基本类型偏执、重复的switch六种代码坏味道。为了防止数据可变,可以设置数据状态,添加有参构造函数;面对数据泥团,可以抽取封装成新类;面对低频字段,可以用方法或封装新类消除低频字段;面对不够明确的基本类型,同样可以封装新的类替代;面对多个条件选择的switch,工厂选择类加封装子类替代。

在对象数据的处理中,我们能看到除了可变数据坏味道,其余数据坏味道基本都是使用相同的重构手法--封装!在上一篇的重复代码与合格方法的坏味道处理手法也是封装(封装成方法或者类),甚至我们后面讲到的发散式变化、霰弹式修改等对象关系问题的处理更是封装!

还记得Java的三大特性:封装,多态,继承。面对不同的业务场景,能够进行不同的合理的封装,才是迈向高手的关键一步!

对象关系间会有什么幺蛾子?

发散式变化

如果某个模块经常因为不同的原因在不同的方向上发生变化,发散式变化就出现了。——Robert C. Martin

发散式变化说的是,不要总是在改同一模块,翻译过来即不要让某一方法或类承担的职责过多。不然任何问题都来同一方法或类里看,改来改去就容易改出问题。出现发散式变化有两个原因,要么类内代码间耦合度太高,要么类内职责太多。优化方法就很简单了,将耦合度较高的代码进行聚合,并根据职责封装为各个新类,每个类各司其职,保证每次改动只在该类中进行。为了减少外部使用该类功能的地方需要同步改动,所以尽可能将该类对外的方法名称和出入参抽象化和固定化,比如之前入参为一个,后面突然改成两个,那你直接就把出参定义一个类对象,把这两个出参放进去嘛,省的别人跟着你动来动去。每次改动都要遵循对外不变,对内优化的原则。

霰(xian)弹式修改

如果每遇到某种变化,你都必须在许多不同的类内做出许多小修改,你所面临的坏味道就是霰弹式修改。——Robert C. Martin

和发散式变化相反,发散式变化是逮着一个模块薅,霰弹式变化是到处逮模块薅。导致霰弹式变化的原因是,没有很好的将关联性较强的数据或方法汇总到一个类中。优化方法就是尽可能聚合呗(这也正体现DDD设计中高内聚低耦合的思想,后期分享)。在聚合过程中注意一点,有时候某一数据计算出来的派生数据可能分散在各个其他类中,导致派生数据的修改比较分散。这个时候可以在源数据所在类中搞一个转化方法,这个方法计算所有的派生数据汇总到一起作为出参,之后每个用到派生数据的地方直接调用该方法,可以有效避免霰弹式修改,同样也可以减少重复代码。

依恋情结

但有时你会发现,一个函数跟另一个模块中的函数或者数据交流格外频繁, 远胜于在自己所处模块内部的交流,这就是依恋情结的典型情况。——Robert C. Martin

从问题症状来看,它其实和霰弹式变化很像。都是本模块和其他模块交互出了问题。前者是对外交互模块太多,后者是对外交互模块太频繁。但不论怎样,优化措施都万变不离其宗,即尽可能把强关联性的类和数据、方法聚合到一起。交互太频繁,那就把这一步频繁操作移动到依赖的类中。举个例子:

public class Order {
  private List<Item> items; // 订单包含多个商品项

  public double calculateTotal() {
      double total = 0;
      for (Item item : items) {
          total += item.getPrice(); // 这里Order类的方法依赖于Item类的状态
      }
      return total;
  }
}

public class Item {
  private double price;

  public double getPrice() {
      return price;
  }
}

上述代码Order类在for循环中频繁访问Item中的getPrice方法。解决措施就是把这个循环计算方法移动到Item类中,如下:

public class Item {
  private double price;

  public double getPrice() {
      return price;
  }

  // 将计算总价的逻辑移动到Item类中
  public double calculateTotal(List<Item> items) {
      double total = 0;
      for (Item item : items) {
          total += item.getPrice();
      }
      return total;
  }
}

public class Order {
  private List<Item> items;

  public double getTotalPrice() {
      return new Item().calculateTotal(items); // 委托Item类来计算总价
  }
}

这样,对数据的频繁访问就变成对方法的一次访问。访问频率大大降低。

过长的消息链

有人称这个坏味道为火车残骸。说的更形象了点。 举个例子:

String name = book.getChinaBook().getAuthor().getName();

像火车一样,点出一节又一节。这种最大的问题是,用它的地方还要了解它的内部结构。你想获取个国内书籍作者的名字,你还需要了解ChinaBook对象和Author对象,这就很离谱,我想看个电视,难道还要先了解下怎样组装电视?另外,如果调用链其中一节发生变化,使用端还要跟着更改,增大了对象使用的风险。最好的解决办法是,将获取对象的行为封装成一个方法:

class Book {
    ...
    public String getAuthorName() {
      return this.chinaBook.author.getName();
    }
    ...
  }
  String name = book.getAuthorName();

当然并不是所有的消息链都是坏味道,比如stream管道流也是消息链但它不属于坏味道,实际开发中需要注意甄别。

内幕交易

内幕,是指只能在两者之间进行,交易,是指两者之间相互交换信息;所以内幕交易这个坏味道,重点指的是两个类之间频繁的数据调用和信息交换,这些数据信息多指私有数据(还记得依恋情结坏味道么?它和内幕交易很类似,不过它是一个类数据频繁的依赖另一个类数据)。但 Martin大佬又在这个坏味道里面多加了一句『继承常会造成密谋,因为子类对超类的了解总是超过后者的主观愿望。』。我也是服了这个老六,继承本身父子类间就是信息继承,何来交易?还密谋,人家谈自己的家事,何来密谋?我是不认为继承是一种内幕交易,但我承认,一些不合理的继承确实也是一种坏味道,我们一一来说吧。

先说相互依赖,举个很简单的例子: BankAccount类中:

public class BankAccount {
  private static Double balance = 0.00;
  private final String amount;

  public Double getActualBalance() {
      return balance > 100 ? 50.00 : 100;
  }

  public Double getAccountBalance(BankService bankService) {
      return  bankService.PerformService() + 100;
  }
}

BankService类中:

public class BankService {
  private Double getMoney;
  private Double exchangeRate;

  public Double PerformService() {
      return getMoney * exchangeRate;
  }
  public Double addMoney(BankAccount bankAccount) {
      return bankAccount.getActualBalance() + 100.00;
  }
}

从代码可以明显看出,BankAccount类和BankService类都爱多管闲事,前者的getAccountBalance方法没事去管BankService的数据,后者的addMoney方法没事去管BankAccount的数据,最好办法用F6搬移函数,职能归位,各管各的:

BankAccount类
public class BankAccount {
  private static Double balance = 0.00;
  private final String amount;

  public Double getActualBalance() {
      return balance > 100 ? 50.00 : 100;
  }

  public Double addMoney() {
      return getActualBalance() + 100.00;
  }
}

BankService类:

public class BankService {
  private Double getMoney;
  private Double exchangeRate;

  public Double PerformService() {
      return getMoney * exchangeRate;
  }

  public Double getAccountBalance() {
      return  PerformService() + 100;
  }
}

如果一个方法中同时用到两个类,也可以考虑把这一类的方法单独放到一个新类中。

第二种不合理的继承,是指有些子类虽然继承了父类,但可能只用到了父类很小的一部分功能,或者根本不使用父类功能。当父子关系“不够亲密”时,继承的耦合有时候就会成为负担。因为每次父类的调整都会影响子类。子类还需要承担很多不必要的“父爱”。所以,这个时候作者建议使用委托替代子类。委托说白了就是两个类取消之前的继承关系,一个类需要另一个时,直接new出另一个类对象并使用其方法。举个例子:

interface DocumentFormat {
  String formatDocument(String content);
}

class PDFDocumentFormat implements DocumentFormat {
  public String formatDocument(String content) {
      // 格式化为 PDF
      return "PDF format of content: " + content;
  }
}

class WordDocumentFormat implements DocumentFormat {
  public String formatDocument(String content) {
      // 格式化为 Word
      return "Word format of content: " + content;
  }
}

class Document {
  private DocumentFormat format;

  public Document(DocumentFormat format) {
      this.format = format;
  }

  public String createFormattedDocument(String content) {
      return format.formatDocument(content);
  }
}

public class Main {
  public static void main(String[] args) {
      Document pdfDocument = new Document(new PDFDocumentFormat());
      System.out.println(pdfDocument.createFormattedDocument("Sample content"));

      Document wordDocument = new Document(new WordDocumentFormat());
      System.out.println(wordDocument.createFormattedDocument("Sample content"));
  }
}

在这个例子中,Document 类不直接继承任何特定的格式类,而是委托给 DocumentFormat 类来进行具体的文档格式实现。

在IDEA中委托取代继承的快捷键是Crtl+Alt+Shift+T->Replace Inheritance with Delegation。

被拒绝的遗赠

这个坏味道讲的是子类继承父类,又不全部使用父类功能,未使用的部分就是被拒绝的遗赠。听起来是不是和内幕交易中不合理的继承很像?我觉得它们讲的就是同一个问题!

子类应该继承超类的函数和数据。但如果它们不想或不需要继承,又该怎么办呢?它们得到所有礼物,却只从中挑选几样来玩!——Robert C. Martin

不过,内幕交易重点放在被拒绝的遗赠过多的情况,直接建议委托替代继承。而在该坏味道中,它根据父子间亲密程度,提供了两个解决办法,父子间较为亲密时,把父类相对子类多余的少部分方法单独下沉到另一个子类中;父子间不是很亲密时,直接用委托取代继承。

总结

这一章介绍了发散式变化、霰弹式修改、依恋情结、过长的消息链、内幕交易、被拒绝的遗赠六个对象关系间的坏味道,其实发散式变化更多是对象内部职责不单一的问题,但是为了和霰弹式修改作比较,我统一放到这一章节。整体来说,你看这对象之间不能太亲密--霰弹式修改、依恋情结;不能太不亲密--被拒绝的遗赠;也不能偷偷亲密--内幕交易;多个对象间更不能一节一节的成套娃般亲密,这个尺度可不好把握哦~

你代码的表达力行不行?

循环语句

从最早的编程语言开始,循环就一直是程序设计的核心要素。但我们感觉如今循环已经有点儿过时,就像喇叭裤和植绒壁纸那样。——Robert C. Martin

循环语句本身没啥问题,就是有些集合、数组用for循环操作提高了代码的复杂度,管道的链式操作更简洁连贯且不容易出错,另外数据量大时使用管道的并行流还能提高性能,因此代码中针对集合或数组过滤、映射、求和、查找第一个匹配项、分组、去重、统计常用管道替代。举个管道并行流的例子:

传统计算数组中所有数平方和:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int sumOfSquares = 0;
for (int number : numbers) {
  sumOfSquares += number * number;
}
System.out.println("Sum of squares (sequential): " + sumOfSquares);

使用管道的方法:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int sumOfSquares = numbers.parallelStream() // 转换为并行流
                              .mapToInt(n -> n * n)
                              .sum(); 
System.out.println("Sum of squares (parallel): " + sumOfSquares);

案例中,四行代码被精简为了一行,代码量减少4倍!parallelStream() 方法将顺序流转换为并行流。并行流会自动将数据分成多个块,并在多个线程中并行处理这些块,大大提高计算性能。

最后复习下常用替换管道快捷键:

快捷键含义
Alt+Enter->Replace with collect用collect替代for循环
Alt+Enter->Replace with sum替代for循环求和
Alt+Enter->Replace with Map.forEachforEach替代复杂for循环

冗赘(zhuì)的元素

这个代码坏味道的英文名是Lazy Element,直译过来是懒惰元素,我理解作者想表达的是某些元素长期不被用到,结果翻译过来起名为冗赘元素,忍不住就想吐槽下,非得搞个生僻字,叫冗余元素不也挺好嘛~

言归正传,这个坏味道很简单。它强调的是有些已经注释掉的代码,删掉它;有些暂时没用以后可能有用的代码,删掉它;有些经过很久依旧职责不多的子类,就把它和父类合并。为的就是代码能够简洁明了。来看看大佬原话怎么说:

可能有这样一个函数,它的名字就跟实现代码看起来一模一样;也可能有这样一个类,根本就是一个简单的函数。这可能是因为,起初在编写这个函数时,程序员也许期望它将来有一天会变大、变复杂,但那一天从未到来;也可能是因为,这个类原本是有用的,但随着重构的进行越变越小,最后只剩了一个函数。不论上述哪一种原因,请让这样的程序元素庄严赴义吧。——Robert C. Martin

排查这类坏味道的方式:可以通过IDEA中Analyze——Inspect Code——Declaration redundancy,再结合人工判断,批量发现冗赘代码,常用重构快捷键如下:

快捷键含义
Crtl+Y整行删除
Alt+Delete安全删除
Crtl+Alt+P抽取为方法参数
Crtl+Alt+F提取为属性字段

夸夸其谈通用性

这里说的通用性核心指的是那些为了通用而创造的接口或抽象类,但有些接口或抽象类长期下来可能就只有一个实现类,这个时候就得重新审视下是否有必要存在这个接口或抽象类了。还有些类或方法只有测试代码中用到,这些类和方法同样可以干掉它。

在编码中专家们强调要抽象,现在又不能太过抽象。这中间的尺度,还真不好把握。不同业务不同场景给出的抽象也不同,谁也不能保证这个抽象未来就不会使用。只能开发依据业务需求自行把握。我觉得这个坏味道基本也是属于冗余设计,完全可以和上一个坏味道归类到一起。

中间人

这个代码坏味道也很简单。如果一个类或对应的方法只负责包装另一个类中方法的返回,不做任何实际的逻辑处理,那就干掉它,直接使用原方法。举个例子:

public class Order {
  private String id;
  private double amount;
}

public class OrderProcessor {
  public void processOrder(Order order) {
      // 实际的订单处理逻辑
  }
}

public class OrderManager { // 中间人
  private OrderProcessor processor;

  public OrderManager(OrderProcessor processor) {
      this.processor = processor;
  }

  public void submitOrder(Order order) {
      // 这里仅仅是转发订单,没有添加任何逻辑
      processor.processOrder(order);
  }
}

上面的订单管理类就是个中间人,不做任何具体逻辑,只管转发。这种就可以直接干掉。使用原方法:

public class OrderProcessor {
  public void handleOrder(Order order) {
      // 订单处理逻辑
  }

  public void submitOrder(Order order) {
      ...
      // 直接处理订单,不再需要中间人
      handleOrder(order);
  }
}
// 现在客户端代码可以直接使用 OrderProcessor
public class Main {
  public static void main(String[] args) {
      OrderProcessor processor = new OrderProcessor();
      Order order = new Order(/* 初始化订单数据 */);
      processor.submitOrder(order);
  }
}

还有一种中间人类似如下这种:

public class BankAccount {
  private final Double balance;
  private final String amount;

  public Double getActualBalance() {
      return getBalance() > 100 ? 50.00 : 100;
  }

  public Double getBalance() {
      return balance;
  }
}

在同一个类中,多此一举搞了一个getBalance,直接使用balance多好:

public class BankAccount {
  private final Double balance;
  private final String amount;

  public Double getActualBalance() {
      return balance > 100 ? 50.00 : 100;
  }
}

解决中间人问题提供两个快捷键:

快捷键含义
Ctrl+Alt+Shift+T -> Remove Middleman移除中间人(第一类中间人)
Ctrl+Shift+N内联(第二类中间人)

总结

这一节介绍了循环语句、冗赘(zhuì)的元素、夸夸其谈通用性、中间人这几种坏味道。这几种坏味道本身对业务逻辑或性能不会产生什么影响。单纯为了使代码更简洁清晰,更易理解,为了提高代码表达力所提出。所以整体优先级相对较低,有些坏味道出现重复,倒也可以理解。

重复的坏味道

根据标题可以看出,这一节的坏味道其实在之前已经提出,但经过一定包装,我们在这一节再重新认识一下。

过大的类

在我看来,这个坏味道更多是其他一些坏味道未解决而造成的表象。先看定义,什么是过大的类?由于属性未分组和职责不单一而包含过多属性、方法和代码行的类。 Martin提出的解决办法是拆分过大的类保证职责单一。可你仔细想想,在上一篇解决重复代码坏味道时,抽取公共方法,方法上移超类,难道不是精简类?解决数据泥团坏味道时将同类属性提炼新类中,难道不是精简类?解决类型码参数和重复的switch坏味道时,把条件分支逻辑用子类替代,难道不是精简类?解决循环语句用管道方法难道不是精简类?解决冗赘元素、临时字段、中间人坏味道,难道不是精简类?所以,何必单独过大的类列出来增加读者的学习负担呢? Martin大佬,不好意思,这里我想批评下你~

异曲同工的类

类异曲同工,是指两个类功能完全相同或者大部分功能完全相同,只是定义不同。这种坏味道没什么可说的,完全相同,就舍弃掉一个,部分相同,就想办法把两个类合并到一起,相同的部分提取父类中。忍不住又想吐槽,请问这个和重复代码的坏味道有什么区别嘞?

纯数据类

我们正常的类,应该包括该类对象需要的属性和常见的对这些属性的业务操作行为。这样才能通过类对象,对现实生活中的某一对象进行完整的建模。但是纯数据类只包括数据字段和访问这些字段的函数,除此之外一无所长。那就存在问题了,我举一个在基本类型偏执坏味道讲过的案例,比如一个金额类,里面某个属性字段是BigDecimal Amount,这个字段就包含金额不为负数,金额保留两位小数,金额有个最大限制等业务操作行为,如果只提供一个纯数据类,那在所有使用到金额类的地方都需要添加这几种操作行为,如果需要对金额类某一字段添加一种新的业务行为,就要所有涉及的地方都进行修改。这个时候,霰弹式修改坏味道就来了。另外,纯数据类中的字段一般为可变字段,这又会导致文章开头讲的可变数据的坏味道。

但并非所有的纯数据类都是坏味道。我们在DDD设计模式下进行开发时,会有很多model类、VO类、DTO类,这些纯数据类的字段一般和数据库的字段一一对应。对这些字段的操作一般意味着最终对数据库实体字段的操作,而不是某一类对象的特有行为,且每个类的行为并不固定,搞成充血模型(数据行为不分离)反而可能会导致过大的类坏味道。所以这一种类没有数据行为也罢。另外,如果一个纯数据类用作函数的返回结果,不用来进行业务处理,这种类中只有纯数据也无可厚非。

总结一下,纯数据类可能是坏味道,但并非一定是坏味道,它是坏味道时一定会引发可变数据、霰弹式修改等其他坏味道。所以,我觉得,这个坏味道也是重复的坏味道。

总结

这一节介绍了纯数据类、过大的类、异曲同工的类三个坏味道。之所说他们是重复的坏味道,是因为它们都是因为没有好好解决之前的坏味道导致的连锁味道。如果有理解之前的坏味道,那理解这几个坏味道也不是问题。

最后

至此,所有的坏味道介绍完毕。命名、注释虽不起眼,却至关重要;重复代码虽有提示,但还得细细甄别;对象数据、对象方法、对象关系的设计,每一个看似平平,实则大有乾坤;最后没事还得多提升提升代码格调,该删的删,该简的简(管道替代循环等等),多挖掘挖掘编辑器的快捷键,熟练掌握这些,你离开发大神就不远喽!

----------------end----------------

我是牛奶,目前是一名互联网开发菜鸟,主要聚焦于互联网技术开发和个人成长的高营养价值内容分享,感兴趣的小伙伴可以在下方加个关注,大家一起共同学习和进步。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1979494.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PXE实验

实验前准备 关闭VMware的dhcp 点击 编辑 点击 虚拟网络编辑器 选择 NAT模式 将dhcp取消勾选 准备两台虚拟机 一台试验机&#xff0c;&#xff08;网络环境正常并且有图形化的界面的rhel7&#xff09; 一台测试机 init 5 --------------> 开启图形化界面 如…

《C/C++实战进阶》介绍

&#x1f680; 前言 本文是《C/C实战进阶》专栏的说明贴&#xff08;点击链接&#xff0c;跳转到专栏主页&#xff0c;欢迎订阅&#xff0c;持续更新…&#xff09;。 专栏介绍&#xff1a;以多年的开发实战为基础&#xff0c;总结并讲解一些的C/C基础与项目实战进阶内容&…

AI用Alice_split_toolset切割音频的采样率

AI用Alice_split_toolset切割音频的采样率 目录 AI用Alice_split_toolset切割音频的采样率 一、Sample rate采样率的概念 二、Alice_split_toolset切割音频的参数 2.1、字符串参数--input_folder输入文件夹路径 2.2、字符串参数--output_folder输出文件夹路径 2.3、字符串…

第一次作业,sql注入总结

sqli-labs靶场中演示&#xff1a; less1&#xff1a; 注入点为&#xff1a; $sql"SELECT * FROM users WHERE id$id LIMIT 0,1"; get输入一个id&#xff0c;可以逃逸出单引号来实现sql注入。 正常的输入为&#xff0c;输出数据库中查询的内容。 http://127.0.0…

如何将PyCharm 中使用 PDM 管理的 Django 项目迁移到 VS Code 并确保一切正常工作?

嗨&#xff0c;我是兰若姐姐&#xff0c;相信很多小伙伴都遇到过这种情况&#xff0c;使用pycharm用习惯了&#xff0c;想换个编辑器&#xff0c;比如换成vscode&#xff0c;今天就告诉大家&#xff0c;如果轻松切换到vscode 步骤 1&#xff1a;在 VS Code 中打开项目 打开 V…

并行状态的广播事件

平行状态的广播事件 此示例显示了并行状态下事件广播操作的行为。 最初&#xff0c;图表处于休眠状态。并行子状态A.A1.A1a和A.A2.A2a处于活动状态。事件E_one发生并唤醒图表&#xff0c;图表从根向下通过层次结构处理事件&#xff1a; 1 图表根会检查根级别是否存在E_one导致…

Docker简介 MacM1安装Docker

文章目录 1 Docker简介2 Docker VS 虚拟机1 Docker优势2 Docker用途 3 MacM1 下载安装Docker1 配置环境变量 4 配置Docker2 设置Docker资源3 设置Docker镜像 参考 1 Docker简介 Docker主要解决了软件开发和运行配置的问题&#xff0c;但是由于其功能的强大&#xff0c;也被应用…

跨境电商下载工具天猫主图sku图等图片信息

优美的图片是电商卖家吸引顾客、展示商品魅力的关键。高质量的图片能够提升产品吸引力&#xff0c;增强用户信任感&#xff0c;促进购买决策&#xff0c;从而直接影响销量和店铺形象。在视觉营销的时代&#xff0c;优秀的商品图片更是流量转化的利器。 使用图快下载器&#xf…

多租户系统数据隔离方案

目录 前言 数据行 数据表 基于业务场景 基于数据量 数据库 数据源表 动态数据源 前言 多租户系统是一种将多个客户的数据和应用程序分开的系统&#xff0c;每个客户被视为一个独立的租户&#xff0c;互不干扰。实现多租户系统的关键之一是确保数据的隔离。 数据隔离的…

Windows 添加自定义服务实现开机(用户登录之前)自动运行 Python 脚本

实现效果 使用 Python 编写的一个脚本, 希望在 Windows 系统启动时, 用户登录之前就自动运行. 准备工作 首先确保 Python 脚本可以手动正常运行, 演示起见, 编写下面的一个简单的脚本用于在 C 盘根目录中生成一个包含脚本运行时间戳的文本文件. Python 脚本存放在 C:\Python…

RISC-V (五)上下文切换和协作式多任务

任务&#xff08;task&#xff09; 所谓的任务就是寄存器的当前值。 -smp后面的数字指的是hart的个数&#xff0c;qemu模拟器最大可以有8个核&#xff0c;此文围绕一个核来讲。 QEMU qemu-system-riscv32 QFLAG -nographic -smp 1 -machine virt -bios none 协作式多任务 …

绘唐科技,绘唐3高级推理创作

绘唐科技&#xff0c;绘唐3高级推理创作绘唐3https://qvfbz6lhqnd.feishu.cn/wiki/D3YLwmIzmivZ7BkDij6coVcbn7W 《绘唐3&#xff1a;高级推理创作》 第一章&#xff1a;谋杀案的启示 在古代绘唐王朝的京都&#xff0c;发生了一起离奇的谋杀案。一位名叫李红的官员&#xff0c…

[Day 44] 區塊鏈與人工智能的聯動應用:理論、技術與實踐

生成对抗网络&#xff08;Generative Adversarial Networks&#xff0c;GANs&#xff09;是一种由Ian Goodfellow等人在2014年提出的深度学习模型&#xff0c;广泛用于图像生成、图像超分辨率、图像修复等领域。GAN由一个生成器&#xff08;Generator&#xff09;和一个判别器&…

状态同步帧同步

帧同步&#xff1a; 有明确的逻辑帧概念&#xff0c;按照固定的逻辑帧间隔同步帧数据 原理 锁帧&#xff1a;mmo那种游戏&#xff0c;服务器需要收到第k帧所有客户端的指令&#xff0c;就算没有操作也发个空指令上去&#xff08;相对来说回合制卡牌这类就简单很多&#xff0…

keil编程时,出现”MULTIPLE PUBLIC DEFINITIONS“,extern声明时不能赋值

原因导致重复定义&#xff1f; 有可能是重复定义导致的&#xff0c;这就需要查看报错位置的变量或函数是不是被重复定义了。也可能是一些语法错误导致的错误 语法错误导致的错误原因 使用extern只是为了说明这个变量在别的地方已经定义过&#xff0c;但使用extern引用时&…

NSS [HNCTF 2022 WEEK3]ssssti

NSS [HNCTF 2022 WEEK3]ssssti SSTI类题目&#xff08;flask&#xff09;毋庸置疑。 有过滤&#xff0c;我们拿burp先fuzz一波。长度159的都是被过滤的。 过滤了下划线、引号、args、os。 我们利用request对象绕过对下划线和引号的过滤&#xff08;题目不允许POST方法&#x…

【机器学习】过拟合和欠拟合、高偏差(High Bias)和高方差(High Variance)的区别、过拟合和欠拟合的示例以及图表表示

引言 在机器学习中&#xff0c;过拟合&#xff08;Overfitting&#xff09;是指模型在训练数据上学习得太好&#xff0c;以至于它捕捉到了数据中的噪声和随机波动&#xff0c;而不是潜在的真实关系&#xff0c;这导致模型在新的、未见过的数据上表现不佳&#xff1b;欠拟合&…

从零开始:MySQL安装与配置完全指南

前言 哇&#xff0c;终于进入到令人激动的MySQL环节了 它可以说是你编程生涯中的最佳朋友&#xff0c;因为它总是能存储你的数据&#xff0c;从不说&#xff1a;“我忘记了你的信 息”。而且&#xff0c;它是免费的&#xff0c;不像一些昂贵的数据库&#xff0c;它从不让你的…

部署k8s+conatinerd环境

1、准备系统环境 禁用默认休眠&#xff08;可选&#xff09; 禁用&#xff1a;systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target启用&#xff1a;sudo systemctl unmask sleep.target suspend.target hibernate.target hybrid-sleep.target …

随机对照临床试验(RCT)与历史对照临床试验(HCT)

临床试验是评估新药、新疗法或新技术有效性和安全性的关键手段。在临床试验中&#xff0c;如何选择对照组是影响试验结果准确性和可信度的重要因素。随机对照临床试验&#xff08;Randomized Controlled Trial, RCT&#xff09;和历史对照临床试验&#xff08;Historical Contr…