用反射解决水果篮问题 [Design, C#]
用反射解决水果篮问题 [Design, C#]
Solve The FruitBasket Problem With Reflection [Design, C#]
Written by Allen Lee
When will my reflection show who I am inside?—— Christina Aguielra,《Reflection》
1. 问题的引入
《OOD 启思录》[1]一书提到了一个有趣的“水果篮问题”[2],我把这个问题概要描述如下:
考虑这样一个水果篮,里面可以装有任意多的苹果(Apple)、桔子(Orange)和香蕉(Banana),而这些都派生自一个叫做水果(Fruit)的基类,并重写水果的 Print 和 Cost 两个方法以实现多态性。
有一天,你希望这些派生类更具个性,即各自具有一个别的派生类没有的特殊行为,例如苹果可以去核、桔子可以切片、香蕉可以剥皮。
并且,你希望迭代整个水果篮,让这些派生类执行它们特有的“个性行为”,即让苹果去核、让桔子切片、让香蕉剥皮,但你知道你已经不能再从多态性上获得更多的便利了,怎么办?
为了再次能从多态性上获得便利,你决定在水果类中加入一个叫 Prepare 的纯虚多态函数,并且让这些派生类把它们的“个性行为”实现到这个函数中。这样,当你使用多态性调用 Prepare 时,如果实际的类型是苹果,那么就去核;如果是桔子就切片;如果是香蕉就剥皮。
不过这个方法有个很大的弊端,就是假如你只希望迭代水果篮,让里面的部分派生类执行它们的“个性行为”,例如只需要水果篮里面的苹果去核,别的原封不动,那么这个方法就帮倒忙了!
对于这个问题,Arthur 在《OOD 启思录》中提到了两个流行的解决方案:“肥接口”方案和“记账”方案。
“肥接口”方案
该方案是作者认为目前最流行,也是很多设计者认为最好的方案。它让水果为苹果定义一个名叫 Core 的空虚方法,并让苹果重写该方法的实现,而其他的派生类就直接继承水果的空方法,然后用同样的方法来处理其他的派生类。这样,当你迭代整个水果篮,并且发送“去核”的消息时,苹果就会知道如何做,而其它派生类由于都直接继承水果类的 Core 这个空方法,就会呆在那里什么也不做。随后 Arthur 举了一个该方法不适用的情景,假如某个人增加了一个樱桃类,该类也需要去核,但用户只想迭代水果篮让苹果去核,那么这种方案也将帮倒忙。再者,如果派生类很多,水果类就要为这些派生类定义很多空方法,这样一来不好看,而来也会造成日后维护的负担。
“记账”方案
由于 Arthur 并没有提到该方案的名字,为了便于描述,我就为它起了这样一个名字。该方案提出水果篮除了维护一个水果列表,还应该为各个派生类维护一个单独的列表(当然,列表里面所存放的是指向实际对象的指针),这样就可以在无需改变水果层次结构的前提下根据用户的需要执行派生类的“个性行为”。然而,当派生类比较多的时候,簿记工作可能是一场噩梦,而且运行时的类型处理上也存在许多隐藏的问题。再者,如果某人增加了一个派生类,例如西瓜,那么你也将有可能需要为此向水果篮添加额外的代码。
Arthur 并没有在《OOD 启思录》中给出一个令人满意的解决方案,他认为这个问题“没有最好的解决方案,所有的方案都有需要解决的问题”。
2. 客户的要求
某日,我被告知要到会议室开一个紧急会议。当我来到会议室时,发现与会者中有一个魔鬼——一个很麻烦的客户,马上就有一种不祥的预感。客户要求开发一个水果篮的类,当我接过客户的需求描述后,我愣住了,我知道即将要面临“水果篮问题”!这个客户是公司的大客户,把工作推掉是不可能的;从过往的经验中,我知道该客户非常善变,如果我提供的产品没有足够的灵活性,那么将陷入一个可怕的维护噩梦。
会上,客户在白板上写下了一下这段 C# 代码:
![](/Images/OutliningIndicators/None.gif)
![](/Images/OutliningIndicators/None.gif)
![](/Images/OutliningIndicators/None.gif)
![](/Images/OutliningIndicators/None.gif)
![](/Images/OutliningIndicators/None.gif)
![](/Images/OutliningIndicators/None.gif)
![](/Images/OutliningIndicators/None.gif)
![](/Images/OutliningIndicators/None.gif)
![](/Images/OutliningIndicators/None.gif)
并得意地说:“我很明白你们开发人员的难处,看,我为你们设想好水果篮将如何被使用了,算是减轻一下你们的负担吧!”
我发现我的顶头上司正望着我,从他的眼神可以看出他在期望我的答复能使客户满意,于是,我只好无奈地笑着说:“没问题,我不会让你失望的。”
3. 问题的分析
由于我不能够碰触水果层次结构,于是只好选择 Arthur 所提到“记账”方案来试着解决“水果篮问题”了。这里我将会充分使用反射机制来改善该方案,使水果篮与水果的派生类完全解耦。
设想水果篮里面维护着这样一个 Dictionary 数据结构:
其中,Dictionary 的 Key 是派生类的运行时类型(Type),而 Value 是是一个集合类(ArrayList),里面存放着对应的 Key 所描述的类型对象实例,即:
![](/Images/OutliningIndicators/None.gif)
这样,我们就知道 Add 方法的代码是如何写的了:
![](/Images/OutliningIndicators/None.gif)
![](/Images/OutliningIndicators/None.gif)
![](/Images/OutliningIndicators/None.gif)
![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](/Images/OutliningIndicators/ContractedBlock.gif)
![](/Images/dot.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
![](/Images/OutliningIndicators/ContractedSubBlock.gif)
![](/Images/dot.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
![](/Images/OutliningIndicators/ContractedSubBlock.gif)
![](/Images/dot.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
![](/Images/OutliningIndicators/ExpandedBlockEnd.gif)
当客户向水果篮添加某个派生类的实例时,例如香蕉,Add 将首先获取该实例的运行时类型,并与 m_Fruits 的各个 Key 做对比,看看是否已经存在香蕉这种类型。如果存在,就把该实例直接添加到与之对应的列表里;否则,为该类型新建一个列表,把该实例添加进去,再把该类型和此列表作为一个 Key-Value 对加入 m_Fruits 里。
当客户要执行某一派生类的“个性行为”时,我们将会从客户中获取两个重要的参数:派生类的类型和派生类的方法名。于是,我们可以使用反射来满足客户的要求:
![](/Images/OutliningIndicators/None.gif)
![](/Images/OutliningIndicators/None.gif)
![](/Images/OutliningIndicators/None.gif)
![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](/Images/OutliningIndicators/ContractedBlock.gif)
![](/Images/dot.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
![](/Images/OutliningIndicators/ContractedSubBlock.gif)
![](/Images/dot.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
![](/Images/OutliningIndicators/ContractedSubBlock.gif)
![](/Images/dot.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
![](/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
![](/Images/OutliningIndicators/ExpandedBlockEnd.gif)
当客户希望迭代整个水果篮,并让桔子切片时,Invoke 首先会检查 m_Fruits 里面是否存在桔子的列表,如果是将会迭代该列表并使用反射机制让桔子切片。值得注意的是,你必须先检查水果篮里面是否存有客户指定的那种水果。
这样,即使客户今晚决定去掉水果层次结构的苹果,并代之以榴莲,我也可以安心的睡觉,不怕被客户的电话吵醒了。
4. 进一步思考
对于“水果篮问题”,我们可以看到多态性对于执行派生类的“个性行为”几乎没有起到作用。如果要勉强使用多态性,那么你很可能会陷入“肥接口”的陷阱,导致水果层次结构的扭曲;如果你不适当地使用簿记工作,也将会陷入繁重的簿记泥潭,导致水果篮过多的与水果层次结构耦合在一起。
而我在这里所提供的方案,主要是以 .NET 丰富的运行时类型信息和强大的反射机制作为中介,使水果篮和水果的派生类解耦。这样不但可以减轻和改善了原本繁杂的簿记工作,而且还使得水果篮具有更好的适应性。
还有什么要考虑的吗?答案是:很多!是的,你没有眼花,上面只是提供了解决问题的基本思路,而我们离问题的解决还有一段很长的距离。那么,还有什么需要考虑呢?
- 你可能需要提供更多的 Invoke 的重载版本以方便那个魔鬼,如果你不希望某晚熟睡时突然接到他的电话,说他想以字符串的方式指定派生类的类型,因为 GUI 也是直接提供一个字符串!
- 在这里,为了简单起见,派生类的“个性行为”都不需要任何参数,也不返回任何结果。但你知道这等好事在现实世界中通常不会发生的,于是,Invoke 必须能够正确处理这些问题。
- 性能,一个我们不可以忽略的因素。程序能正常运行通常不会使客户满意,它还必须及时响应客户端的请求。你别指望客户会很有耐性地打开你的程序,然后去冲杯咖啡喝,最后回来看看运行的结果。
这里,我还有另外一个解决方案:
![](/Images/OutliningIndicators/None.gif)
![](/Images/OutliningIndicators/None.gif)
![](/Images/OutliningIndicators/None.gif)
![](/Images/OutliningIndicators/ExpandedBlockStart.gif)
![](/Images/OutliningIndicators/ContractedBlock.gif)
![](/Images/dot.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
![](/Images/OutliningIndicators/ContractedSubBlock.gif)
![](/Images/dot.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
![](/Images/OutliningIndicators/ContractedSubBlock.gif)
![](/Images/dot.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
![](/Images/OutliningIndicators/ContractedSubBlock.gif)
![](/Images/dot.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
![](/Images/OutliningIndicators/ContractedSubBlock.gif)
![](/Images/dot.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/ExpandedSubBlockStart.gif)
![](/Images/OutliningIndicators/ContractedSubBlock.gif)
![](/Images/dot.gif)
![](/Images/OutliningIndicators/InBlock.gif)
![](/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
![](/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
![](/Images/OutliningIndicators/ExpandedSubBlockEnd.gif)
![](/Images/OutliningIndicators/ExpandedBlockEnd.gif)
这个方案看起来更简单,尤其是水果篮里的数据结构以及 Add 方法,可以说不能再简单了,而 Invoke 也只不过是在迭代时对每个对象进行类型检查。
我并没有对这两种方案进行严格的性能测试,因为我只想在本文讨论设计的问题;但估计后者的性能更胜一筹,因为从定性的角度来看,后者占用的内存更少。当然,这仅仅是我的猜测,至于具体的性能测试工作,我会留给那些有兴趣的人做。
- [1] [美] Arthur J. Riel 著;鲍志云 译;《OOD 启思录》;人民邮电出版社,2004
- [2] 关于该问题的详细内容,请参见《OOD 启思录》一书 P108 的《5.19 没有最优解的问题》
转载于:https://www.cnblogs.com/allenlooplee/archive/2005/06/14/174427.html
用反射解决水果篮问题 [Design, C#]相关推荐
- 项目总结10:通过反射解决springboot环境下从redis取缓存进行转换时出现ClassCastException异常问题...
通过反射解决springboot环境下从redis取缓存进行转换时出现ClassCastException异常问题 关键字 springboot热部署 ClassCastException异常 反射 ...
- android自定义控件不显示,解决Android Studio Design界面不显示layout控件的问题
Android Studio更新到3.1.3后,发现拖到Design中的控件在预览界面中不显示: 解决办法: 在Styles.xml中的parent="..."中的Theme前添加 ...
- 通过反射解决在HuaWei手机出现Register too many Broadcast Receivers的crash
转载请注明出处:http://blog.csdn.net/llew2011/article/details/79054457 Android开发适配问题一直是一个让人头疼的话题,由于国内很 ...
- Android 源码系列之二十通过反射解决在HuaWei手机出现Register too many Broadcast Receivers的crash
转载请注明出处:http://blog.csdn.net/llew2011/article/details/79054457 Android开发适配问题一直是一个让人头疼的话题,由于国内很多厂商都有对 ...
- 安卓解决layout的design模式报错This view is not constrained
今天写一个新的layout,想着用design模式进行拖拽写,但是拖得很完美,最后报错: This view is not constrained, it only has designtime po ...
- 《博客园精华集---CLR/C#分册》
<博客园精华集---CLR/C#分册> 转:http://www.cnblogs.com/anytao/archive/2008/09/04/lovechina_bestclr_3rdfi ...
- 构造方法前可以用public修饰吗_程序员,你连反射都不会,还敢说自己会Java吗?...
一.反射机制 1.1 框架 在学习Java的路上,相信你一定使用过各种各样的框架.所谓的框架就是一个半成品软件,已经对基础的代码进行了封装并提供相应的API.在框架的基础上进行软件开发,可以简化编码. ...
- java 反射api_Java的反射API
java 反射api 如果您曾经问过自己以下问题: –"如何在字符串中仅包含其名称的方法调用?" –"如何动态列出类中的所有属性?" –"如何编写 ...
- Java的反射API
如果您曾经问过自己以下问题: –"如何在字符串中仅包含其名称的方法调用?" –"如何动态列出类中的所有属性?" –"如何编写一种将任何给定对象的状 ...
最新文章
- J2ME游戏引擎程序结构
- 【ASIC设计】ASIC设计流程
- C语言实现通用链表初步(一)
- 李宏毅老师ML_HW1——PM2.5预测
- android studio生成签名导打包的方法
- LeetCode(884)——两句话中的不常见单词(JavaScript)
- 【Todo】Java类型转换总结
- [Usaco2008 Mar]River Crossing渡河问题
- 10.企业应用架构模式 --- 数据源架构模式
- Fiddler抓包工具 学习笔记
- python 把当前目录文件夹中的所有图片缩放为640*480
- 运营进阶:打造好文案的万能公式
- 老男孩Linux67期第一课
- python照片处理生成3d模型_【神器】摄影实时建模,用照片生成3D模型
- 今天使用overleaf生成个人简历
- 渥太华大学计算机硕士课程,渥太华大学留学生经验分享:攻克语言关最简单的方法就是少用中文...
- matlab三元一次方程组的解包含未知数,用matlab解三元一次方程组_matlab解高阶方程_matlab二分法求方程的近似解...
- C++将一个cpp文件中的变量应用到另一个cpp文件中
- Java SE 基础部分经典100道笔试题
- 自然的密码---36幅由算法生成的六芒星图像