热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Java改写重构第2版第一个示例

《重构:改善既有代码的设计》是一本经典的软件工程必读书籍。作者马丁·福勒强调重构技术是以微小的步伐修改程序。但是,从国内的情况来而论,“重构”的概念表里分离。大家往往喜欢打着“重构
《重构:改善既有代码的设计》是一本经典的软件工程必读书籍。作者马丁·福勒强调重构技术是以微小的步伐修改程序。 但是,从国内的情况来而论,“重构”的概念表里分离。大家往往喜欢打着“重构”的名号,实际上却干的是“刀劈斧砍”的勾当。产生这种现象的原因,一方面是程序员希望写出可维护,可复用,可拓展,灵活性好的代码,使系统具长期生命力;另一方面,重构的扎实功夫要学起来、做起来,颇不是一件轻松的事,且不说详尽到近乎琐碎的重构手法,光是单元测试一事,怕是已有九成同行无法企及。所以,重构变质为重写,研发团队拿着公司的经费,干着“重复造***”的事儿,最终“重构”后的软件仍然不能使人满意,反倒是一堆问题,用户不愿意买单,程序员不愿意继续维护,管理人员也担着巨大的压力。痛苦的滋味在心底蔓延。

写在前面

《重构:改善既有代码的设计》是一本经典的软件工程必读书籍。作者马丁·福勒强调重构技术是以微小的步伐修改程序

但是,从国内的情况来而论,“重构”的概念表里分离。大家往往喜欢打着“重构”的名号,实际上却干的是“刀劈斧砍”的勾当。产生这种现象的原因,一方面是程序员希望写出可维护,可复用,可拓展,灵活性好的代码,使系统具长期生命力;另一方面,重构的扎实功夫要学起来、做起来,颇不是一件轻松的事,且不说详尽到近乎琐碎的重构手法,光是单元测试一事,怕是已有九成同行无法企及。所以,重构变质为重写,研发团队拿着公司的经费,干着“重复造***”的事儿,最终“重构”后的软件仍然不能使人满意,反倒是一堆问题,用户不愿意买单,程序员不愿意继续维护,管理人员也担着巨大的压力。痛苦的滋味在心底蔓延。

转头来看,Martin Fowler 时隔 20 年后的第 2 版,没有照搬第一版,而是把工夫做得更加扎实了,我有幸发现这本书,解我之惑,实属幸事一件。由于第 2 版中使用 Javascript 作为展现重构手法的语言,可是本人惯用的语言却是 Java,因此本着 “实践出真知” 的原则,我想尝试用 Java 语言来对示例进行改写,在分享思路的同时,也希望能够有人与我讨论,甚至指出我的错误,在此深表感谢。

废话不多说了,我们赶紧开始

项目地址

Gitee 项目地址

git clone https://gitee.com/kendoziyu/code-refactoring-example.git

起点

有些看到文章的小伙伴,可能还没拿到这本《重构2》,所以我先把原文需求贴出来,另外在改写时,我会参考并结合《重构》第 1 版中的代码。

设想有一个戏剧演出团,演员们经常要去各种场合表演戏剧。通常客户(customer)会指定几出剧目,而剧团则根据观众(audience)人数及剧目类型向客户收费。该团目前出演两种戏剧:悲剧(tragedy)和喜剧(comedy)。给客户发出账单时,剧团还根据到场观众的数量给出“观众量积分”(volume credit)优惠,下次客户再请剧团表演时,可以使用积分获得折扣————你可以把它看作一种提升客户忠诚度的方式。

该剧团将 剧目 的数据存储在一个简单的 JSON 文件中。

plays.json...

{
"hamlet":{"name":"Hamlet", "type":"tragedy"},
"as-like":{"name":"As You Like It", "type":"comedy"},
"othello":{"name":"Othello", "type":"tragedy"}
}

他们开出的 账单 也存储在一个 JSON 文件里。

invoices.json...

