《重构》学习(2)拆分逻辑与多态使用

系列文章目录

1. 《重构》学习(1)拆分 statement 函数
2. 《重构》学习(2)拆分逻辑与多态使用


1.5 拆分计算阶段与格式化阶段

在上一篇中,我们将 getStatement() 函数进行了重构,这样我们就可以新增功能了。
比如, getStatement() 是返回一个 String 字符串的, 客户希望我们返回的是一个 Json 文件或者 HTML格式的详情账单。

因为我们的计算代码已经被分离出来了,所以我可以只需要为顶层的7行函数新增一个 HTML 的版本。 但问题是,就算之前的函数组织的多么良好,我总不能每次都要去复制粘贴到另外一个新函数中。我希望同样的计算函数可以被 Json、String、HTML 版本的账单打印函数共用。 也就是复用计算逻辑。

要实现复用有许多种方法,这里使用的技术是拆分每个阶段。 这里把目标是将逻辑分成两个部分:一个部分计算详单所需的数据,另一部分将数据渲染成文本或HTML。 第一阶段会创建一个中转数据结构,再把它传给第二阶段。

在该例中,需要将 getStatement() 拆成两个部分, 新增一个 renderPlainText() 函数用于第二阶段生成存文本的账单,这里的思路主要为生成中间数据,大概样子如下面所示:

    fun getStatement(): String {
        val statementData = StatementData().apply {
            customer = invoice.customer
            performances = invoice.performances.toMutableList()
            plays = playInfos.toMutableMap()
        }
        return renderPlainText(statementData)
    }

    /**
     * 生成纯文本的方法
     */
    private fun renderPlainText(data: StatementData): String {
        var result = "这里您的账单: ${data.customer} \n"
        for (perf in data.performances) {
            result += " ${data.playFor(perf).name}: ${data.amountFor(perf) / 100f} (${perf.audience} 观众) \n "
        }

        result += "所需缴费:¥${data.totalAmount() / 100f}\n"
        result += "你获取了 ${data.totalVolumeCredits()} 信用分 \n"
        return result
    }

这里我们确实做到了让 renderPlainText() 只处理中间数据的 data,但是我们还要调用像 playForamountFortotalAmount() 这些计算函数。
我们拆分的目的,就是不想让 renderPlainText() 来做这些事情, 所以我们需要把这些计算函数,提前的更早,所以我们需要在getStatement()

这里我继续进行细化,思路为:

  1. 拓展 PerformanceData,因为其目前只有观众数量和 id,如果使其能够关联 PlayInfo ,那么它就能获取 type这些信息了
  2. 拓展了 PerformanceData 后,我们就能更加轻松的为每一场表演,计算 出场费(amount) 和 信用分(volumeCredits),从而也能通过累加获得所有表演的 出场费总额 和 总的信用分
  3. 将 2 里面的信息塞入到 StatementData 中, 再让 renderPlainText() 来处理这个 Statement,这样它不用关心计算逻辑,而只用拿取想要的参数,并拼接成文本输出即可

根据这个思路,我的重构代码如下:

data class PerformancesData(
    var playId: String = "",
    var audience: Int = 0,
    var playInfo: PlayInfo = PlayInfo("", ""), //该表演的信息
    var amount: Int = 0,  // 一场表演所需要的费用
    var volumeCredits: Int = 0 //一场表演产生的信用点
)

/**
 * 用来计算一次账单的工具
 */
class Statement(private val invoice: Invoices, private val plays: MutableMap<String, PlayInfo>) {

    data class StatementData(
        var customer: String = "", // 客户名称
        var performances: List<PerformancesData> = emptyList(),  // 所有表演信息
        var totalAmount: Int = 0, // 客户总共需要缴费
        var totalVolumeCredits: Int = 0 // 客户总共获得的信用分
    )

