活用设计模式

一、设计模式的隐喻
    武功套路是习武的 门径, 新手要一招一式地练习套路,烂熟于心之后,便熟能生巧,在实战之中见招拆招、运用自如——此时习武之人已从“新手”成长为“好手”。“高手”则没有套路,实战之中只有自然反应,然而一招一式浑然天成、恰到好处,似有似无、无中生有。“高手”之上还有更高的“高手”,他们达到的境界非我等凭借金氏武侠小说可以揣测。
设计模式之于设计,好比套路之于武术。“新手”要一个接一个地学习模式,“好手”能够活用模式,“高手”则没有模式。
设计模式的“内功”是面向对象的基本原则。这些原则是“神”,模式是“形”。高手拼的是“内功”,对面向对象的基本原则有了正确而深刻的领悟,才能用好设计模式,避免“走火入魔”。
在很多设计模式著作的前几章都会介绍一些面向对象的基本原则,这几章非常重要。学通了这几章,后面的模式就不过如此了。学完了设计模式,也最好翻过头来重新看看这几章,保证会有新的领悟。
二、 为什么使用设计模式
对任何设计都可以凭主观(对设计很难做出客观评价)判断得出它是一个好的设计,还是一个坏的设计。使用设计模式是为了避免坏的设计。 Robert C. Martin 在他的著作《敏捷软件开发 原则、模式与实践》中描述了拙劣设计的症状:
l         僵化性(Rigidity):设计难以改变。
l         脆弱性(Fragility):设计易于遭到破坏。
l         牢固性(Immobility):设计难以重用。
l         粘滞性(Viscosity):难以做正确的事情。
l         不必要的复杂性(Needless Complexity):过分设计。
l         不必要的重复(Needless Repetition):过多的重复。
l         晦涩性(Opacity):混乱的表达。
Martin 叔叔没有给出“好”设计的定义。避免了上述症状的设计可能是个符合要求的设计,但未必是“好”设计。软件设计具有“艺术”特征,一个好的设计必定妥当、优雅、满足需求,反之则未必。使用设计模式是为了消除软件设计的恶劣症状,而不是保证给出一个好的、正确的设计。设计模式可能源于人们对软件的恐惧感吧(林星的文章中有句话:方法论源于恐惧)。
Martin 叔叔对上述拙劣设计的症状进行了详细描述。我们看到,产生上述恶劣症状的根本原因都是需求变化。使用设计模式是为了消除设计的恶劣症状,而产生恶劣症状的根本原因是需求变化。那么是否可以这样说:使用设计模式是为了拥抱变化。
三、 什么时候使用设计模式
Martin 叔叔的书中有段话:
“在学习它们(设计原则和模式)的时候,请记住,敏捷开发人员不会对一个庞大的预先设计应用那些原则和模式。相反,这些原则和模式被应用在一次次的迭代中,力图使代码以及代码所表达的设计保持干净。”
我从这段话中体会到这样几层含义:
l       代码是设计(这是Martin叔叔强调的一个观点,这个观点可以参考文献2的附录D);
l       设计模式是为了使设计适应变化;
l       设计模式是重构的工具;
l       设计一开始就要保持干净、简单,以后仍然要保持干净、简单;
l       不能过度使用设计模式。
使用设计模式的目的是为了适应未来的变化,变化之所以存在是因为它的不可预知性——如果可以预知,则不能称其为变化。如何判断哪些需求可能变化,哪些需求可能不变,并且在最大程度上保持设计的干净、简单,这是些工艺问题,而不是工程问题。既然是工艺问题,那么就只能给出原则,不便给出标准。我认为使用设计模式的基本原则是:对未来极有可能发生变化的问题给出最简单、修改成本最低的解。
四、 避免过度使用设计模式
易维护的程序首先要易理解,在易理解的代码上才好维护。过分地使用设计模式可能令程序晦涩、复杂,从而降低程序的易维护性。
Switch 语句曾经遭致诟病,许多重构的例子就拿Switch开刀。我认为Switch语句是高效的语句,可以写出极优雅、简单的代码。在很多情况下,直接使用Switch语句比把它拆成若干个Class更“干净”。
再比如,有一段四百多行的代码负责整个系统的调度,如果未来的变化仅仅是修改这四百行代码而不会大量添加代码,那么把这四百多行代码集中在一个函数里面有何不妥?这比把它拆分成十来个Class更加容易维护。
五、 讨论几个具体的模式
1 、 创建模式(Creational Pattern)
工厂(Factory)模式是很常用的模式。工厂模式的应用情景明确,设计思想简单。从使用多态到只用一个静态方法,工厂模式的变化形式有很多。类厂并不承载业务逻辑,需求变化对类厂的影响通常很小,因此使用重量级的工厂模式往往并不划算。一组包含层级关系的重量级的工厂类,可能意味着过度设计。
单例(Singleton)模式和工厂模式关系密切。从实现的角度讲,单例模式是工厂模式的一个特例,但是因为两个模式的应用情景不同,所以它们属于不同的设计模式。
抽象工厂(Abstract Factory)模式是工厂模式的推广。抽象工厂模式的应用情景更加特殊和严格。在一个使用抽象工厂的设计中,如果未来发生不同产品族各自演化的情形,那么抽象工厂模式就可能崩溃了。在实际应用中,不同产品族各自演化,最终分道扬镳的情形是有的,用户提出这样需求而产生的后果的确“触目惊心”。在使用抽象工厂模式之前,一定要保证从现在到未来都能够用一致的方式“生产”这些产品族。
把工厂模式稍加变化就可以得到建造(Builder)模式。工厂模式的“加工工艺”是隐藏的,而建造模式的“加工工艺”是暴露的。这点不同,使建造模式在更加灵活的同时也略失优雅。
2 、 模板模式(Template Method)和策略(Strategy)模式
模板模式和策略模式的应用情景类似,但实现方式不同,前者使用继承,后者使用委托。
模板模式有可能是最“古老”的模式之一,在使用面向对象技术的早期,“继承”大行其道,很多设计人员可能不自觉地使用过模板模式。模板模式的缺点是把具体实现和通用算法紧密地耦合起来,使得具体实现只能被一个通用算法操纵。然而在继承关系中,父类的信息可以更多地暴露给子类,这种(违背面向对象设计原则的)微妙的沟通有时在一些特定的情况下显得更加廉价和方便。
策略模式是委托的经典用法。策略模式消除了通用算法和具体实现的耦合,使得具体实现可以被多个通用算法操纵。策略模式增加了类层次,结构上面比模板模式复杂。
模板模式和策略模式通常可以互相替换。它们都像试卷,模板模式是填空题,策略模式是选择题。
3 、 简化问题的模式
门面(Facade)模式把一组复杂的接口隐藏在一个简单且特定的接口后面。
调停者(Mediator)模式把对象之间的引用关系包装在一个特定的容器里面。
组合(Composite)模式描述了整体与部分的结构关系,并且允许用一致的方式处理这个结构。
上面几个模式对使用者而言,都在一定程度上起到了简化问题的作用。
4 、 扩展功能的模式
访问者(Visitor)模式和装饰(Decorator)模式都可以在不改变现有类结构的基础上,动态地增加功能。
访问者模式把现有类结构上的对象“分配”到一个名为访问者的类中,在访问者的相应方法中配置对象、改变对象或扩展功能。
装饰模式把现有类结构上的对象“注入”到一个装饰类中,在装饰类中扩展它的功能。
访问者模式和装饰模式在实际效果上是不同的。访问者模式可以把对象分配到相应的方法里,从而对每个对象分别进行加工或扩展。而装饰模式只能用一致的方式对所有的被装饰对象进行加工或扩展,要想实现不同的加工或扩展,只能增加新的装饰类。
过多的“装饰类”有可能使业务逻辑分散,并且使程序结构复杂。针对每一个具体的派生类,“访问类”都要有一个对应的方法,增加派生类的时候也要增加访问类的方法。扩展功能的需求是经常发生的,是否有必要使用上述模式则值得再三考虑。
5 、 其他常用的模式
桥梁(Bridge)模式。Class是封装了行为和属性的容器,然而Class的一组行为可能独立演化,这时最直接的想法是使用继承,把各不相同的行为封装在不同的子类里。桥梁模式从另外的角度解决了这个问题。桥梁模式把独立演化的行为封装在另外一个类体系里,与原来的类体系分别独立演化,两个类体系在抽象层次是“使用”关系。在很多OO教材里面用Shape类封装属性和Draw方法,在桥梁模式里,“形状”和“画笔”是两组独立演化的类体系,在抽象层次,“形状”使用“画笔”绘制自己。
适配器(Adapter)模式令被适配的对象或类遵循既有的接口协议,因此适配器一词常常出现在更高一层的架构设计中。从相反的角度讲,适配器能够改变依赖关系的指向,把一个接口和它的实现类脱藕,从而获得更大的灵活性。
命令(Command)模式被Martin称为“最简单、最优雅的模式之一”。命令模式的魅力在于它为每个类“培训”出了相同的技能,经过“培训”的类容易管理、容易组织、容易调度,能够产生不可思议的能力。
观察者(Observer)是实现对象间通讯的经典方法,它历史悠久,使用广泛。尽管J2EE内置了观察者模式,在.NET里也可以通过 Delegate 实现相似的功能,但是掌握 观察者模式的原理和实现,依然是十分必要的。
6 、 不太重要的模式
J2EE 和.NET对迭代子(Iterator)模式和原型(Prototype)模式都有内建的支持,备忘录(Memento)模式则可以利用Class的序列化能力来代替。
可以用其他的方式替代责任链(Chain of Responsibility)模式,例如观察者模式、消息机制等。
享元(Flyweight) 模式和解释器(Interpreter)模式都是应用情景比较具体的模式,因此它们的应用范围比较窄。
   如果在一开始就知道某些底层策略一定会被替换掉,那么使用代理(Proxy)模式来隔离这些策略还是有必要的。否则,几乎没有使用代理的必要。
 
参考文献:
1.Gamma etc., Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley1995.
(中译本:《设计模式:可复用面向对象软件的基础》,李英军等译,机械工业出版社,2000年)

2.Robert C. Martin,Agile Software Development Principles, Patterns, and Practices.(中译本:《敏捷软件开发 原则、模式与实践》,邓辉译,清华大学出版社,2003年)

作者:
张昱 曾就职于浪潮集团、联想集团,现就职于中科院电子所。超过十年的软件工作经验。对分析设计、项目管理、编程实践有着浓厚的兴趣。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值