简单设计之识别重复 - 图1

清扬照着原来袁帅给她展示的简单设计金字塔手绘了一个,对比着自己的点睛总结,发呆起来,神情略带微笑。她最近正在为参加OOBootcamp做一些基础学习,面向对象、自动化测试这两块骨头让她啃得好不吃力。这不,刚接触到一个看起来很“简单”的框架,感觉像是骑上了一匹快马,即将体验“春风得意马蹄疾,一日看尽长安花”的舒畅。

未解的困惑

「通过测试」,她很清楚自己的定位,作为应用软件程序员,软件功能满足业务需求,这当然是不可动摇的信仰。「消除重复」这个平日里也没有觉得有什么特别的点,为何会出现在这里呢?她的第六感告诉自己这里面可能没那么简单,要不然袁帅为啥会特意提前准备了一张卡片:

简单设计之识别重复 - 图2

而且原文中还以俏皮的口吻提到「重复乃万恶之源 — Kent Beck没有说过」,重复真的有这么邪恶吗?空旷的办公室只剩下清扬一个人在灯光下望着窗外,陷入了沉思。

“古来圣贤皆寂寞… 小鬼这么晚还在思考人生呀!”听到声音,清扬回过神来看了眼时间 — 21:05。OOBootcamp一如既往地拖堂了半小时,结束后,袁帅和Jeany朝清扬欢快地走来。

袁帅瞥了一眼清扬手上的卡片,大概猜到了清扬在琢磨什么。他放下电脑坐下,端起杯子给清扬甩了个眼神:“着急回去为明天的约会准备睡眠吗,聊聊?” “约个鬼啊,周末要准备下周小组的技术分享,我想分享简单设计,正好有些疑惑待解答。”

“我一会儿有个有氧训练,先回去了哈,期待清扬的技术分享哦。” Jeany边打招呼边向电梯走去,将其高挑的背影留在了灯光下。

“帅哥,你说简单设计中「消除重复」这条原则,这么简单的点值得一提吗?两段代码长得一样,还有什么好说的,谁见到了都会想着消掉吧!”

“是这样吗?你有没有听过CCCV(CMD + C | CMD + V)程序员?” 袁帅反抛了个问题。“啥,CCTV?哦… 懂了,他们是在添加重复逻辑!” 清扬有点儿一惊一乍,自问自答,然后她也意识到不是每个人都有这种Sense,甚至有人在违背它而不自知。

开始探索

“来,这么晚了,咱们就玩个轻松点的游戏呗。” 袁帅边说边打开电脑接到显示器上。“游戏?”清扬一脸好奇。“对呀,很简单的几个代码小示例,你来为它们提炼出重复的类型?”

实现过程

清扬刚要继续问,袁帅示意她看显示器:“先来看个简单点的哈。”

  1. public String to() {
  2. return "Customer: " +
  3. toCustomerName.getTitle() +
  4. toCustomerName.getFirstName() + " " +
  5. toCustomerName.getLastName() +
  6. System.lineSeparator() +
  7. "Address: " +
  8. toAddress.getCity() + ", " +
  9. toAddress.getProvince() + ", " +
  10. toAddress.getZipCode() +
  11. System.lineSeparator() +
  12. "Tel: " + toTel;
  13. }
  14. public String from() {
  15. return "Customer: " +
  16. fromCustomerName.getTitle() +
  17. fromCustomerName.getFirstName() + " " +
  18. fromCustomerName.getLastName() +
  19. System.lineSeparator() +
  20. "Address: " +
  21. fromAddress.getCity() + ", " +
  22. fromAddress.getProvince() + ", " +
  23. fromAddress.getZipCode() +
  24. System.lineSeparator() +
  25. "Tel: " + fromTel;
  26. }

“这个简单啊,最直观的重复!” 清扬突然兴奋了起来。

“是滴,这个是最直观的「实现过程」重复。”袁帅边说边在卡片上记下了一条。清扬明白他的意思了 — 要给重复代码的案例提炼总结出一种方便记忆的类型。

“两段代码的实现逻辑看起来几乎一模一样,很容易识别,而且现在一些IDE都会自动提示。” 清扬补充了一句。

“对的,不要放过IDE的任何提示。当看到IDE的提示,去看一眼提示信息,很多时候是我们犯了一些小的错误被侦查出来,比如拼写错误。有时候确认有些提示不用理会时,可针对性的ignore掉。” 袁帅说完突然想到有些有代码洁癖的程序员,会致力于IDE零提示。