{
    "customer":"BigCo",
    "performances":[
        {
            "playId":"hamlet",
            "audience":55
        },
        {
            "playId":"as-like",
            "audience":35
        },
        {
            "playId":"othello",
            "audience":40
        }
    ]
}

等下我要来解析这两组 JSON 对象,不妨先来分析一下实体类之间的关系:

发票(Invoice)
  
public class Invoice {
    private String customer;
    private List performances;
    public String getCustomer() {
        return customer;
    }
    public void setCustomer(String customer) {
        this.customer = customer;
    }
    public List getPerformances() {
        return performances;
    }
    public void setPerformances(List performances) {
        this.performances = performances;
    }
}
  

表演(Performance)
  
public class Performance {
    private String playId;
    private int audience;
    public String getPlayId() {
        return playId;
    }
    public void setPlayId(String playId) {
        this.playId = playId;
    }
    public int getAudience() {
        return audience;
    }
    public void setAudience(int audience) {
        this.audience = audience;
    }
}
  

剧目(Play)
  
public class Play {
    private String name;
    private String type;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getType() {
        return type;
    }
    public void setType(String type) {
        this.type = type;
    }
}
  

接着,书中直接就给出了 打印账单信息 的函数 function statement(invoice, plays) {}。注意,《重构2》书中有提到,

当我在代码块上方使用了斜体(中文对应楷体)标记的题头 “function xxx” 时,表明该代码位于题头所在函数、文件或类的作用域内。

所以,结合《重构(第 1 版)》中的 Java 示例,我对第二版的示例做了一些改造:

Statement.java...

public class Statement {

    private Invoice invoice;
    private Map plays;

    public Statement(Invoice invoice, Map plays) {
        this.invoice = invoice;
        this.plays = plays;
    }

    public String show() {
        int totalAmount = 0;
        int volumeCredits = 0;
        String result = String.format("Statement for %s\n", invoice.getCustomer());
        StringBuilder stringBuilder = new StringBuilder(result);

        Locale locale = new Locale("en", "US");
        NumberFormat format = NumberFormat.getCurrencyInstance(locale);

        for (Performance performance : invoice.getPerformances()) {
            Play play = plays.get(performance.getPlayId());
            int thisAmount = 0;
            switch (play.getType()) {
                case "tragedy":
                    thisAmount = 40000;
                    if (performance.getAudience() > 30) {
                        thisAmount += 1000 * (performance.getAudience() - 30);
                    }
                    break;
                case "comedy":
                    thisAmount = 30000;
                    if (performance.getAudience() > 20) {
                        thisAmount += 10000 + 500 *(performance.getAudience() - 20);
                    }
                    thisAmount += 300 * performance.getAudience();
                    break;
                default:
                    throw new RuntimeException("unknown type:" + play.getType());
            }

            volumeCredits += Math.max(performance.getAudience() - 30, 0);

            if ("comedy".equals(play.getType())) {
                volumeCredits += Math.floor(performance.getAudience() / 5);
            }

            stringBuilder.append(String.format(" %s: %s (%d seats)\n", play.getName(), format.format(thisAmount/100), performance.getAudience()));
            totalAmount += thisAmount;
        }
        stringBuilder.append(String.format("Amount owed is %s\n", format.format(totalAmount/100)));
        stringBuilder.append(String.format("You earned %s credits\n", volumeCredits));
        return stringBuilder.toString();
    }
}

值得一提的有:

  1. 从 Java 1.7 开始,switch 开始支持字符串了

  2. NumberFormat.getCurrencyInstance 这个 API,可以为我们打印货币信息

Main.java...

public class Main {

    static final String plays = "{" +
            "\"hamlet\":{\"name\":\"Hamlet\",\"type\":\"tragedy\"}," +
            "\"as-like\":{\"name\":\"As You Like It\",\"type\":\"comedy\"}," +
            "\"othello\":{\"name\":\"Othello\",\"type\":\"tragedy\"}" +
            "}";

