换种思路去理解设计模式

1 前言

  看过许多关于设计模式的博客,也读过关于设计模式的书。几乎所有的介绍的开头,直接就引入了“设计模式”或者“某某模式”。设计模式到底是因什么而来?这是一个很重要的问题。孙悟空从石头缝里蹦出来,《西游记》还介绍了这个石头的来历呢。

  要想了解一个东西,至少有“3W”——what、why、how——是什么、为什么、怎么用。看现在大部分的文章或者书籍,重点介绍的还是“what”,这就有点类似于:为了用设计模式用设计模式。在这种思想的教导下去了解设计模式,学不会也很正常。

     另外,介绍一个东西的用处时,不要弄一些小猫小狗、肯德基、打篮球、追MM这话总例子。这就像用小学课本的儿童故事来给你讲解一个人生道理一样,你听着明白,但是真能理解吗?

 

2  概述

  记得之前看过一篇博客,具体内容现在都忘记了。记得上面有句话大体是什么说的:所谓的设计模式,我们日常工作中经常用,只是我们没有想过像GoF一样,把这些日常用到的模式总结归纳,形成结构化的理论。

  可见,设计模式不真正是GoF提出的概念,而是他们作为一个有心人,把人们日常工作中遇到的设计问题,全面的总结,才形成了之后的“23种设计模式”。

   首先,设计模式解决的肯定是系统设计问题,而且会用到面向对象来解决的。所以,本书开头先说设计原则和面向对象。面向对象基础知识,大部分人应该都了解;至于设计原则,不了解的人必须要先了解。

  其次,我们将模拟一个简单的对象声明周期过程,从对象的创建、封装、组合、执行和操作,一步一步走来,会遇到许多情况和问题。针对问题,我们将通过思考,利用面向对象和设计原则,解决这个问题。而解决这个问题的方法,便是一种设计模式。

  最后,23种设计模式不是一盘散沙,是有关系的。就是对象的生命周期一步一步的将各个设计模式串联在了一起。对象的生命周期中,会一步一步的遇到总共23种设计问题,所以才会有23种设计模式。

3  设计原则

  设计模式解决的肯定是系统设计的问题,所以首先从“设计”说起。

  设计所要解决的主要问题,是如何高效率、高质量、低风险的应对各种各类变化,例如需求变更、软件升级等。设计的方式主要是提取抽象、隔离变化,有5大设计原则——“SOLID”,具体体现了这个思路。

  • S - 单一职责原则:

  一个类只能有一个让它变化的原因。即,将不同的功能隔离开来,不要都混合到一个类中。

  • O - 开放封闭原则:

  对扩展开放,对修改封闭。即,如果遇到需求变化,要通过添加新的类来实现,而不是修改现有的代码。这一点也符合单一职责原则。

  • L - Liskov原则:

  子类可以完全覆盖父类。

  • I - 接口隔离原则:

  每个接口都实现单一的功能。添加新功能时,要增加一个新接口,而不是修改已有的接口,禁止出现“胖接口”。符合单一职责原则和开放封闭原则。

  • D – 依赖倒置原则:

  具体依赖于抽象,而非抽象依赖与具体。即,要把不同子类的相同功能抽象出来,依赖与这个抽象,而不是依赖于具体的子类。

 

  总结这些设计原则可知,设计最终关注的还是“抽象”和“隔离”。面向对象的封装、继承和多态,还有每个设计模式,分析它们都离不开这两个词。

4  面向对象基础

  继承、封装、多态

  接口、抽象类

5  一个对象的生命周期

  一个对象在系统中的生命周期可以概括为以下几点:

  • 对象创建:

  想到对象创建,最多的就是通过new一个类型来创建对象。但也会有许多特殊的情况,例如对象创建过程很复杂,如何解耦?等等。

  • 对象组合、包装:

  一个对象创建后,可能需要对其就行包装或者封装,还可能由多个对象组成一个组合结构。在这过程中,也会遇到各种问题。

  • 对象操作:

  对象创建了,也组合、包装完毕,然后就需要执行对象的各种操作,这是对象真正起作用的阶段。对象的操作情况众多,问题也很多。

  • 对象消亡:

  直到最后对象消亡,在C#中将被GC回收。

 

  以上简单介绍这个过程,其中的具体描述以及遇到的情况和问题,会在下文中详细讲解

