换种思路去理解设计模式(下)

开写之前,先把前两部分的链接贴上。要看此篇,不许按顺序看完前两部分,因为这本来就是一篇文章,只不过内容较多,分开写的。

换种思路去理解设计模式(上)


换种思路去理解设计模式(中)

8       对象行为与操作对象


8.1     过程描述


所谓对象行为和操作对象,需要三方面内容:

操作过程:

一般表现为一个方法。该方法接收一个对象或者组合类型的参数,然后对这个对象或者组合进行操作,例如修改属性、状态或者结构等。

操作的对象或组合:

会作为实参传入操作过程,会被操作过程修改属性、状态或者结构等。

受影响的对象或组合:

由于修改其他对象或者组合,可能会影响到另外的对象或者组合,因此需要考虑这种影响关系,一般会用通知或者消息订阅的方式实现。

从上文的对象创建和对象组合两个模块,应该理解出在系统设计中会遇到各种复杂的情况。而对象操作比前两者更加复杂,因为前两者是创建一个静态的结构,而对象操作则是一个动态的变化。在日常的开发工作中,对象操作也是我们付出精力最多的地方。

下面我们就对象操作过程中遇到的一些常见情况做详细的分析。

8.2     情况1:“多配置”操作的优化

  当我们的一个方法因为需要实现多配置而不得不写许多条件判断语句时,我们会考虑将这个方法抽象出来,然后派生不同的子类。这是基本的设计思路。

  现在我们将这个情况复杂化,业务场景多了,一个方法无法实现这些功能,就需要拆分。

  

  如果这种情况下,再出现因为很多配置而不得不写许多条件判断语句时,我们肯定还需要再次考虑抽象和派生。效果如下图:

  

   这就是——模板方法模式。

  理解这个模式其实很简单,只要知道根据多配置需要抽象、拆分即可。至于这里的“模板”,可根据实际情况来使用或者改变。

8.3     情况2:串行操作的优化


  针对对象进行操作时,类似于流程一样的串行操作,在系统中应用非常广泛。而且各个串行的节点都有相对统一的操作过程,例如工作流的每个审批节点,都会修改对象状态以及设置下级审批人等。

  遇到这种场景,我们最初会思考以下思路:

  

  后来随着系统的升级和变更,代码越来越多,维护越来越困难。我们会先考虑将每一步操作都独立成一个方法:

  

   一般的串行操作,可以用以上代码结构来处理,需要修改处可以再根据实际情况再重构。但如果串行操作中有条件因素,可能就有优化的空间了。如下代码:

  

  当随着我们的条件越来越多,业务关系越来越负责时,维护这段代码就越来越复杂,也可能因为多人维护而带来版本问题。需要优化。

  分析任何问题,都先要从业务上抽象。以上代码抽象出来有两点:“操作”和“条件”。“操作”很容易抽象的,但是“条件”却不好抽象。没关系,职责链模式给了我们灵感,我们可以把“条件”看作“下一步的操作”。

  好了,我们把“操作”和“下一步的操作”抽象出来。然后将每一步的操作处理作为抽象的一个派生类。

  

  如上图,每个子类是一步处理。每一步处理都实现了抽象类的RequestHandler()方法,都继承了抽象类的success属性(即下一步执行者)。这样我们就可以设计ConcreteHandler1的success为ConcreteHandler2,ConcreteHandler2的success为ConcreteHandler3,从而构成了一个“职责链”。

  这就是职责链模式的设计思想。

8.4     情况3:遍历对象各元素


  当对一个对象操作时,遍历对象元素是最常见的操作之一,使用Java和C#的人用的最多的就是for和foreach(先放下foreach不说,后文会有解释),C和C++一般用For循环。For循环简单易用,但是有它设计上的缺点,先看一段for循环代码:

  

  代码中obj是个特别刺眼的变量,如果代码较多了,就会出现满篇的obj。这里的代码是客户端,过多的obj就会导致了大量的耦合。如果obj的类型一点有修改,就会可能导致整个代码都要修改。

  设计是为了抽象和分离。我们需要一种设计来封装这里的obj以及对obj的遍历过程。我们定义一个类型,它接收obj,负责遍历obj,并把遍历的接口开放给客户端。

  

  代码中,我们通过Next()和IsDone()就可以完成一个遍历。可以给客户端提供First()、Current()、Last()等快捷接口。

  这样,我们可以用这种方式迭代对象。

  

  代码中只用到一个obj,因为我们如果再有迭代过程,可以用iterator对象,而不是obj。这就是迭代器模式的设计思路。

  

  前文中提到了foreach循环。其实foreach是C#和java已经封装好的一个迭代器,它的实现原理就是上文中讲到的方法。在日常应用中,foreach在大部分情况下能满足我们的需求。但是要真正理解foreach的用以,还需这个迭代器模式的帮助。