功能语义

“刚才够简单吧,来,继续第二关。”袁帅清了清嗓子。

public boolean isValidIp(String ipAddress) {
    if (ipAddress.isEmpty()) {
        return false;
    }
    String regex = "^(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[1-9])\\."
        + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
        + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
        + "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)$";
    return ipAddress.matches(regex);
}

public boolean checkIP(String ipAddress) {
    if (ipAddress.isEmpty()) {
        return false;
    }
    List<String> ipUnits = Arrays.asList(ipAddress.split("."));
    if (ipUnits.size() != 4) {
        return false;
    }
    for (int i = 0; i < 4; ++i) {
        int ipUnitIntValue;
        try {
            ipUnitIntValue = Integer.parseInt(ipUnits.get(i));
        } catch (NumberFormatException e) {
            return false;
        }
        if (ipUnitIntValue < 0 || ipUnitIntValue > 255) {
            return false;
        }
        if (i == 0 && ipUnitIntValue == 0) {
            return false;
        }
    }
    return true;
}

“呀,这个好典型,我上个迭代就在代码库中见到过,校验IP地址规范的,我还在Code Review中特意提出来了。当时我给它起了个名字叫「功能语义」重复。”

“厉害呀!”袁帅给清扬竖起了大拇指,让本来有点小得意的她露出了开心的笑容。接着他又侃侃而谈:“这个可能需要你对代码有一定的敏感度,比如看到类似的方法名或类名的时候,通常,针对他们的单元测试是鉴定这种坏味道比较靠谱。像异曲同工的类也属于这种重复。”

语法语义

袁帅使用了快捷键跳转到下一个文件:“来看这个”

public  boolean checkIn(String fingerprint){
        Employee employee = EmployeeRepository.query(fingerprint);
        int type = employee.getType();
        String record;
        switch (type) {
            case Employee.ENGINEER:
                record = "I am an Engineer, My Name is" + employee.getName();
                break;
            case Employee.SALESMAN:
                record = "I am a Salesman, My Name is" + employee.getName();
                break;
            case Employee.MANAGER:
                record = "I am a Manager, My Name is" + employee.getName();
                break;
            default:
                record = "";
        }
        if (checkInRecords.isEmpty()) {
            return false;
        }
        checkInRecords.put(fingerprint, record);

        return true;
    }

    public int payAmount() {
        if(type == Employee.ENGINEER) return monthlySalary;
        if(type == Employee.SALESMAN) return monthlySalary + commission;
        if(type == Employee.MANAGER) monthlySalary + bonus;

        throw new RuntimeException("Invalid employee");
    }

“咦,这个也是重复吗?”清扬定睛看了10几秒,疑惑地自言自语起来。“用多态取代条件表达式…” 袁帅觉得这个对她来说有点难度,给了一点小提示。

“哦,这里出现了重复的条件分支,未来如果需要增加一个分支,就需要改几个地方,这种是叫分支重复吗?”清扬明白了袁帅的提示,试探性做了总结。

“「语法语义」重复,这个有一定的隐蔽性,内容上,他们的条件分类是重复,形式上,不同的语法实际上表达了一个意思。重复的Switch有时候也会通过switch case和if-else体现出来,要多加留心哦。”见清扬所有所思地点头着,袁帅适可而止。

执行逻辑

“是时候出大招了!” 袁帅故作玄虚的展示了一段新代码。

public class AccountService {
    private AccountRepository accountRepository;

    public AccountService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }

    public Account login(String email, String password) {
        if (!AccountValidation.isValidEmail(email)) {
            // Throw EmailInvalidException
        }
        Account account = accountRepository.getByEmail(email);
        if (account == null) {
            // Throw AccountNotExistedException
        }
        if (!account.verifyPassword(password)) {
            // Throw PasswordInvalidException
        }
        return account;
    }
}

public class AccountRepository {
    public Account getByEmail(String email) {
        if (!AccountValidation.isValidEmail(email)) {
            // Throw EmailInvalidException
        }
        // Query db to get user by email
        return null;
    }
}

“这个可能看起来没有任何重复代码,仔细留意代码运行时的执行过程哦。”袁帅见清扬眼睛快要贴到屏幕上了,过了好久还没有发声,便给了一些提示。

