从变化逻辑的封装谈设计模式

通常来说,对于某个满足了我们大部分需要的类,可以创建一个它的子类,并只改变其中我们不期望的部分(需要变化部分)。只是继承一个类,就可以重用该类的代码,这是一件多美好的事情啊!不过,像大多数美好的事情一样,过度使用往往会变得不美好。根据可替换原则(LSP), public 继承具有概念上的现实意义,它代表的是一种is-a关系。使用继承之前一定要问问是否真的属于is-a的关系,否则继承非常容易被过度使用。基于此,建议优选使用对象组合(object composition)而不是类继承(class inheritance)。Strategy模式是将需要变化的逻辑封装在外部,然后通过组合方式把这部分易变化的逻辑传递给主题类。Strategy有各种变体,咱们来一一探讨:

方法一:如果需要变化的逻辑可以包装到一个函数中,可以藉由Function Pointers实现Strategy模式。(例子引自<>)

class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
	typedef int (*HealthCalcFunc) (const GameCharacter&);
	explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc):healthFunc(hcf){}
	int healthValue() const
	{ return healthFunc(*this);}
	//...
private:
	HealthCalcFunc healthFunc;
};

这个做法是Stategy设计模式的简单应用。和virtual继承的做法比较,可以绑定不同的函数甚至提供了运行期绑定不同函数指针的可能。弊端是无法访问类对象的non-public成分。(如果有此需求,可能要考虑friend函数或者private继承的方式了,不在此讨论范围)

方法二:藉由函数对象完成Strategy模式。

使用tr1::function的对象,可持有任何可调用物(函数指针、函数对象或成员函数指针),只要其签名式兼容于需求端。本质与方法一相同。关键在于函数对象的使用上。

方法三:经典Strategy模式:把需要变化的逻辑做成一个分离开来的继承体系中的virtual成员函数。

标准的Strategy模式更加容易辨识和交流。这个方法的吸引力在于更加纯粹的面向对象设计(考虑一下非c++程序员的感受,以下面向对象相关设计转为更加面向对象的C#语言来表达。)。而且,如果变化的逻辑单元多于一个,可以做成多个不同的virtual方法。如果要将一个新的健康算法策略纳入应用,只要为HealthCalcFunc继承体系添加一个derivedclass即可。

继续我们的探讨,如果HealthCalcFunc的逻辑不仅要考虑到算法本身的不同,还要考虑计算对象(GameCharacter)的不同,那要怎么办呢?

变化如下:

class EvilBadGuy:public GameCharacter {
public:
	//...
	override int healthValue() const
	{healthCalcObj.calcHealthValue(this);}
private:
	HealthCalcFunc healthCalcObj;
};

这涉及到两个多态分发。第一个分发是healthValue函数。该分发辨别出所调用的healthValue方法所属对象的类型。第二个分发是calcHealthValue方法,它辨别出要执行的特定函数。这两个分发共同实现了让不同的子类实例调用不同的calcHealthValue函数的目的。

等等, HealthCalcFuncclass仅仅提供计算的逻辑,其实我们并一定非要在GameCharacter中保留它的实例状态。仅仅在需要的时候传入对象就行了。去掉聚合的data member, healthValue函数接口变为:

int healthValue(HealthCalcFunc) const 

这样一来聚合关系就变成了依赖关系。如果我们再把类和函数接口的名字改变一下,完整的UML图应该如下所示:

啊,跑题了!这不是visitor模式吗?

上边的UML图确实就是一个visitor模式的类型图。:-)管它的,接着往下进行我们的故事吧。虽然用这一模式能解决我们的问题。想想看,它有没有什么问题,还有没有改进的空间呢?

注意!访问者层次结构的基类中对于被访问层次结构中的每个派生类都有一个对应函数。因此,就有一个依赖环把所有的被访问派生类绑定在一起。这样有什么问题呢?

