学习设计模式是重要的,在工作中运用好设计模式是能保持高维护性和拓展性的保证,学好设计模式对理解框架也是比较重要的,因为框架也有一部分是设计模式组成的
总体来说设计模式分为三大类:
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
这里记录一下自己对每个模式的方式和调用时机和意义的自己的通俗理解。
在这里说明一下,对象是指类实例化出来的变量,
单例模式的目的就是保证在程序运行的过程中至始至终,某个类只存在一个对象
在安卓开发等其他开发体系中需要注意线程的问题,所以写单例模式需要注意线程同步问题,但是unity主要就三种线程,主线程,工作线程,渲染线程,关于游戏的逻辑基本全部在主线程进行,工作线程和渲染线程都是用来做渲染上的任务的,所以unity的单例模式不需要担心线程同步的问题。
关于线程安全的单例模式可以参考 关于C#的Lock关键字实现线程安全的单例模式
单例模式的线程同步模式写法也可以参考设计模式(二)单例模式的七种写法
这里写下我最常使用的静态模式写法
public class Singleton {
static Singleton instance;
private Singleton(){}
public static Singleton GetInstance(){
if(instance == null){
instance = new Singleton ();
}
return instance ;
}
}
或者
public class Singleton : MonoBehaviour {
static Singleton instance;
public static Singleton GetInstance(){
return instance ;
}
void Awake(){
if(instance == null)
{
instance = this;
}else{
Destroy(gameObject);
}
}
}
上面说到的文章有一种写法这里介绍下,这种写法不仅可以在java上面允许,c#也是允许的
public class Singleton {
private Singleton(){
}
public static Singleton getInstance(){
return SingletonHolder.sInstance;
}
private static class SingletonHolder {
private static final Singleton sInstance = new Singleton();
}
}
上面这种写法从语法上 保证了多线程的安全性,SingletonHolder是个私有内部静态类,
关于C#的静态类的特性这里有很好的说明 C#静态类
在这里用到的特性是,静态类不能有实例,私有静态类的特性保证了Singleton这个类不管有没有实例对象,有几个实例对象都只有一个SingletonHolder 的类,这些都是语法上面给到的保证, 由于只有这一个类不管是不是多线程同步访问,都只是返回一个实例对象。
这里额外话说下C#静态类还有个特性就是可以对已知类做拓展,比如声明一个静态类B,对已知类A进行拓展的时候,可以这样写
static class B{
void mm (this A a, int k)
{
}
}
这样类A相当于多了个方法,可以这样调用
A a = new A();
a.mm(1);
建造者模式,按照我的理解就是对于一些比较类似并且过程比较稳定代码变化不大但是有一定复杂度的任务,按照细致程度不同或者环节上的不同,通过不同的类进行定义实现这些环节,当然这些环节之间没有关系不会互相影响, 以这些不同环节的类自由组合来达到实现这些相类似的任务的目的。
例如写程序需要得到一个汽车类的对象,对于外部调用者来说,只需要传一些参数例如牌子,车型,配置就可以得到一个汽车类的对象。但是我们可能会需要很多的汽车例如WEY的suv,传祺G8等,这些车基本上建造过程比较类似,并且在长期过程中车辆的制造流程和技艺变化不会出现明显的改变,就可以考虑使用建造者模式,车的制造涉及部件的制造 各部件放一起的组装,组装顺序等:
对于车的每个部件都定义成一个类, 并且定义一个车的基类A,A里面还有各个部件的变量引用其部件对象,
然后各个部件的组装流程定义到一个基类B里面,基类B里面有A的引用,对A引用里面的各个组件对象进行赋值操作,有赋值操作1 赋值操作2 赋值操作3等,例如选择车轮类型并安装到A,选择车顶类型并安装到A
然后关于组装顺序的设定定义到一个基类C里面,C里面有B的引用,C里面对B的组装操作1 2 3等方法的执行顺序进行定义,例如操作顺序是321或者123等,先装车轮再装车顶等
基类定义大多数制造流程的相同的地方,然后不同的车再继承自基类,重写一些不一样的地方,就可以实现建造者模式了
参考文章:一篇文章就彻底弄懂建造者模式(Builder Pattern)
这样代码的复用性比较高,如果新增了一个汽车产品,对于其制造过程的不一样的部分,对相应的类进行继承并改造即可.例如同样的组装顺序类C,对不同的组装过程B1和B2都适用,则将B1类或者B2类的对象分别作为某个C类的对象的内部引用实例,然后进行建造即可
观察者模式就是对某个对象的某些举动进行观察,一旦某个对象有该举动,就会采取措施,观察者模式用于对一些时机不确定的行为进行监听。这种操作能够降低对象之间交互的耦合度
观察者模式在很多框架设计中用到还是比较多的。
例如微信用户关注公众号之后,一旦公众号有消息,就会通知用户。
例如游戏里面很多系统会对网络加载主角信息的初始化完成进行监听,一旦初始化完成,这些系统也会接下来进行一些动作。
设计模式(五)观察者模式
Unity中的UI组件的各种AddEventListener也是观察者模式的一种写法
UnityEvent也是属于观察者模式
最普通的做法是将观察者对象作为引用存储在被观察者对象的某个变量里面,在事件发生的时候就将这些存储的对象挨个通知 就如上面这篇文章
介绍一种改良版本 自定义低耦合观察者模式, 这种版本用法非常简单,看半分钟就懂,写法也是非常简单,而且耦合性比普通做法低不是一点点,这种版本更像观察者模式和中介者模式的结合,中介者模式在下文会介绍到
简单来说,代理模式是指继承自相同接口或者基类的两个类A和B,B的类定义里面持有A对象的引用,B与A的方法都是相同的,B类在定义的时候某些方法调用的是A对象引用的相同方法,程序访问A类的对象是通过B的对象进行的。 这种时候,B代理了A。代理模式通常用在设计者不希望外部能够直接访问某些对象的时候,代理模式来不对被代理对象进行一些功能的扩展。
上述的黑体字的做法并不是代理模式定死的做法,也就是说代理类和被代理类不一定要继承自相同接口或者类,这里只是为了举个实例做法。只要实现了访问某个对象必须通过另一个对象,就算实现了代理模式。
设计模式(六)代理模式
装饰模式,继承自相同接口或者基类的两个类A和B,B的类定义里面持有A对象的引用,B与A的方法都是相同的,这些相同的方法中,B类在定义的时候不仅调用A对象引用的相同方法,B的这些方法内还会写对A对象做一些额外逻辑处理来达到对A进行扩展的目的。
外部参考设计模式(七)装饰模式
例如A类和B类都有方法c,B类的方法c有除了对内部对象a的c方法调用外还有一些拓展,现在有A类的对象a1 和 a2和B类的对象b,当B类的b对象内部持有的仅仅是a2的时候,调用b的某个方法c只是对a2有起作用,而对a1是没有影响的,如果使用直接改写A类的方式来达到与上述装饰模式相同的扩展效果,则有改到所有A类对象的风险,如果将传入B类的A类对象改成其他对象,则对a2的扩展功能即可撤销。
装饰模式在一些情况下可以代替使用继承方式来扩展一个类,使用场景包括:
优点
缺点
中介者模式,简单来说就是开发者在设计中让自己希望的某部分或者全部类之间互相的方法调用通过一个中介者类来实现,例如原本程序中有A类B类和C类,他们之间都有互相之间的对象引用,例如A类有B和C的引用,这样A与BC之间就有直接耦合,这时改成增加一个中介者M类,ABC之间都持有一个M类的相同对象引用,ABC的方法调用M类对象的方法给M类传送消息,M类持有ABC等的引用,在这些M类被调用的方法中,M类负责具体的和其他类的交互。 这样ABC类就解耦了其他类,只是和中介者M类耦合。这样类之间的交互结构由杂乱无章的变成了星形的结构,星的中心是中介者。中介者模式一般用于程序已经形成一定规模之后,是pureMVC,StrangeIOC框架的核心
设计模式(十四)中介者模式
原型模式,简单来说,开发者根据开发需要,对于某个在程序中可能大量存在实例的类A,可以考虑 (通过继承自己语言的某个接口)使得类A的实例a可以通过某个方法拷贝一个类A的新实例a1,a1内容和a完全一样,这种拷贝操作完全是比较节省性能的, 这样类A实例的创建就绕过了实例化这个耗时的步骤。 这种拷贝操作绕过了构造函数的步骤,客观来说,拷贝出来的实例对象内容和原对象是完全一样的,实例对象内容不包括静态类型的变量和方法因为那不属于实例对象而是属于类,换句话说,所有该类的实例对象共享相同的静态变量和方法。
拷贝操作分成浅拷贝和深拷贝,两者各有用途。浅拷贝和深拷贝是对于实例里面的引用类型来进行区别定义的,引用类型就是类的实例变量,区别于基本类型值类型。
在不同语言中值类型和引用类型的定义范围有一些差别,开发者需要了解好自己开发所用语言的值类型和引用类型范围,使用原型模式的时候遇到问题才能更快排错。
浅拷贝的定义是如果被拷贝的实例对象a内的某个变量引用c指向某个类B的实例对象b的情况下,拷贝出来的实例对象a1的这个变量引用c依然指向原来的b.
深拷贝的定义是如果被拷贝的实例对象a内的某个变量引用c指向某个类B的实例对象b的情况下,拷贝出来的实例对象a1的这个变量引用c依指向的是新的B类的实例b1,这个b1的内容和b完全一样。
开发者可能没注意到深拷贝和浅拷贝的区别,用浅拷贝根据a对象复制出来了a1 a2等,但是修改里面的某个引用类型变量的时候,才发现一改全改了。
C#提供了ICloneable的MemberwiseClone方法与Clone方法给开发者继承来实现原型模式,其中Memberwise负责浅拷贝,Clone负责深拷贝。
原型模式介绍参考链接:
设计模式(十六)原型模式
c#原型模式使用参考链接:
C#之MemberwiseClone与Clone
C#中4种深拷贝方法介绍
我们可能在写程序的时候会遇到这种情况,某个类的对象有若干种状态,并且带有若干种操作,这若干种操作在每种状态中内容都是不一样的。如果这若干种状态和若干种操作写到一个类里面通过switch语句甚至嵌套switch语句进行判断,如果操作内容比较复杂, 则代码会看起来有些复杂,维护性不是很高。
例如MP3有若干种状态,开机 关机 待机 休眠 等,同时MP3有播放 暂停 上一首 下一首 调大音量 调小音量 开启 关闭等操作,一般写法是,每个操作都定义一个方法,然后方法里面判断状态,根据不同的状态执行不同的内容。
这时状态模式可以了解一下,在状态模式中,上述的每个状态如开机 关机 待机 休眠等都定义成了一个独立的类,每个类都继承自基类MP3类,MP3类定义了开启 关闭 上一首 下一首 等函数,子类根据自己表示的状态重写这些函数,总调用对象A持有一个MP3类的引用,A在合适的情况下根据当前的状态将MP3变量引用不同的状态类, 例如A在被外部函数调用开机函数的时候将MP3变量引用到开机类的对象。A在调用操作的时候直接调用引用的对应操作即可。
对于日后操作变得日益复杂的情况下,这种写法相比普通写法,可读性和维护性会越来越明显。
状态模式和装饰模式的操作过程有些类似,都是一个对象A里面的引用持有另一个对象B,并且A调用函数的时候调用直接调用B的功能名称相似的函数,不同的地方是,装饰模式中,A调用的时候不仅调用了B的,还进行了一些额外的操作,但是状态模式中,一般没进行额外操作,并且状态模式的B代表的是某个状态,两者写法有些类似但是目的是不一样的。
参考链接
设计模式(十五)状态模式
享元模式的目的主要是为了通过减少创建对象实例的方式节约内存。
减少创建对象实例的方式是通过对象池和复用共用对象的方式。
对象池简单来说就是将不用的对象存储起来而不是删除,需要的时候直接获取而不是再创建,真没有不用的对象再执行创建。
复用共用对象通过设计外部状态和内部状态来实现。
在一些文摘中,内部状态是对象可共享出来的信息,存储在享元对象内部并且不会随环境的改变而改变。而外部状态是对象依赖的一个标记是随环境改变而改变的并且不可共享的状态。
根据文摘中的含义和对享元模式实现的理解,我认为内部状态是对象池对于相同实例对象的寻找和获取标识,实例对象有一些用于自己接下来需要实现的功能的必要变量,在对象池获取到实例对象之后,这些必要变量由外部调用函数传递给实例对象,这些必要变量称为外部状态。
对象池通过共享的不随外部环境改变而改变的内部状态来找到符合的实例对象,找到之后再根据实例对象的实际使用环境设置随环境改变而改变的外部状态。在外部状态设置完了之后,就可以进行使用实例对象了。
在程序中需要用到比较多类似但是不完全相同的对象的时候可以使用享元模式,
享元模式与单纯使用对象池相比,类设计的时候需要考虑为了复用而引入的外部状态和内部状态, 享元模式的缺点是提高了代码的复杂性。
参考链接
享元模式
设计模式(十二)享元模式
这个模式简单来说就是在父类里面定义好总体的流程的各个方法以及流程里面的分支控制,在子类里面对各个方法进行细节控制等操作以及确定分支是否执行,是比较充分的利用了面向对象的继承特性。如果项目里面当前或者以后会有多个类似的类,可以考虑使用模板方法模式,把公共流程部分提取出来作为父类。
例如,造轿车和造SUV以及大多数造车的流程大体相同,都继承一个Car类,Car类对造车流程进行了总的结构定义,轿车和SUV对结构里面的一些细节根据自身情况进行调控,以及根据自身情况判断流程里面的一些分支是否需要执行。如果后面准备造货车或者巴士,则大体也是继承这个Car类后,再重新根据货车或者巴士的情况进行细节操作。
这个模式的缺点是,避免了代码的重复。
这个模式的缺点是,如果有太多相似的子类,父类的结构也许会变得比较复杂,并且拓展父类的时候需要考虑到拓展对每个子类的影响
参考链接
设计模式(九)模版方法模式
假设现在要求做一个到医院看病的功能,看病包括挂号,就诊,缴费,取药等步骤,单一职责原则要求我们对维护性有要求的情况下尽量不要将这些功能写到一个类,所以我们将每个步骤写成一个类,这种情况下如果外部需要进行看病功能,还要自己对这些步骤功能进行逻辑组合,每次都要重新处理这相对固定不变的组合过程不仅赘余还有可能出错,对于维护性也不是好的写法。
对于一些将逻辑有关联或者功能相似的一些功能类,他们互相协作可以完成更大的功能,如果外部只是松散的依次调用这些类,会造成一些重复的调用组合代码还容易出错。
外观模式用来处理这种情况,对于这些类,外观模式的做法是用一个外观类将这些作为变量引用,对外提供这些功能类组合而成的更大功能的接口,而自己对于完成这些功能的逻辑组合处理内部进行处理好。
在外观模式中,对于上述的情况,我们写一个接待员类,接受患者对象之后,里面每个步骤接待员都处理好了,这样外部调用比较方便。
参考文章 设计模式(八)外观模式
策略模式 :举个例子来说明,现在开发压缩类,里面有快速压缩,高效压缩,加密压缩三种选择,一般的做法是这三个选择各做成方法,由压缩时候进行分支选择。还有种做法是将这三种做法各做成一个类,每个类都有自己的压缩方法,方法里面根据自己的特性进行压缩功能, 主压缩类持有这三种压缩方法的一种,持有哪种由调用函数在调用时候决定,主压缩类在调用压缩的时候直接调用持有压缩方法对象的压缩方法即可。与前者相比,后者的维护和拓展空间明显广阔很多,后者的做法就属于策略模式的做法了。
策略模式是某个主类对于一些功能,有不同的执行手段,而每个执行手段都是一个策略类,主类开放外部自己的各种策略,由外部调用的时候进行决定使用哪种策略。
策略模式和状态模式的实施手法非常类似,这里说一下两者之间的一些区别,
策略模式主类持有的类变量引用的是各个代表策略的类,每个策略类之间没有关系,主类使用哪个策略类是由调用函数主动决定的开发者需要知道主类到底有哪些策略,每个策略有什么特点和意义,好在开发时决定什么时候使用哪种。
状态模式主类持有的类变量引用的是各个代表状态的类,每个状态类之间都有对描述相同功能的方法,只是方法里面的执行内容会根据自身的状态进行调整,调用者不能决定主类什么时候使用那种状态,状态切换在主类里面已经定义好了,不对外开放,开发者需要知道主类有哪些状态,状态特点,状态之间的逻辑关系。
使用场景:
1 如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
2 一个系统需要动态地在几种算法中选择一种。
3 遇到较多的选择语句时候,可以考虑换用策略模式。
参考文章
设计模式之策略模式和状态模式(strategy pattern & state pattern)
设计模式(十一)策略模式
写代码的时候,通常会遇到一个对象a请求另一个对象b完成某件事,一般写法是在a的方法里面调用b对象的某个方法,如果涉及到操作的撤销和重做则需要用命令模式会好一些。
命令模式是将操作抽象成命令,真正达到命令的功能的对象则放到这个命令里面作为执行者,上面的例子中,b对象提取到了命令里面作为命令的执行者,a对象作为命令的发起者调用命令,调用者直接执行某个命令。
因此,命令模式可以轻松实现命令的复用,和拓展以及命令的排队执行,撤销和恢复。
参考文章:
设计模式之命令模式
C#设计模式之11:命令模式
持续更新中