    fun getStatement(): String {
        val statementData = StatementData().apply {
            customer = invoice.customer
            performances = invoice.performances.toMutableList().map { aPerf -> enrichPerformance(aPerf) }
            totalAmount = totalAmount(this)
            totalVolumeCredits = totalVolumeCredits(this)
        }
        return renderPlainText(statementData)
    }

    /**
     * 生成纯文本的方法
     */
    private fun renderPlainText(data: StatementData): String {
        var result = "这里您的账单: ${data.customer} \n"
        for (perf in data.performances) {
            result += " ${perf.playInfo.name}: ${perf.amount / 100f} (${perf.audience} 观众) \n "
        }

        result += "所需缴费:¥${data.totalAmount / 100f}\n"
        result += "你获取了 ${data.totalVolumeCredits} 信用分 \n"
        return result
    }

    /**
     * 填充 单个 Performance其他数据, 用于计算逻辑
     */
    private fun enrichPerformance(aPerf: PerformancesData): PerformancesData {
        return PerformancesData().apply {
            // 原有数据
            audience = aPerf.audience
            playId = aPerf.playId
            // 新增计算数据
            amount = amountFor(aPerf)
            playInfo = playFor(aPerf)
            volumeCredits = volumeCreditsFor(aPerf)
        }
    }

    private fun amountFor(aPerf: PerformancesData): Int {
        var result: Int
        when (playFor(aPerf).type) {
            "tragedy" -> {
                result = 40000
                if (aPerf.audience > 30) {
                    result += 1000 * (aPerf.audience - 30)
                }
            }
            "comedy" -> {
                result = 30000
                if (aPerf.audience > 20) {
                    result += 10000 + 500 * (aPerf.audience - 20)
                }
                result += 300 * aPerf.audience
            }
            else -> throw Exception("unknown type:${playFor(aPerf).type}")
        }
        return result
    }

    private fun totalAmount(statementData: StatementData): Int {
        var result = 0
        for (perf in statementData.performances) {
            result += amountFor(perf)
        }
        return result
    }

    private fun totalVolumeCredits(statementData: StatementData): Int {
        var result = 0
        for (perf in statementData.performances) {
            result += volumeCreditsFor(perf)
        }
        return result
    }

    private fun volumeCreditsFor(perf: PerformancesData): Int {
        var result = 0
        result += (perf.audience - 30).coerceAtLeast(0)
        if (playFor(perf).type == "comedy") result += floor(perf.audience / 5f).toInt()
        return result
    }

    private fun playFor(perf: PerformancesData): PlayInfo =
        plays[perf.playId] ?: PlayInfo("", "")
}

重新跑一次测试用例,没问题。
我们通过 enrichPerformance 来为每个 Performance 计算了单次的表演信息,再改造了下之前的 totalAmount()totalVolumeCredits() 函数,这样 renderPlainText() 已经看不到任何的计算函数了,这样才算真正的分离了计算逻辑与渲染逻辑。

最后用 管道(责任链模式)来代替循环,这里使用 fold 操作符:

    private fun totalAmount(statementData: StatementData): Int {
        return statementData.performances.fold(0) { totalAmount, performancesData ->
            totalAmount + performancesData.amount
        }
    }

    private fun totalVolumeCredits(statementData: StatementData): Int {
        return statementData.performances.fold(0) { totalVolumeCredits, performancesData ->
            totalVolumeCredits + performancesData.volumeCredits
        }
    }

我们现在看到 Statement 这个类已经有点长了, 因为它里面包含了计算逻辑,计算逻辑有很多个单元,但最终都只是为产生一个 StatementData 而服务,所以我们可以将生成 Statement 的逻辑放在另一个文件(类)里面去进行。

所以我这里新建另一个文件(类),这个类的作用: 通过输入 Invoice 和 PlayInfos,可以输出一个 StatementData,然后 getStatement() 函数通过调用这个获取到中间数据后, 给 renderPlainText 使用:

/**
 * 计算 StatementData
 */
