换种思路去理解设计模式(上)
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:借用外部接口
7.3 情况2:给对象增加新功能
7.4 情况3:封装功能
7.5 情况4:递归关系的组合
7.6 情况5:分离多层继承
7.7 情况6:封装组合,供客户使用
7.8 总结
8. 对象行为与操作对象
……
下一篇:换种思路去理解设计模式(中)