《重构-改善既有代码设计》案例之C#版(5)
书接上文...
之前我把Performance类加了一个amount属性,后来我想了下.这个Performance属于输入类,最好是不要动它,因为一般我们的输入结构是不能够随我们自己的心意随意变动的.
如果我们确实需要给它加入属性的话.我们最好自己定一个中间转换类.所以我还原了这个Performance,另外加了一个PerformanceEnhance类继承原来的Performance类.仅仅为了加这个amount就加了一个类,我又不甘心,所以我又想了很久还有什么其他的属性或方法可以加入这个类.
突然就灵光乍现.如果一个Performance给定了剧目和观众数量的话.那么这个演出的价格和积分实际上就已经确定了.(软考里面不是学过函数依赖嘛,好像数据库范式也有提到字段依赖啥啥的)那么价格和积分可以作为这个PerformanceEnhance的属性
第一步如下

1 public class Performance 2 { 3 public string playID; 4 5 public int audience; 6 } 7 8 public class PerformanceEnhance : Performance 9 { 10 public int Amount { get; set; } 11 12 public int VolumeCredits { get; set; } 13 14 public Play Play; 15 }
第二步呢.就是一个新的非常重要的重构手法了:以多态取代条件表达式
面向对象的编程中其实有非常多的条件表达式可以用多态取代.
在这个案例中呢。我们注意到在计算金额的时候我们根据play.type做了switch处理.在计算积分的时候我们又根据play.type做了if判断.
试想一下,如果哪天又多了一个新的字段需要计算,比如演出需要支付演出税.我们又得再加条件表达式分别处理.
甚至如果哪天剧目的类型又加了一种 童话剧。那我们一改就要改3个地方条件表达式,非常容易改错以及遗漏。所以我们要 以多态取代条件表达式 重构
思路就是 把计算金额和积分的方法,移动到子类中,然后我们只需要在一开始创建对象之前,像工厂模式一样swtich case 创建好对象。后面就自然而然,不同的对象用不同的方法了。再也不要写额外的条件表达式了。有点抽象。还是具体看这个例子吧。
目前有两种类型,comedy和tragedy,所以我们至少需要新建两个子类,那么父类怎么抽出来呢。我又灵光一现,嗯,第一步我们不是引入了一个PerformanceEnhance类嘛,它可不可以作为父类呢。先来试试吧

1 public class PerformanceEnhance : Performance 2 { 3 public PerformanceEnhance(Performance performance, Dictionary<string, Play> _plays) 4 { 5 audience = performance.audience; 6 playID = performance.playID; 7 Play = _plays[performance.playID]; 8 } 9 10 public virtual int Amount { get; set; } 11 12 public virtual int VolumeCredits { get; set; } 13 14 public readonly Play Play; 15 } 16 17 public class ComedyPerformance : PerformanceEnhance 18 { 19 public ComedyPerformance(Performance performance, Dictionary<string, Play> _plays) : base(performance, _plays) 20 { 21 } 22 23 public override int Amount 24 { 25 get 26 { 27 int result = 30000; 28 if (audience > 20) 29 { 30 result += 1000 + 500 * (audience - 20); 31 } 32 33 return result; 34 } 35 } 36 37 public override int VolumeCredits 38 { 39 get 40 { 41 int result = Math.Max(audience - 30, 0); 42 43 result += audience / 5; 44 45 return result; 46 } 47 } 48 } 49 50 public class TragedyPerformance : PerformanceEnhance 51 { 52 public TragedyPerformance(Performance performance, Dictionary<string, Play> _plays) : base(performance, _plays) 53 { 54 } 55 56 public override int Amount 57 { 58 get 59 { 60 int result = 40000; 61 if (audience > 30) 62 { 63 result += 1000 * (audience - 30); 64 } 65 66 return result; 67 } 68 } 69 70 public override int VolumeCredits 71 { 72 get { return Math.Max(audience - 30, 0); } 73 } 74 }
这里步子有点大了。。。这是个不好的习惯,最好还是按照书上的方法一步一步来。
解释一下
我把AmountFor()和VolumeCreditsFor()这两个方法根据其中的条件表达式分别移入了ComedyPerformance和TragedyPerformance的Amount和VolumeCredits属性.这一步应该都能看懂没啥问题
为啥PerformanceEnhance的构造函数要引入performance, _plays两个参数呢?嗨,这不是为了把原本performance里面的playid和audience取出来嘛,_plays 是另外用来给这个play字段赋值么
这里需要展示一下如何创建PerformanceEnhance的子类,并通过performance和_plays,给其赋值

