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

网友投稿 659 2022-09-08

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

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

系列文章目录

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

目录

​​系列文章目录​​​​1.5 拆分计算阶段与格式化阶段​​​​1.6 按类型重组计算过程​​

​​1.6.1 创建演出计算器类​​​​1.6.2 将函数搬进计算其​​​​1.6.3 使演出计算器表现出多态​​​​1.7 小结​​

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} for (perf in data.performances) { result += " ${data.playFor(perf).name}: ${data.amountFor(perf) / 100f} (${perf.audience} } result += "所需缴费:¥${data.totalAmount() / 100f}\n" result += "你获取了 ${data.totalVolumeCredits()} return result }

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

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

拓展​​PerformanceData​​​,因为其目前只有观众数量和 id,如果使其能够关联​​PlayInfo​​ ,那么它就能获取 type这些信息了拓展了​​PerformanceData​​ 后,我们就能更加轻松的为每一场表演,计算 出场费(amount) 和 信用分(volumeCredits),从而也能通过累加获得所有表演的 出场费总额 和 总的信用分将 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) { data class StatementData( var customer: String = "", // 客户名称 var performances: List = 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} for (perf in data.performances) { result += " ${perf.playInfo.name}: ${perf.amount / 100f} (${perf.audience} } result += "所需缴费:¥${data.totalAmount / 100f}\n" result += "你获取了 ${data.totalVolumeCredits} 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) { 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 = "

这里您的账单: ${data.customer}

\n" result += "\n" result += "" for (perf in data.performances) { result += "\n?" } result +="
演出座位花费
${perf.playInfo.name}(${perf.audience} result += "${perf.amount / 100f}
\n" result += "

所需缴费:¥${data.totalAmount / 100f}

\n" result += "

你获取了 ${data.totalVolumeCredits} 信用分

\n" return result }

1.6 按类型重组计算过程

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

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

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

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 小结

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

到这里第一章已经学完了,画个图小结下:

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Python LeNet网络详解及pytorch实现
下一篇:Kotlin协程巩固
相关文章

 发表评论

暂时没有评论,来抢沙发吧~