    static final String invoices = "[{" +
            "\"customer\":\"BigCo\",\"performances\":[" +
            "{\"playId\":\"hamlet\",\"audience\":55}" +
            "{\"playId\":\"as-like\",\"audience\":35}" +
            "{\"playId\":\"othello\",\"audience\":40}" +
            "]" +
            "}]";
    public static void main(String[] args) {
        TypeReference> typeReference = new TypeReference>(){};
        Map playMap = JSONObject.parseObject(plays, typeReference);
        List invoiceList = JSONObject.parseArray(invoices, Invoice.class);
        for (Invoice invoice : invoiceList) {
            Statement statement = new Statement(invoice, playMap);
            String result = statement.show();
            System.out.println(result);
        }
    }
}

运行上面的 Main 主类,会得到如下输出:

Statement for BigCo
 Hamlet: $650.00 (55 seats)
 As You Like It: $580.00 (35 seats)
 Othello: $500.00 (40 seats)
Amount owed is $1,730.00
You earned 47 credits

新需求

在这个例子里,我们的用户希望对系统做几个修改。首先,他们希望以 HTML 格式输出详单。另外,他们还希望增加表演(Play)的类型,虽然还没决定增加哪种以及何时试演。这对戏剧场次的计费方式、积分方式都有影响。在这样的需求前提下,如果你不想以后面对一堆莫名奇妙的 BUG,被逼着各种加班,那我们现在就要着手重构上面的示例了。

如果你要给程序增加一个特性,但是发现代码因缺乏良好的结构而不易于进行更改,那就先重构哪个程序,使其比较容易添加该特性,然后再添加该特性。

重构第一步

重构前,先检查自己是否有一套可靠的测试集。这些测试必须有自我检验能力。

所以,我把 Main.java 稍微改变了一下,设计成了一个简单的测试:

点击查看 StatementTest.java

- 基于 Junit 的单元测试

  
public class StatementTest {
    @Test
    public void test() {
        String expected = "Statement for BigCo\n" +
                " Hamlet: $650.00 (55 seats)\n" +
                " As You Like It: $580.00 (35 seats)\n" +
                " Othello: $500.00 (40 seats)\n" +
                "Amount owed is $1,730.00\n" +
                "You earned 47 credits\n";
        final String plays = "{" +
                "\"hamlet\":{\"name\":\"Hamlet\",\"type\":\"tragedy\"}," +
                "\"as-like\":{\"name\":\"As You Like It\",\"type\":\"comedy\"}," +
                "\"othello\":{\"name\":\"Othello\",\"type\":\"tragedy\"}" +
                "}";
        final String invoices = "{" +
                "\"customer\":\"BigCo\",\"performances\":[" +
                "{\"playId\":\"hamlet\",\"audience\":55}" +
                "{\"playId\":\"as-like\",\"audience\":35}" +
                "{\"playId\":\"othello\",\"audience\":40}" +
                "]" +
                "}";
        TypeReference> typeReference = new TypeReference>(){};
        Map playMap = JSONObject.parseObject(plays, typeReference);
        Invoice invoice = JSONObject.parseObject(invoices, Invoice.class);
        Statement statement = new Statement(invoice, playMap);
        String result = statement.show();
        Assert.assertEquals(expected, result);
    }
}
  ,>

接下来的可以照着书上的要求执行,以微小的步伐开始你的重构之旅了,如果有不明白的也可以参考一下我的例子 code-refactoring-example

拆分计算阶段和格式化阶段

我们希望同样的计算函数可以被 文本版 详单和 HTML版 详单共用。
实现复用有许多种方法,而我最喜欢的技术是 拆分阶段。这里我们的目标是将逻辑分成两部分:一部分计算详单所需的数据,另一部分将数据渲染成文本或者HTML。第一阶段会创建一个中转数据结构,再它传递给第二阶段。

我们可以创建一个 StatementData 作为两个阶段间传递的中间数据结构。建议大家根据书上的讲解实际操练,这里仅仅提供一种思路,我的实操过程已经放在了 Gitee 上面,有兴趣的可以参考和修改。

