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

Posted RikkaTheWorld

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《重构》学习拆分逻辑与多态使用相关的知识,希望对你有一定的参考价值。

系列文章目录

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 以上是关于《重构》学习拆分逻辑与多态使用的主要内容,如果未能解决你的问题,请参考以下文章

《重构》学习概述

《重构》学习常用的重构手法 下

一次简单易懂的多态重构实践,让你理解条件逻辑

《重构》学习拆分 statement 函数

第1章 重构,第一个案例:运用多态取代switch

代码重构规范