6   创建一个对象

6.1   过程描述

一般对象的创建可以new一个类型,相信系统中绝大部分的对象创建也是这么做的。但是如果遇到以下情况,直接用new一个类型,会遇到各种各样的问题。

6.2   情况1:拷贝创建

  系统中肯定会遇到这种情况,新建对象时,要用到一个现有对象的许多属性、方法等。这时候再通过new一个新的空对象,还需要把这些属性、方法都赋值到新对象中,带来不必要的工作量。

  提出这个问题,我们会想到克隆,也可能已经在系统中用到了克隆。其实这个就是一个比较简单的设计模式——原型模式。我们把这个“克隆”动作抽象到一个接口中,需要克隆的类型,实现这个接口即可。

        

         C#已经在FCL(Framework Class Library)中定义了一个接口——IColoneable,因此不需要我们在自己定义该接口,只需要在用到的地方实现即可。IColoneable接口只定义了一个Colone方法:

        

  例如FCL中的String类,实现了IColoneable接口,并实现了接口方法Colone()。

        

 

6.3  情况2:限制单一对象

  如果一个对象定义的属性和方法,可供系统的所有模块使用,例如系统的一些配置项。此时无需再去创建多个对象。也不允许用户创建多个对象,因为一旦修改,只修改这一个对象,系统的所有模块都将生效。

  我们把这个只能实例化一次的对象叫做“单例”,这种模式叫做单例模式

  其实系统中的静态类,就是这种“单例”的设计思想。例如FCL中的Console类,它是一个静态类,它给系统提供的就是一个“单例”类。

   

  只不过Console是一个类型,而不是对象,缺点就是无法作为对象赋值和传递。如果系统中需要的“单例”就是一些功能,涉及不到对象的赋值和传递,完全可以用静态类实现,没必要非得用单例对象。

  对象的单例模式,关键在于限制类型的构造函数,不让使用者随意new一个新对象,且看代码:

  

  重点:将构造函数设置为private,只能内部调用;用一个静态字段来存储对象。

  可见,无论单例是类型还是对象,都需要通过“静态”来实现。

6.4   情况3:复杂对象

  创建一个新对象时,一般需要初始化对象的一些属性。简单的初始化可以用通过构造函数和直接赋值来完成。

  

  但是如果一个对象的属性过多,业务逻辑很复杂,就会导致复杂的创建过程。这种情况下,用构造函数是不好解决的。如果用直接赋值,就会导致大量的if…else…或者switch…case...的条件判断。这样的代码将给系统的维护和扩展带来不便,而且如果不改变设计,会随着维护和扩展,会出现更多的条件判断。随着代码量的增加,维护难度更大。如果再是多人同时维护,那就麻烦了。

  

  显然,这样的代码不是我们所期望的。设计上也不符合单一指责原则、开放封闭原则。所以,对于一个复杂对象的创建过程,我们将考虑重构。

  我们把对象创建的过程抽象出来,做成一个框架,然后派生不同的子类,来实现不同的配置。将复杂对象的构建与其表示分离,这就是建造者模式

  

  上图中,我们最终要创建的是Product类型的对象,Product是个复杂对象。如果直接new一个对象,再赋值,会导致大量条件判断。

  所以,我们将对象创建过程抽象到一个Builder抽象类中,然后用不同的子类去实现具体的对象创建。这样的设计相比之前大量的if-else-代码,优势是非常明显的,并且符合单一职责原则和开放封闭原则。应对需求变更、新功能增加、多人协同开发都是有好处的。

6.5   情况4:功能相同的对象

  最经典的就是数据操作。创建一个用于SQL server的SQLDBHelper类,又创建了一个用于Oracle的OracleDBHelper类,这两个类所实现的功能是完全一样的,都是增删改查等。如果这两个类是孤立的,那系统数据库切换时候,将导致SQLDBHelper和OracleDBHelper两个类之间的切换,而且改动工作量随着系统复杂度增加。

  

  而且如果增加一个数据库类型,也会导致系统代码的大量修改。

  

  这个问题的根本是违反了依赖倒置原则。客户端应该依赖于抽象,而不是具体实现。我们应该把数据操作的功能抽象出来,然后通过派生子类来实现具体。

  

  这样设置之后,我们创建对象的代码就会变成:

  

  面对不同的数据库,我们需要判断并创建不同的实现类。

  

  可以把这段代码封装成一个方法,这就是一个简单的“工厂”。所谓工厂,就是封装一个对象创建过程,对于一种抽象,到底由哪个具体实现,由工厂决定。

  

  这是一个简单工厂模式。另外,工厂方法模式抽象工厂模式也都是在这个基础上再去抽象、分离,而出来的。