我们这里拆分函数时有一个目标:让 renderPlainText 只操作通过 data 传递进来的数据(data 就是 StatementData 的实例对象),经过一系列搬移函数之后,我们可以达成这个目标:

    /**
     * 使用纯文本渲染
     * @param data 详单数据
     * @return
     */
    private String renderPlainText(StatementData data) {
        String result = String.format("Statement for %s\n", data.getCustomer());
        StringBuilder stringBuilder = new StringBuilder(result);

        for (Performance performance : data.getPerformances()) {
            stringBuilder.append(String.format(" %s: %s (%d seats)\n", performance.getPlay().getName(), usd(performance.getAmount()), performance.getAudience()));
        }
        stringBuilder.append(String.format("Amount owed is %s\n", usd(data.getTotalAmount())));
        stringBuilder.append(String.format("You earned %s credits\n", data.getTotalVolumeCredits()));
        return stringBuilder.toString();
    }

按计算过程重组计算过程

接下来我们将注意力集中到下一个特性改动:支持更多类型的戏剧,以及支持他们各自的价格计算和观众量积分计算。而改动的核心在 enrichPerformance 函数就是关键所在,因为正是它用每场演出的数据来填充中转数据结构。目前它直接调用了计算价格函数 amountFor,和计算观众量积分函数 volumeCreditsFor 。我们需要创建一个类,通过这个类来调用这些函数。由于这个类存放了与每场演出相关数据的计算函数,于是我们把它称为演出计算器 PerformanceCalculator

我们把 amountFor, volumeCredits 都搬到了 PerformanceCalculator 中。play 字段严格来说,是不需要搬移的,因为它并未体现出多态性。但是这样可以把所有数据转换集中到一处地方,保证了代码的一致性和清晰度。改动后如下:

private Performance enrichPerformance(Performance performance) {
      PerformanceCalculator calculator = new PerformanceCalculator(performance, playFor(performance));
      performance.setPlay(calculator.play());
      performance.setAmount(calculator.amount());
      performance.setVolumeCredits(calculator.volumeCredits());
      return performance;
}

以工厂函数取代构造函数

private Performance enrichPerformance(Performance performance) {
      PerformanceCalculator calculator = createPerformanceCalculator(performance, playFor(performance));
      ...(同上)
      return performance;
}

private PerformanceCalculator createPerformanceCalculator(Performance performance, Play play) {
      return new PerformanceCalculator(performance, play);
}

以子类取代类型码,新建 ComedyCalculator 和 TragedyCalculator 并且让他们继承 PerformanceCalculator

private PerformanceCalculator createPerformanceCalculator(Performance performance, Play play) {
      switch (play.getType()) {
            case "tragedy": return new TragedyCalculator(performance, play);
            case "comedy": return new ComedyCalculator(performance, play);
            default:
                throw new RuntimeException("unknown type:" + play.getType());
      }
}

以多态取代条件表达式

public class ComedyCalculator extends PerformanceCalculator {

    public ComedyCalculator(Performance performance, Play play) {
        super(performance, play);
    }

    @Override
    public int amount() {
        int result = 30000;
        if (performance.getAudience() > 20) {
            result += 10000 + 500 *(performance.getAudience() - 20);
        }
        result += 300 * performance.getAudience();
        return result;
    }

    @Override
    public int volumeCredits() {
        return (int) (super.volumeCredits() + Math.floor(performance.getAudience() / 5));
    }
}
public class TragedyCalculator extends PerformanceCalculator {

    public TragedyCalculator(Performance performance, Play play) {
        super(performance, play);
    }

    @Override
    public int amount() {
        int result = 40000;
        if (performance.getAudience() > 30) {
            result += 1000 * (performance.getAudience() - 30);
        }
        return result;
    }
}

总结

以一张图总结本文内容:

  1. 例中我们用到了数种重构手法。包括提炼函数内联变量搬移函数以多态取代条件表达式等。
  2. 我们用 拆分阶段 的技术分离计算逻辑与输出格式化的逻辑。

好代码的检验标准就是人们能否轻而易举地修改它!

与君共勉