8.5     情况4:对象状态变化


  改变一个对象的状态,是再常见不过的操作了。例如一个对象的状态变化是:

  

  这几乎是最简单的流程了,我们一般的开发思路如下:

  

  这是最基本的思路,如果再改进,可能会把状态统一维护成一个枚举,而不是硬编码在代码中直接写“编辑”“结束”等。

  

  但是这样改进,代码的逻辑和结构是不变的。仍然存在一个问题——当状态变化的逻辑变复杂时,这段代码将变得非常复杂——大家应该都明白,在真正的系统中,状态变化可比上面那个图片复杂N倍。

  大家可能都注意到了,一旦遇到这种问题,那肯定是违反了开放封闭原则和单一职责原则。要改变这个问题,我们就得重构这段代码,从设计上彻底避免。

  首先要做的还是抽象,然后再去隔离和解耦。当前这个业务场景中,能抽象出来的是“状态”和“状态变化”。

  那么我们就把状态作为一个抽象类,把状态变化这个都做作为抽象类中的抽象方法,然后针对每个状态,都实现成一个子类。结构如下:

  

  然后再把对象关联到状态上,根据依赖倒置原则,对象将依赖于抽象类而非子类。

  

  上图中,Context的状态是一个State类型,所以无论State派生出多少个子类,都能成为Context的状态。至于Context的状态变化,就在State子类的Handle方法中实现。例如ConcreateStateA的handle方法,可以将Context的状态赋值成ConcreteStateB对象,ConcreteStateB的handle方法,可以将Context的状态赋值成ConcreteStateC(图中没有)对象……一次类推。这样就将一个复杂的状态变化链,分解到每一步状态对象中。

  这种设计思路就是——状态模式。

8.6     情况5:记录变化,撤销操作

  上文提到如何改变对象的状态,从这里我们可以想到状态的撤销,以及其他各个属性修改之后的撤销操作。撤销操作的主要问题就在于如何去保存、恢复旧数据。

  最简单的思路是直接定义一个额外的对象来存储旧数据,

  

  如果需要撤销,再从存储旧数据的对象中获取信息,重新赋值给主对象。

  

  由此可能发现,上图中客户端的代码非常繁琐,而且客户端几乎查看到了主对象类型和封装对象类型的所有信息,没有了所谓的“封装”。这样带来的后果是,如果主对象属性有变化,客户端立刻就不可用,必须修改。

  其实客户端应该只关注“备忘”和“撤销”这两件事情、这两个操作,不必去关心主对象中有什么属性,以及备忘对象有什么属性。再思考,“备忘”和“撤销”这两个动作都是针对主对象进行的,主对象是这两个动作的执行者和受益者。那么为何不把这两个动作直接交给主对象进行呢?

  根据以上思路重构代码可得:

  

  在原来代码的基础上,我们又给主对象增加了两个方法——创建备忘和撤销。接下来客户端的代码就简单了。

  

  正如我们上面所说的,客户端关注的只是这两个动作,而不关心具体的属性和内容。

  这就是——备忘录模式。看起来很简单,也很好理解。它没有用到面向对象的太多特点,也没有很复杂的代码,仅仅体现了一个设计原则——单一职责原则。利用简单的代码对代码职责就行分离,从而解耦。