这样,就很难实现对访问者结构的增量编译,并且很难向被访问层次结构中增加新的派生类。如果被访问层次结构非常不稳定,经常需要创建许多新的派生类,那么每当向被访问层次结构中增加一个新的派生类时,就必须要更改并且重新编译Visitor基类以及它的所有派生类。在C++中,情况更糟,每当增加任何一个新的派生类时,整个被访问层次结构就必须要被重新编译、重新部署。

问题的关键就在于解除这种如此强绑定的依赖环。让我们做出以下改动:

public override int accept (DegeneratedVisitor v)
{
    int value = 0;
    try
    {
        EvilBadGuyVisitor ev = (EvilBadGuyVisitor)v;
        value = ev.visit(this);
    }
    catch (ClassCastException e)
    {
    }

    return value;
}

DegeneratedVisitor蜕变成一个空接口。对于被访问层次结构的每个派生类,都有一个对应的访问者接口。如此一来,依赖变成了稳定的接口依赖关系。即使发生扩展,变化将被集中在具体的visitor子类当中。被访问派生类中的accept函数把Visitor基类转型(cast)为适当的访问者接口。如果转型成功,该方法就调用相应的visit函数。

其实Visitor的派生类并不需要针对每一个被访问的派生类都实现visit函数,当且仅当它们需要发生交互的时候。给进化后的模式起一个新的名称:AcyclicVisitor模式。

这种方法解除了依赖环,并且更容易增加被访问的派生类以及进行增量编译。当然万事皆有缺点,它的缺点就是:模型更加复杂。由于转型需要花费大量的执行时间,当被访问层次结构的宽度和深度增加之后,会更加糟糕。

把话题带回到我们的出发点——对需要变化部分的逻辑进行封装和扩展。Strategy模式侧重于所施加策略的多样性,从而对其进行继承体系外的封装和扩展。Visitor更多的侧重与被访问主体的不同处理需求。如果存在有需要以不同方式进行处理的数据结构,就可以使用Visitor模式。二者都在不改变现有类层次结构的情况下实现了变化逻辑的扩展。其实还有更多的不改变类层次结构的情况下扩展功能的方式。例如Decorator模式,ExtensionObject模式。不同的模式,不同的方法具有不同的应用场景和侧重点。

最后我想强调的是:千万不要为了设计模式而模式,不要去死记硬背设计模式。开始面向对象设计之前,先让自己对继承,封装,多态的有更深刻理解。面向对象是思想!

从变化逻辑的封装谈设计模式,布布扣,bubuko.com

时间: 2024-08-03 11:13:11

从变化逻辑的封装谈设计模式的相关文章

浅谈设计模式1-策略模式

对于大多数面向对象的初学者来说,将思维模式从面向过程转变过来是一个比较困难的过程.很多人在用面向对象语言编写程序的时候,依然会感觉自己在用面向过程的思维,笔者分享这篇文章的用意便是希望可以对大家有一些积极的影响. 阅读本文可以是没有接触设计模式,但需要一定的面向对象基础,至少简单理解封装,继承多态. 对于刚开始接触设计模式来说,一开始就说概念性的东西,很少能够理解.所以我们可以先跳过这些,通过一个小的程序场景来进行一个比较直观的认识. 模拟魂斗罗发射子&弹 相信大家小的时候玩过一款叫魂斗罗的游戏

浅谈设计模式的学习(上)

作为一个开发人员,能写出一个漂亮可扩展的代码,绝对是一件令人愉快的事情.那设计模式就是一门必修课! 本文就自己学习设计模式的一点经历做一个记录. 本人在读大学时,为了学习设计模式就买了一本<java与模式>的数据,书籍有一千多页很重.而且价格不菲.没办法,花那么多钱买的不看岂不浪费.于是每天早上读一章,坚持几个月我终于读完了.这几个月真是煎熬啊,几个月下来,回忆一下似乎自己真得也没收获到什么,很悍然啊.难道是书籍不好吗还是我读的不认真?其实在我现在看来都不是.而为什么读完了却什么也没收获到呢?

从王者荣耀谈设计模式?