编程时,需要遵循营地法则:希望我们都可以“保证你离开时的代码库一定比你来时更健康”。


推荐阅读
  • 在Effective Java第三版中,建议在方法返回类型中优先考虑使用Collection而非Stream,以提高代码的灵活性和兼容性。 ... [详细]
  • 本文将深入探讨 Unreal Engine 4 (UE4) 中的距离场技术,包括其原理、实现细节以及在渲染中的应用。距离场技术在现代游戏引擎中用于提高光照和阴影的效果,尤其是在处理复杂几何形状时。文章将结合具体代码示例,帮助读者更好地理解和应用这一技术。 ... [详细]
  • 本文详细介绍如何在华为鲲鹏平台上构建和使用适配ARM架构的Redis Docker镜像,解决常见错误并提供优化建议。 ... [详细]
  • Spring Boot使用AJAX从数据库读取数据异步刷新前端表格
      近期项目需要是实现一个通过筛选选取所需数据刷新表格的功能,因为表格只占页面的一小部分,不希望整个也页面都随之刷新,所以首先想到了使用AJAX来实现。  以下介绍解决方法(请忽视 ... [详细]
  • 在Ubuntu 18.04上使用Nginx搭建RTMP流媒体服务器
    本文详细介绍了如何在Ubuntu 18.04上使用Nginx和nginx-rtmp-module模块搭建RTMP流媒体服务器,包括环境搭建、配置文件修改和推流拉流操作。适用于需要搭建流媒体服务器的技术人员。 ... [详细]
  • 尽管Medium是一个优秀的发布平台,但在其之外拥有自己的博客仍然非常重要。这不仅提供了另一个与读者互动的渠道,还能确保您的内容安全。本文将介绍如何使用Bash脚本将Medium文章迁移到个人博客。 ... [详细]
  • 本文介绍了如何在不同操作系统上安装Git,以及一些基本和高级的Git操作,包括项目初始化、文件状态检查、版本控制、分支管理、标签处理、版本回退等,并简要提及了开源许可协议的选择。 ... [详细]
  • 本文介绍了一个使用Slideview组件实现循环轮播效果的例子,并将其作为ListView顶部的一项。此ListView包含了两种不同的模板设计,一种以Slideview为核心,另一种则是标准的单元格模板,包含按钮和标签。 ... [详细]
  • 原文地址:https:blog.csdn.netqq_35361471articledetails84715491原文地址:https:blog.cs ... [详细]
  • 本文详细介绍了 Java 中 org.w3c.dom.Node 类的 isEqualNode() 方法的功能、参数及返回值,并通过多个实际代码示例来展示其具体应用。此方法用于检测两个节点是否相等,而不仅仅是判断它们是否为同一个对象。 ... [详细]
  • JavaScript 实现图片文件转Base64编码的方法
    本文详细介绍了如何使用JavaScript将用户通过文件输入控件选择的图片文件转换为Base64编码字符串,适用于Web前端开发中图片上传前的预处理。 ... [详细]
  • Python Requests模块中的身份验证机制
    随着Web服务的发展,身份验证成为了确保数据安全的重要环节。本文将详细介绍如何利用Python的Requests库实现不同类型的HTTP身份验证,包括基本身份验证、摘要式身份验证以及OAuth 1认证等。 ... [详细]
  • RTThread线程间通信
    线程中通信在裸机编程中,经常会使用全局变量进行功能间的通信,如某些功能可能由于一些操作而改变全局变量的值,另一个功能对此全局变量进行读取& ... [详细]
  • 本文详细记录了 MIT 6.824 课程中 MapReduce 实验的开发过程,包括环境搭建、实验步骤和具体实现方法。 ... [详细]
  • android开发分享荐                                                         Android思维导图布局:效果展示及使用方法
    思维导图布局的前身是树形布局,对树形布局基本使用还不太了解的朋友可以先看看我写的树形布局系列教程,了解了树形布局的使用方法后再来阅读本文章。先睹为快来看看效果吧,横向效果如下:纵向 ... [详细]
author-avatar
和寧世杰471
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有