6.6   总结

  对象创建并不是new一个类型这么简单,以上四种情况在日常开发过程中应用也都比较常见。

  上面通过对象创建过程的种种情况,随之介绍出了:原型模式、代理模式、建造者模式、工厂模式。虽然现在还不能完全了解这些模式的细节,但是至少明白了这些模式应对什么问题,有了明确的定位。而这才是最关键的,有了定位,有了高层次的理解,再看细节就变得容易多了。

 

后文继续,敬请期待!

---------------------------------------------------------------------------------------------

继上一篇换种思路去理解设计模式(上)继续讲。如果没有看过上一上一篇博客,请先点击看看。要不然直接看这篇会找不着头绪。

7   多个对象组成结构

7.1     过程描述

  上一节介绍了如何创建一个对象。但大多数情况,一个对象是不够用的,这时候就需要把对象包装、封装、多对象组合。有时候还需要将一个组合作为一个整体使用,组合要提供对外的接口,也可能会用到系统原有的接口。

  下面针对每种情况详细介绍。

7.2     情况1:借用外部接口

  有开发经验的人知道,日常大部分开发都是在已有系统基础上开发的。即便是从新开发的系统,也要依赖于一个框架或者库。

  所以,我们每时每刻都在用系统已有的接口。但是如果这些接口不满足我们的需求,我们就需要重新对接口封装一下,让其符合当前的规则。就是这个我们日常用的技巧,被GoF总结成为一个模式——适配器模式

  不用看代码和类图,也能明白它的意思。不必太计较代码和类图的细节,重点在于理解设计思想。

  顾名思义,适配器就是做一个隔离,起到了解耦的作用。例如我们日常用的笔记本电脑适配器。

7.3   情况2:给对象增加新功能

  系统总是在不断的维护和升级当中,也可能在不断的需求变更当中,因此为对象增加新功能,是再常见不过的了。那么如何为对象增加新功能呢?

  最直接的回答就是改代码呗。改类型的代码,增加方法、属性等。

  

  对于这种修改,首先想到的应该是违反了“开放封闭原则”和“单一职责原则”,违反这种原则带来的坏处很多。代码越改越多;每次更改都有可能影响以前代码;多人维护一个文件,不利于协同开发……

  

  如果用“抽象”“隔离”的思想来思考这一问题,很容易就能找出思路:第一,把原有功能和新增功能隔离;第二,两者都依赖于一个抽象,这个抽象就是对象应该有的所有功能;第三,外部客户将依赖于抽象,它不会察觉内部的变化(依赖倒置原则)。

  这就是装饰模式

  

  从上面的类图看,ConreteComponent是原始类型,Decorator是一个抽象类,它的派生类负责添加新功能。这里理解的重点,在于Decorator类中有一个Component属性,相当于Decorator封装了一个Component,直接调用他原有的功能,并且可以新增功能。当然,这些操作都是可以派生在子类中实现的。而且不同的子类可以实现增加不同的功能。

  这样的抽象和分离就符合开放封闭原则和单一职责原则,也不会出现代码量过多、多人维护不便等问题。

7.4    情况3:封装功能

  对于有些功能,我们不希望客户端直接调用,而是在调用时候先做一个判断,或者加一个缓存。其实就是在真实功能和客户端之间,加一个中间层。而这不能让客户端调用察觉。

  如果你把这个中间层直接加入到真是功能中,虽然这可以不让客户端察觉,那将会给系统带来隐患,违反“单一职责原则”。如下:

  

 

  首先,如何不让客户端察觉?答案很简单——依赖倒置原则——让客户端依赖于一个抽象。这个抽象将如何实现呢? 具体的实现和中间层都要去实现。如下:

  

   类图如下:

  

 

  这就是代理模式

  每个设计模式要体现的都是一种设计的思路,代理模式就是要在客户端和底层实现加一层,在该层中实现一些业务场景。4s店就是客户于汽车厂家的代理。

  具体是否要都去实现同一个接口,这种细节不重要,不要去过于纠结这些类图和代码。