“找到了!AccountValidation.isValidEmail(email)执行了两遍。”袁帅刚把水杯凑到嘴边听到清扬大声叫起来,水溅了一脸,清扬见状哈哈大笑起来。

“这个重复执行,就叫「执行逻辑」重复吧!”清扬趁袁帅去抽纸的一会儿总结出一个让他很满意的词,也收获了他赞许的眼光。

“重复执行一些判断逻辑,这种在实际项目中很常见,尤其是有些很喜欢防御式编程的程序员,觉得哪一行代码都是不安全的,即便是自己写,在哪里都要校验。”袁帅做了一个小补充。

配置文件

游戏快接近尾声了,袁帅打开了两个yml文件:

# application.yml

server:
  port: 8000

spring:
  datasource:
    url: jdbc:h2:file:./db/exam_quiz;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;INIT=CREATE SCHEMA IF NOT EXISTS exam_quiz
    platform: h2
    usermane: sa
    password:
    driver-class-name: org.h2.Driver
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    properties:
      hibernate:
        show_sql: true
        use_sql_comments: true
        format_sql: true
    hibernate:
      ddl-auto: validate
  h2:
    console:
      enabled: true
      path: /console
      settings:
        trace: false
        web-allow-others: true

# application-dev.yml

spring:
  datasource:
    url: jdbc:h2:file:./db/exam_quiz;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;INIT=CREATE SCHEMA IF NOT EXISTS exam_quiz
    platform: h2
    usermane: sa
    password:
    driver-class-name: org.h2.Driver
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    properties:
      hibernate:
        show_sql: true
        use_sql_comments: true
        format_sql: true
    hibernate:
      ddl-auto: validate

“这不是配置文件吗?”清扬一眼就认出来了。

“不同环境进行的配置信息里面有一些固定的配置信息在不同环境是一致的,这些信息可以提取到基础的配置文件中,在其他的环境对应的配置文件中,只需要定制修改特定的配置信息。有点类似OO里面通过继承来复用一些逻辑。”清扬之所以一气呵成讲得这么有条理,得益于她上周才将项目的配置文件通过一些校对工具进行了比对,消除了不少重复的配置信息,并在Code Review中给其他小伙伴做了同步。

“「配置信息」重复!”两人异口同声的说出了这个词,随即相视一笑。

邪恶注释

虽然快10点了,两人对代码依然兴致勃勃,见清扬没有要走之意,袁帅追加了一个问题:“注释是不是一种重复呢?”

清扬思考了片刻,喝了口水,略带正义的讲到:“除了必要的注释,我感觉大部分无用的注释其实就是糟糕代码的遮羞布,而且它跟代码可以看成是一种重复,并且还有一些人拿出站不住脚的理由来涂鸦这块布 — 代码写得很复杂,写个注释也是为了让别人容易看懂嘛!”

“嗯,在绝大多数场景下它还很邪恶!为什么说它邪恶呢?因为当代码被改动了,如果忘了修改注释,从此它就变得无比邪恶,就像一个小魔鬼一直对路过的人龇牙咧嘴的做着同样的自我介绍,然而早已释是码非了。” 袁帅说着扮演了一个魔鬼龇牙咧嘴的样子,把清扬给逗乐了。

在卡片上写下了最后一条 — 「邪恶注释」重复。袁帅看了眼手表 — 10:15,他长舒了一口气:“收工!”

跟SRP的瓜葛

袁帅正将电脑装包之际,余光看到清扬快速进入自己的电脑邮箱,隐约看到她打开了博客大赛的文章:简单设计五原则

“帅哥,你看到王大师(王承志,前端大佬)对你博客文章的回复了吗?看起来像是在说SRP跟重复的冲突,我没太看懂!”清扬的语气未显现出一丝疲惫。

“嗯,是不是还想继续讨论一下呀?” “这样子,今天比较晚了,你周末深入了解一下SRP,咱们下周专门找时间讨论一下如何?”没等清扬回答,袁帅先发制人。

“哦,好吧!” 清扬有点意犹未尽。

“送给你,周末愉快!”袁帅把刚才总结的卡片递给她,并给了个眼神示意她收拾。

清扬快速收拾包包后,手里拿着袁帅递给她的卡片,跟袁帅一起朝电梯走去,一路随手关掉了所有亮着的灯。进入电梯后,清扬迫不及待地阅读起来。细心的她发现袁帅还在上面还记录了一些Tips,心里感激自己遇到一个这么好的Buddy。

简单设计之识别重复 - 图3