class StatementAdapter(private val invoice: Invoices, private val plays: MutableMap<String, PlayInfo>) {
    fun createStatementData(): StatementData {
        return StatementData().apply {
            customer = invoice.customer
            performances = invoice.performances.toMutableList().map { aPerf -> enrichPerformance(aPerf) }
            totalAmount = totalAmount(this)
            totalVolumeCredits = totalVolumeCredits(this)
        }
    }
    ...
}

然后在 Statement 里面调用:

    fun getStatement(): String {
        val adapter = StatementAdapter(invoice, plays)
        return renderPlainText(adapter.createStatementData())
    }

这样我们就可以在 Statement 中加入 HTML 的账单获取方法了:

    fun getHtmlStatement(): String {
        val adapter = StatementAdapter(invoice, plays)
        return renderHtml(adapter.createStatementData())
    }

    /**
     * 生成 HTML文本的方法
     */
    private fun renderHtml(data: StatementData): String {
        var result = "<h1>这里您的账单: ${data.customer}</h1>\n"
        result += "<table>\n"
        result += "<tr><th>演出</th><th>座位</th><th>花费</th></tr>"
        for (perf in data.performances) {
            result += "<tr><td>${perf.playInfo.name}</td><td>(${perf.audience} 观众)</td>"
            result += "<td>${perf.amount / 100f}</td></tr>\n?"
        }
        result +="</table>\n"
        result += "<p>所需缴费:<em>¥${data.totalAmount / 100f}</e></p>\n"
        result += "<p>你获取了 <em>${data.totalVolumeCredits}</em> 信用分</p> \n"
        return result
    }

1.6 按类型重组计算过程

接下来我们将进行下一个改动:增加更多类型的戏剧,并且支持他们各自的演出费计算和观众量积分计算。
对于现在的结构,可以在 amountFor() 的 when 中新增分支逻辑即可。但是这样做的问题是:
很容易随代码的堆积而腐坏,容易埋下坑

要为程序引入结构、显示地表达出“计算逻辑的差异是由类型代码确定”,这里可以引入多态。这里需要建立一个继承体系:

  1. 它有戏剧、悲剧两个子类
  2. 子类包含独立的计算逻辑,包括 amount 、 volumeCredits的计算,调用者调用后,编程语言帮你分发到不同的子类去计算
  3. 用多态来取代条件表达式,将多个同样的类型码用多态来取代

1.6.1 创建演出计算器类

enrichPerformance() 函数是关键所在,因为它就是用于计算每场演出的数据,来填充中间数据的,所以我们需要创建一个类,
专门存放计算相关的函数, 于是将其称为 演出计算器, PerformanceCalculator

    private fun enrichPerformance(aPerf: PerformancesData): PerformancesData {
        return PerformancesData().apply {
            // 原有数据
            audience = aPerf.audience
            playId = aPerf.playId
            // 新增计算数据
            playInfo = playFor(aPerf)
            amount = amountFor(aPerf)
            volumeCredits = volumeCreditsFor(aPerf)
        }
    }
    ...
class PerformanceCalculator(aPerf: PerformancesData) {
}

然后开始把 enrichPerformance 的东西搬过来,首先最容易的就是 playInfo 了,但是由于它没有体现多态性,所以将以赋值的方式,直接将该值通过构造函数传入:

class PerformanceCalculator(val aPerf: PerformancesData,val playInfo: PlayInfo)
    ...
    private fun enrichPerformance(aPerf: PerformancesData): PerformancesData {
        return PerformancesData().apply {
            ...
            // 新增计算数据
            val calculator = PerformanceCalculator(aPerf, playFor(aPerf))
            playInfo = calculator.playInfo
            ...
        }
    }

1.6.2 将函数搬进计算其

接下来需要搬移 amount 逻辑,然后修改一下传参名、以及内部的变量名:

class PerformanceCalculator(val aPerf: PerformancesData, val playInfo: PlayInfo) {