7.5    情况4:递归关系的组合

  上文提到如何更有效率的维护对象功能和新增功能,以及更有效率的封装对象。这两种做法的输出,其实还是一个单个的对象。如何将一个个对象组合成一个视图,系统中最常见的无非是两种——列表、树,以及两者的结合体——TreeGrid

  列表是比较简单的结构,按实际的需求应用,不会产生太多误解。而树结构却有值得讨论之处。最简单的树节点实现的代码如下:代码很简单,只有节点的名称,和对代码下级节点的管理。

  

  如果我们应对的业务很简单,例如类似于windows系统的文件夹树,即每个节点的类型都一样,每个节点的功能也都一样,叶子节点和摘要节点在功能上没有区别。这种情况下,可以用以上代码轻松应对。

  但是如果遇到以下情况呢,如下图:

  

  这也是个树结构,但是每个节点类型都不一样,形式的功能也不一样,“个人”是个叶子节点,不能再添加下级节点。这种情况下,再用以上那段代码就会出现许多问题,如多个功能集中在一个代码文件中,多人维护一段代码等。

   如何解决这一问题,组合模式给予我们灵感。

  

  根据以上类图,可以看出组合模式解决这一问题的思路是:将树结构中的节点的统一功能抽象出来,不同类型的节点,用不同的子类去实现。类图中只有两个子类,我们可以根据自己的实际情况来派生多个子类。

  这样解释想必大部分人都能理解该模式的设计思路,不必再用代码挨着表达了。关键在于理解如何分析问题,如何抽象,如何隔离,如何解耦,最终就是如何设计。

  这样设计符合开放封闭原则、职责单一原则,对于客户端也符合依赖倒置原则。

7.6   情况5:分离多层继承

  在对象组合过程中,难免会出现继承的情况,甚至会出现多层继承。根据设计原则——少继承、多聚合。因此不建议我们使用多层继承。而是尽量把这种多层继承的关系,变成聚合的关系。

  在一个多层继承结构中,如果底层节点可以抽象出相同的功能,即可变为聚合关系。如:

  

  如上图,子类可以提取出一个抽象。变成这样的设计:

  

  这样就把多继承变成了聚合。

        

  可以总结归纳以下这种情况。我们把左侧“发送消息”及其子类叫做“抽象”,右侧的“发送方式”及其子类叫做“实现”。那么我们现在做的就是从“实现”和“抽象”的继承关系,变成了两者的聚合关系。

  这就是——桥接模式。以下是类图:

  

  他应对的场景是抽象有多样性,实现也有多样性。抽象的抽象只依赖于实现的抽象。从而解耦抽象和实现的关联。

7.7   情况6:封装组合,供外部使用

  当一个组合封装完成后,要提供统一的接口供外部客户端使用。而不是让客户端在组合内部任意的调用。

  这就是外观模式。很好理解,也经常用到,可能只是不知道这个名字而已。它像一个包袱一样包起来组合,只留规定的接口。

  

  外观模式简单易懂,不需要类图和代码过多解释。

7.8   总结:

  (注:未包括“Flyweight享元模式”。将在后续版本更新中加入。)

  其实以上这几种情况,就是结构性的设计模式对应的问题,每种情况对应一种设计模式。结合自己或多或少的开发经验,仔细考虑分析这几种情况,肯定每种情况都是你在编码中遇到的,也是一个对象组合很可能需要的。

  遇到这些问题时,你当时是怎么解决的?不一定非得按照设计模式上的解决方式。但是要已定符合设计原则。设计模式只是一个“术”,提供一个解决思路或者灵感,而设计原则、设计思想才是“道”。

 

  特别说明:

  上、中两篇其实是我已经整理好的资料,直接复制粘贴到这里的(从word中往这里面贴图挺麻烦),因此更新比较快。这些资料是我从5.12开始,每天都花一小时时间整理的,算来也两周了。下一篇资料现在刚刚开始整理,估计不会很快就更新上。因为我需要把剩下的设计模式挨个分析,然后串起来,不容易。有需要的朋友得耐心等两天,但是我肯定会在发出来。

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

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

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

 

8       对象行为与操作对象

8.1     过程描述

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

l  操作过程:

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

l  操作的对象或组合:

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

l  受影响的对象或组合:

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

 

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

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

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($->@)


1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看REaDME.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值