1 private PerformanceEnhance GetEnhance(Performance perf, Dictionary<string, Play> _plays) 2 { 3 PerformanceEnhance performanceEnhance; 4 switch (_plays[perf.playID].type) 5 { 6 case "comedy": 7 performanceEnhance = new ComedyPerformance(perf, _plays); 8 break; 9 case "tragedy": 10 performanceEnhance = new TragedyPerformance(perf, _plays); 11 break; 12 default: 13 throw new Exception($"Unknown Type:{_plays[perf.playID].type}"); 14 } 15 16 return performanceEnhance; 17 }
另外对于这个StatementData类我也有想法,
之前StatementData.Performances 字段用的是new List<Performance>(),我们已经换成new List<PerformanceEnhance>()
还专门建了一个EnrichStatementData方法用来给StatementData充入数据.其实duck不必
我们直接在StatementData的构造函数中干这些事就可以了呀 我们把TotalAmount()和TotalVolumeCredits()方法分别移入StatementData.TotalAmount属性和StatementData.TotalVolumeCredits属性中。然后在构造函数中给Customer和StatementData.Performances赋值

1 public class StatementData 2 { 3 public string Customer; 4 5 public List<PerformanceEnhance> Performances = new List<PerformanceEnhance>(); 6 7 public int TotalAmount 8 { 9 get 10 { 11 int result = 0; 12 foreach (var perf in Performances) 13 { 14 result += perf.Amount; 15 } 16 17 return result; 18 } 19 } 20 21 public int TotalVolumeCredits 22 { 23 get 24 { 25 int result = 0; 26 foreach (var perf in Performances) 27 { 28 result += perf.VolumeCredits; 29 } 30 31 return result; 32 } 33 } 34 35 public StatementData(Invoice invoice, Dictionary<string, Play> _plays) 36 { 37 Customer = invoice.Customer; 38 foreach (var perf in invoice.performances) 39 { 40 Performances.Add(GetEnhance(perf, _plays)); 41 } 42 } 43 44 private PerformanceEnhance GetEnhance(Performance perf, Dictionary<string, Play> _plays) 45 { 46 PerformanceEnhance performanceEnhance; 47 switch (_plays[perf.playID].type) 48 { 49 case "comedy": 50 performanceEnhance = new ComedyPerformance(perf, _plays); 51 break; 52 case "tragedy": 53 performanceEnhance = new TragedyPerformance(perf, _plays); 54 break; 55 default: 56 throw new Exception($"Unknown Type:{_plays[perf.playID].type}"); 57 } 58 59 return performanceEnhance; 60 } 61 }
因此我们创建中转数据的时候也不用那个CreateStatementData()方法和EnrichStatementData()方法了

1 public string Statement() 2 { 3 StatementData statementData = new StatementData(_invoice, _plays); 4 return RenderPlainText(statementData); 5 } 6 7 public string HtmlStatement() 8 { 9 StatementData statementData = new StatementData(_invoice, _plays); 10 return RenderHtml(statementData); 11 }
这样调用多简单。完成
最后编译、启动
OK 完成.这个案例到这里就结束啦。
纯粹记录自己的学习
如果你还看到了这里
谢谢你花了时间读这个狗屁不通的帖子。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现