软件开发中涉及到的设计模式很多,这里重点讨论工作中常见的一些设计模式,围绕王者荣耀中的场景进行展开. 1:策略模式 策略模式demo <?php //1:抽象策略接口:王者荣耀 abstract class kingGlory{ abstract function showTime(); } //2:具体策略角色 //鲁班 class luban extends kingGlory{ public function showTime(){ echo  '猥琐发育,躲坦克后面'; } } //王昭

Python+selenium自动化公共逻辑步骤封装

开篇 个人博客"Python+selenium的GUI自动化实现"提到的chrome与IE浏览器调用插件已上传至51CTO下载,对应链接分别为:chrome,http://down.51cto.com/data/2171584:IE,http://down.51cto.com/data/2171585:有需要的直接下载即可:  正文 关于自动化,其实质就是用机器操作代替手工执行,从而减少人力投入.节约项目运营成功.优秀的自动化框架,可能的一个发展过程,前期自动化用例写作实现过程,可能需

浅谈设计模式的学习(下)

时间过得真快啊,不知不觉又要周末了,借这个周末时间.把<浅谈设计模式的学习(下)>补上吧. 在<浅谈设计模式的学习(中)>中,说到了保持抽象的思维.接下来说一下第四点,做一个分享,也记录一下自己的学习历程. 4.学习设计模式,就不要把它看的太认真    设计模式是一个编程思想,它不是具体的代码套路.举个例子说明一下: 由于家传,接触到了一些中国的传统武术.当我与那些不懂传统武术的人交流的时候,他们总是认为中国的传统武术都是些套路.花架子,只是用来好看.在他们认为,两人打架,别人出拳

浅谈设计模式3-模板方法模式

模版方法模式,个人认为还是用处比较多的一个设计模式,而且也是比较好学和理解的一个.依然来通过模拟一个场景来慢慢了解. 现在我们来实现一下泡茶这个过程.首先我们需要烧开一壶水,然后往茶壶中放茶叶,加入开水,等待茶泡好. 经过前两次的分享,大家应该具备了基本的面向对象的思想了,这里就不再用面向过程的方式演示了. 首先,有一种普通人,他泡茶的方式是这样的 public class Common     { public void MakeTea()         {             Heat

从BaseActivity与BaseFragment的封装谈起

源至Hongyang微信公众号,博客源址:从BaseActivity与BaseFragment的封装谈起 这篇博客主要是从BaseActivity与BaseFragment的封装开始,总结自己在实战开发中关于Fragment的注意事项以及心得体会.先看以下效果图: 这里模拟的是用户登录模块,你可能会说,很普通的效果嘛,这有啥.嘿嘿,那我要告诉你的是,这么多模块仅仅由两个Activity构成的.等你从头到尾看完这篇博客,你就会惊叹其中的奥秘了.废话不多说,开始. 多模块Activity+多Frag

浅谈设计模式的学习(中)

在<浅谈设计模式的学习(上)>中我说到了设计模式的基石-----抽象思维.为什么需要抽象思维呢?因为越抽象就越不容易出错,就像有些领导人说话:坚持改革开放.但怎么算坚持改革开放呢,没有具体的标准,因事而异,所以就不容易违背这个坚持改革开放的原则了. 3.学习设计模式,要保持抽象的思维     什么是抽象思维呢?真的不好说,抽象的东西往往难以说明白,听了也难以搞明白,还是通过具体的例子来说吧 有这么一个学生请假的场景,如果请假时间一天以内则有班长批准就可以了,三天以内则需要老师批准,超过三天就得

浅谈设计模式

米老师布置了作业,分析23种设计模式的异同,绞尽脑汁,冥思苦想还是认为初学设计模式,此问题过于高深实在不敢妄语.可是作业始终是要交的,只好浅谈一下设计模式,如理解有误,也算留下了一个底子,日后回头再看,这便是成长. 关于分类,设计模式分为三大类,分别为行为型,创建型,和状态型. 亦有几大原则,分别为:单一职责原则.开放-封闭原则.依赖导致原则.迪米特法则. 总体来说,不论什么模式,都是为了更好的发挥面向对象的思想,来加强类内部的内聚,来降低类与类之间的耦合,尽量做到"高内聚,低耦合",