《重构》学习拆分逻辑与多态使用
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,但是我们还要调用像 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<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 中新增分支逻辑即可。但是这样做的问题是:
很容易随代码的堆积而腐坏,容易埋下坑
要为程序引入结构、显示地表达出“计算逻辑的差异是由类型代码确定”,这里可以引入多态。这里需要建立一个继承体系:
- 它有戏剧、悲剧两个子类
- 子类包含独立的计算逻辑,包括 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 以上是关于《重构》学习拆分逻辑与多态使用的主要内容,如果未能解决你的问题,请参考以下文章