    fun amount(): Int {
        var result: Int
        when (playInfo.type) {
            "tragedy" -> {
                result = 40000
                if (aPerf.audience > 30) {
                    result += 1000 * (aPerf.audience - 30)
                }
            }
            "comedy" -> {
                result = 30000
                if (aPerf.audience > 20) {
                    result += 10000 + 500 * (aPerf.audience - 20)
                }
                result += 300 * aPerf.audience
            }
            else -> throw Exception("unknown type:${playInfo.type}")
        }
        return result
    }
}

// 调用:
    private fun enrichPerformance(aPerf: PerformancesData): PerformancesData {
        return PerformancesData().apply {
            ...
            amount = calculator.amount()
        }
    }

改完后,我需要对原有的函数改造成一个委托函数,让它直接调用新函数:

    private fun amountFor(aPerf: PerformancesData): Int {
        return PerformanceCalculator(aPerf, playFor(aPerf)).amount()
    }

同样的,我们把 volumeCreditsFor() 函数也搬过去:

  ... 
  volumeCredits = calculator.volumeCredits()
  ..

1.6.3 使演出计算器表现出多态

我们已经将全部计算逻辑搬到一个类中,这个时候就需要将其多态化了。
第一步,应用以子类取代类型码引入子类,弃用类型码。 为此,我们需要为演出计算创建子类,并在 createStatementData 里获取对应的子类。 这里可以使用工厂模式来创建:

    private fun enrichPerformance(aPerf: PerformancesData): PerformancesData {
        return PerformancesData().apply {
            ...
            val calculator = createPerformanceCalculator(aPerf, playFor(aPerf))
            ...
        }
    }
    
    private fun createPerformanceCalculator(aPerf: PerformancesData, play: PlayInfo): PerformanceCalculator {
        return when (play.type) {
            "tragedy" -> TragedyCalculator(aPerf, play)
            "comedy" -> ComedyCalculator(aPerf, play)
            else -> throw  java.lang.Exception("unknown type:${play.type}")
        }
    }
...
/**
 * 戏剧计算器
 */
class ComedyCalculator(private val perf: PerformancesData, playInfo: PlayInfo) :
    PerformanceCalculator(perf, playInfo) {
}

/**
 * 悲剧计算器
 */
class TragedyCalculator(private val perf: PerformancesData, playInfo: PlayInfo) :
    PerformanceCalculator(perf, playInfo) {
}

接下来就可以开始搬移计算逻辑了

abstract class PerformanceCalculator(private val aPerf: PerformancesData, val playInfo: PlayInfo) {

    abstract fun amount(): Int

    open fun volumeCredits(): Int {
        var result = 0
        result += (aPerf.audience - 30).coerceAtLeast(0)
        return result
    }
}

/**
 * 戏剧计算器
 */
class ComedyCalculator(private val perf: PerformancesData, playInfo: PlayInfo) :
    PerformanceCalculator(perf, playInfo) {

    override fun amount(): Int {
        var result: Int = 30000
        if (perf.audience > 20) {
            result += 10000 + 500 * (perf.audience - 20)
        }
        result += 300 * perf.audience
        return result
    }

    override fun volumeCredits(): Int {
        return super.volumeCredits() + floor(perf.audience / 5f).toInt()
    }
}

/**
 * 悲剧计算器
 */
class TragedyCalculator(private val perf: PerformancesData, playInfo: PlayInfo) :
    PerformanceCalculator(perf, playInfo) {
    override fun amount(): Int {
        var result: Int = 40000
        if (perf.audience > 30) {
            result += 1000 * (perf.audience - 30)
        }
        return result
    }
}

到此为止,我们可以新增任意类型的戏剧,只需要写出Calculator,并实现一些计算公式即可。

1.7 小结

和一开始的代码相比,代码量增加了很多,但是结构却完全不一样。 新结构带来的好处就是不同戏剧种类的计算各自集中到了一个地方,如果大多数修改都设计特定类型的计算,像这样按类型进行分离就很有意义,对拓展良好。

到这里第一章已经学完了,画个图小结下:
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值