微前端架构如何改变企业的开发模式与效率提升
659
2022-09-08
《重构》学习(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
重新跑一次测试用例,没问题。 我们通过 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
然后在 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 = "
演出 | 座位 | 花费 |
---|---|---|
${perf.playInfo.name} | (${perf.audience} result += " | ${perf.amount / 100f} |
所需缴费:¥${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小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~