8.7    情况6:对象之间的通讯 – 一对多

  一个对象的属性反生变化,或者一个对象的某方法被调用时,肯定会带来一些影响。所谓的“影响”具体到系统中来看,无非就是导致其他对象的属性发生变化或者事件被触发。

  近来在生活中遇到这样的两个场景。第一,白天用手机上的某客户端软件看NBA文字直播,发现只要某个球员有进球或者篮板之类的数据,系统中所有的地方都会更新这个数据,包括两队的总分比分。第二,看jquery源码解读时,jquery的callbacks的应用也是这种情况,事例代码之后贴出。这两种情况都是一对多通讯的情况。

  

  (看以上代码的形式,很像C#中的委托)

  

   先不管上面的代码或者例子。先想想这种一对多的通讯,该如何去设计,然后慢慢重构升级。最简单的当然是在客户端直接写出来,浅显易懂。这样写代码的人肯定大有人在(因为我之前曾是这样写的):

  如果系统中这段代码只用一次,这样写是没有问题的。但是如果系统有多地方都是context.Update(),那将导致一旦有修改,每个地方都得修改。耦合太多,不符合开放封闭原则。

  解决这个问题很简单,我们把受影响对象的更新,全部放到主对象的更新中。

  

  再想想还会遇到一个问题:难道我们每次调用Context的Update时,受影响的对象都是固定的吗?有工作经验的人肯定回答否。所以,我们这样盲目的把受影响对象的更新全部塞到Context的Update是有问题的。

  其实我们应该抽象出来的是“更新”这个动作,而应该把具体的哪些对象受影响交给客户端。如下:

  

  上图中,我们把受影响对象的类型抽象出一个Subject类,它有一个Update()方法。在主对象类型中,将保存一个Subject类型的列表,将存储受影响的对象,更新时,循环这个列表,调用Update方法。可见,Context依赖的是一个抽象类——依赖倒置原则。

  这样,我们客户端的代码就成了这样:

  

  这次终于实现了我们的预想。可以对比一下我一开始贴出来的js代码。效果差不多。

  

  这就是大家耳熟但并不一定能详的——观察者模式。最常见的例子除了jquery的callbacks之外,还有.net中的委托和事件。此处不再深入介绍,有机会再把委托和事件如何应用观察者模式的思路介绍一下。

8.8     情况7:对象之间的通讯 – 多对多

  上文中提到一对多的通讯情况,比一对多更复杂的是多对多的通讯。如果用最原始的模式,那将出现这种情况,并且这种情况会随着对象的增加而变得更加复杂。N个对象就会有N*(N-1)个通讯方式。

  

  所以,当你确定系统中遇到这种问题,并且越发不可收拾的时候,就需要重构代码优化设计。思路还是一样的——抽象、隔离、解耦。当前场景的复杂之处是过多的“通讯”链条。我们需要把所有的“通讯”链条都抽象出来,并隔离通讯双方直接联系。所以,我们希望的结构是这样的。

  

  其实这就是中介者模式。

  以上只是一个思路,看它是怎么实现的。

  

  首先,把所有需要通讯的类型(成为“同事”类型),抽象出一个基类,基类中包含一个中介者Mediator对象,这样所有的子类中都会有一个Mediator对象。子类有Send()方法,用于发送请求;Notify()方法用于接收请求。

  

  其次,中介者Mediator类型,基类中定义Send()抽象方法,子类中要重写。子类中定义了所有需要通讯的对象,然后重写Send()方法时,根据不同情况,调用不同的同事类型的Notify()方法。如下:

  

  这样,在同事类型中,每个同事类的Send()方法,就可以直接调用中介者Mediator的send()方法。如下:

  

  最后,总体的设计结构如下:

  

  越看似简单的东西,就越难用。因为简单的东西具有通用性,而通用就必须适应各种环境,环境不同,应用不同。中介者模式就是这样一种情况。如果不信,可以具体思考一下,你的系统中哪个场景可以用中介者模式,并且不影响其他功能和设计。

  在具体应用中,还是把重点放在这个设计的思路上,不必太拘泥与它的代码和类图,这只是一个demo而已。

8.9     情况8:如何调用一个操作?

  对于这个问题,我想大部分人都会一笑而过:如何调用?调用就是了。一般情况下是触发一个单独的方法或者一个对象的某个方法。但是你应该知道,我既然在这个地方提出这个问题,就肯定不是这样简单的答案。

   难点在于如何分析“操作”这个业务过程。其实“操作”分为以下三部分:

  • 调用者

  • 操作

  • 执行者

  首先,调用者不一定都是客户端,可能是一个对象或者集合。例如我们常见的电视遥控器,就是一个典型的调用者对象。

  其次,操作和执行者不一样。操作时,除了真正执行之外,还可能有其他的动作,如缓存、判断等。

  最后,这样的划分是为了职责单一和充分解耦。当你的需求可以用简单的函数调用解决时,那当然最好。但是如果后期随着系统的升级和变更而变得业务复杂时,就应该考虑用这种设计模式——命令模式。

  

  上图是命令模式的类图。左侧是的Command和ConcreteCommand是操作(命令)的抽象和实现,这个不重要,我们可以把这两个统一看成一个“操作”整体。Invoker是调用者,Receiver是真正的执行者。

  调用过程是:Invoker.Excute() -> Command.Excute() ->
Receiver.Action()。这样我们还可以在Command中实现一些缓存、判断之类的业务操作。可以按照自己具体的需求编写代码。

  具体的代码这里就不写了,把这个业务过程理解了,写代码也很简单。重点还是在于理解“操作”(命令)的业务过程,以及在复杂过程下对调用者、操作、执行者之间的解耦。

8.10     情况9:一种策略,多种算法

  

  假如上图是我们系统中一个功能的类图,定义一个接口,用两个不同的类去实现。客户端的代码调用为:

  

  有出现了比较讨厌的条件判断,任何条件判断的复杂化都将导致职责的混乱和代码的臃肿。如果想要解决这种问题,我们需要把这些逻辑判断分离出来。先定义一个类来封装客户端和实现类的直接联系。

  

  如此一来,客户端的调用代码为:

  

  这就是——策略模式。类图如下:

  

  

  附:关于这个策略模式,思考了很久也没有想出一个何时的表达方法,我没有真正理解它的用处,感觉它说的很对,但是我觉得它没多少用处。所以,针对这个模式的表述,大家先以参考为主。如果我有了更好的理解方式,再修改。

8.11     情况10:简化一个对象组合的操作

  针对一个对象组合,例如一个递归的树形结构,往往对每个节点都会有相同的操作。代码如下:

  

   如果对象结构较复杂,而且新增功能较多,代码将会变得非常臃肿。

  解决这个问题时,不好直接去抽象。一来是因为现在已经在一个抽象的结构中,二来也因为每个节点新增的功能,不一定都相同。所以,现在我们最好的方式是将“新增功能”这个未来不确定的事情,交给另外对象去做。先去隔离。

  另外定义一个Visitor类,由它来接收一个Element对象,然后执行各种操作。

  

  此时在Element类中,就不需要每次新增功能时,都重写代码。只需要在Element类中加入一个方法,该方法将调用Visitor的方法来实现具体的功能。

  

  这就是——访问者模式,它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

8.12     总结

  注:Interpreter解释器模式不常用,暂不说明。

  本节介绍了对象行为和对象操作过程中的一些常用业务过程以及其中遇到的问题,同时针对每种情况引入了相应的设计模式。

  这块儿的过程较复杂,梳理起来也比较麻烦,当前的描述和一个系统的流程相比,我想还是有不少差距的。但是相信大家在看到每种情况的时候,或多或少的肯定有过类似的开发经历,每种情况都是和我们日常的开发工作息息相关的。如果这些介绍起不到一个教程或者引导的作用,那就权当是一次抛砖引玉,或者一次学习交流。

  最终还是希望大家能从系统的设计、重构、解决问题的角度去引出设计模式,然后才能真正理解设计模式。

9      总结


  从5.12开始写,到今天6.4,磕磕绊绊的总算写完了初稿。虽然不到一个月,但是坚持下来也很不容易。而且这只是一个开始,我想再在这个基础上继续写第二版、第三版,当然还需要再去看更多的书、博客以及结合实际的开发经验和例证。

  且先不说应用,即便是真正理解设计模式,也不是易事,没有开发经验、没有一个有效的方法,学起来也是事倍功半。甚至会像我之前一样,学一次忘一次。我觉得我现在提出的思路有一定效果,至少对于我是有效的。大家如果有其他建议或者思路,欢迎和我交流
wangfupeng1988$163.com($->@)

换种思路去理解设计模式(下),布布扣,bubuko.com

时间: 2024-09-30 21:29:29

换种思路去理解设计模式(下)的相关文章

如何通俗理解设计模式及其思想

术与道 数据结构,算法,设计模式被认为是程序员必备技能的三叉戟,如果说编程语言的语法特性和业务编码能力是[术],那么这三者可以称得上是[道]--[术]可以让你在IT行业赖以生存,而[道]则决定你未来在技术这条道路上可以走多远. 边学边忘的窘境 先自我介绍一下. 我是一个两年工作经验的Android开发人员,就像很多同行一样,对于数据结构,算法,设计模式这些被奉为程序员必修的三门内功,几乎没有去系统性地学习过(曾经专业课的数据结构,如今也基本还给了老师). 你问我想不想当一个称职的程序员,当然!数

深入理解设计模式(终):总结--设计模式是药,没病就不要吃药

系列链接地址: 深入理解设计模式---系列目录 一.产品汪的神助攻,代码狗的安慰剂 定义:设计模式,指的是一个场景(context)下的一种解决方法(Solution),只要大家都认可某种模式,那么就只需要很短的一个名字,就可以代替很多很多的语言和文字交流,如果你觉得设计模式降低生产效率,那只能说你在这一行还要继续修炼. 宗旨:保证系统的低耦合高内聚,指导它们的原则无非就是封装变化,责任单一,面向接口编程等设计原则 目的:就是为了让代码变得更容易理解和维护 精髓:将复杂的逻辑抽离出来,让开发人员

深入理解设计模式六大原则

深入理解设计模式六大原则 万变不离其宗,不管是Java还是C++,凡是面向对象的编程语言,在设计上,尽管表现形式可能有所不同,但是其实质和所需遵守的原则都是一致的.本文便是带领读者去深入理解设计模式中的六大原则,以期帮助读者做出更好的设计. 单一职责原则 单一职责原则:Single Responsibility Principle,简称SRP 定义: 应该有且仅有一个原因引起类的变更. 问题场景: 类C负责两个不同的职责:职责D1,职责D2.当由于职责D1需求发生改变而需要修改类C时,有可能会导

Asp.net设计模式笔记之一:理解设计模式

GOF设计模式著作中的23种设计模式可以分成三组:创建型(Creational),结构型(Structural),行为型(Behavioral).下面来做详细的剖析. 创建型 创建型模式处理对象构造和引用.他们将对象实例的实例化责任从客户代码中抽象出来,从而让代码保持松散耦合,将创建复杂对象的责任放在一个地方,这遵循了单一责任原则和分离关注点原则. 下面是“创建型”分组中的模式: 1.Abstract Factory(抽象工厂)模式:提供一个接口来创建一组相关的对象. 2.Factory Met

按自己的想法去理解事件和泛型(C#)

上一篇那些年困扰我们的委托(C#)讲了委托,这一篇自然就轮到事件了. 不喜欢官方的表达方式,喜欢按照自己的想法去理解一些抽象的东西,我是一个喜欢简单怕麻烦的人. 事件 考虑到委托使用的一些缺陷,就有了事件.委托是不安全的,打个比方,如果把委托当作共有字段,那么事件就相当于是属性的概念. 事件就是被限制使用的委托变量,事件里面封装了一个多播委托. 事件语法:public event 委托类型 事件名; 事件的作用:事件的作用与委托变量一样,只是功能上比委托变量有更多的限制.比如:只能通过+=或者-

从认知角度去理解设计

设计并不是一味只求美感或者感觉,设计同样是一门建立在多学科基础上的科学,从认知角度来理解设计能帮助我们设计出更多尊重用户的作品,这样的设计才能经得起时间的考验,让更多用户所喜爱. 下面是我对<认知与设计——理解ui设计准则>这本书的概要与理解. 一.影响我们感知的因素     a. 经验影响感知: 我们根据经验对事物的预想:先入为主的主观印象往往影响感知,当我们带有    不同的主观感受去观察同一张图片时会看到不同的东西 我们的认知框架:认知框架即是不断置身的各种环境在我们心智中建立起开模式,

从有限状态机的角度去理解Knuth-Morris-Pratt Algorithm(又叫KMP算法,”看毛片“算法)

转载请加上:http://www.cnblogs.com/courtier/p/4273193.html 在开始讲这个文章前的唠叨话: 1:首先,在阅读此篇文章之前,你至少要了解过,什么是有限状态机,什么是KMP算法,因为,本文是从KMP的源头,有限状态 机来讲起的,因为,KMP就是DFA(Deterministic Finite Automaton)上简化的. 2:很多KMP的文章(有限自动机去解释的很少),写得在我看来不够好,你如果,没有良好的数学基础就很难去理解他们(比如下图), 因为,你

从汇编角度来理解linux下多层函数调用堆栈执行状态

注:在linux下开发经常使用的辅助小工具: readelf .hexdump.od.objdump.nm.telnet.nc 等,详细能够man一下. 我们用以下的C代码来研究函数调用的过程. C++ Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int bar(int c, int d) { int e = c + d; return e; } int foo(int a, int b) { return bar(a, b); } int main(

从逆向的角度去理解C++虚函数表

很久没有写过文章了,自己一直是做C/C++开发的,我一直认为,作为一个C/C++程序员,如果能够好好学一下汇编和逆向分析,那么对于我们去理解C/C++将会有很大的帮助,因为程序中所有的奥秘都藏在汇编中,很多问题我们从表面上无法看出到底是为什么,只要用逆向工具一分析,很快就能知道其中的所以然来.我们都知道虚函数表是放在类对象的最前面,但是很多人并不知道虚函数表的第一项存放的是什么,下面我用IDA分析一下C++的虚函数调用,从汇编的角度去理解虚函数.此文只适合具有逆向分析基础的读者,如果没有逆向分析