读书笔记-设计模式-可复用版-5种创建型总结
五种设计创建型设计模式:
1.原型设计模式 Prototype
2.单例设计模式 Singleton
3.工厂方法设计模式 Factory Method
4.抽象工厂设计模式 Abstract Factory
5.建造者设计模式 Builder
单例模式和原型模式都是创建自身的对象
而工厂方法,抽象工厂,建造者都是创建的第三方对象
以前面试的时候,就经历过提问,什么是工厂方法和抽象工厂,以及抽象工厂和建造者模式有什么区别。
在使用过程中,要注意,没有哪种设计模式是完美无缺的,都会有两面性,我们能做的就是通过设计模式的应用,提高复用性,扩展性,灵活性,将耦合性降到最低,一点不耦合是不可能的:)
尤其是在面对复杂的需求时.
当我们开始实现需求时,一定要记得,能用组合,优先使用组合,面向接口编程,而不要面向具体的类。
概念定义及说明:
================================
1.原型设计模式-Prototype
定义:
通过克隆(Clone)原型来创建新的实例
原型是指我们要克隆/复制的对象,已经在内存中,通常是完成了初始化的对象
作用:
通过克隆原型来创建新的实例,提高了代码的复用性,不需要重新去加载资源,一系列可能比较耗时的初始化工作,直接克隆/复制成果,并对不同的地方再进行调整,提高开发以及代码的运行效率
比如,游戏中出现的很多敌人,数值属性状态存在很多的相似性,我们可以先创建好一个敌人原型(模板),通过克隆模板来创建多个敌人
Unity中的Prefab就是原型的一个很好应用,将“组装”好的对象,序列化成字节流,以文件的形式存放在磁盘上,使用时,通过读取文件进行反序列化,将字节流还原成对象,具体的参数属性在Prefab中通常是已经计算好的,在反序列化以后再对不同的参数进行修改调整,并且在游戏中,我们也经常通过克隆已经“配置”好的对象,来创建新的对象
原型模式可以说已经深入到语言及标准设计层面了,就像迭代器模式已经是语言的一部分了,所以我们在使用过程中,可能会感受不到他们的存在感,实际上,他们一直在起着很重要的作用。
注意事项:
浅拷贝(Shadow Copy)深拷贝(DeepCopy)
克隆原型一定会面临的两个问题,产生的原因在于值类型和引用类型在内存上存储的区别
java和C#均是基于“环境”的语言(虚拟机和CLR),在C/C++等底层语言的基础上,将很多工作进行了简化,克隆方法Clone,在Java和C#中,均定义成了系统级的接口,用户无需自己定义
MemeberwiseClone是Object类中提供的用于“浅拷贝”的API,逐成员的复制,如果要克隆的原型中包含其它引用类型,则需要使用深拷贝(DeepCopy),避免引用指向堆中同一块内存地址,另外,引用类型比较多或是循环引用,建议通过序列化和反序列化的形式解决,在实际情况 ,复杂的对象,手动的进行Clone操作是不现实的
关于序列化和反序列化,如果不能够替代,还是要利用他的特性,当然,任何耗时的操作,都不能放在Update中按帧执行
================================
2.单例设计模式-Singleton
概念:
保证当前类的实例,在整个程序的运行周期中,有且仅有一个实例,并提供一个访问它的全局接口。
单例最简单,但应用也最为广泛。
只要类的对象(职责)是独一无二的,均可以采用Singleton模式
需要注意的地方:
1.实现Singleton单例模式,需要注意什么?
1) 保证当前的实例,在内存中,有且仅有一个,不可以直接进行new创建对象,构造函数是私有的,提供一个静态的公共接口用于访问唯一的实例
2) 不可以被继承,这样会导致实例不唯一,所以类通常都是设置为sealed
3)多线程环境下,需要处理同步问题
2.实现多线程下,Singleton线程安全有几种有哪些?
常见的形式有:
1)single check lock
2)double check lock
3)not quite as lazy
4)full lazy
5)System.Lazy<T>(.Net 4.0(or higher)
3.多线程环境(并发)下,数据不同步是如何造成的,如何解决?
为了CPU和编译的利用率,提高性能,CPU和编译器会对指令进行重新排序(指令重排reorder),这会引起代码的编写顺序和实际内存的读写顺序是乱序的,单线程环境下是没有问题的,但在多线程环境就会引起数据不同步,导致结果不正确
比如你在编写代码的时候,先修改A,再修改B,但内存处理可能并不是按照这个顺序的,可能会调换位置,并且修改的值可能一直保存在了寄存器中,没有更新到缓存或是主存,这样其它线程读取的时候,并不能保证每次读取到的都是新值!
解决办法是通过添加内存屏障Memory Barrier
Memory Barrier就是一条CPU指令,他可以确保操作执行的顺序和数据的可见性
1)保证执行顺序
2)保证数据的可见性
这两点就可以解决多线程的同步问题,确认了执行顺序,值被修改以后也会立即的更新到内存中,保证了下一个线程读取到的值是新的。
相当于告诉 CPU 和编译器先于这个命令的必须”先“执行,后于这个命令的必须”后“执行。
内存屏障也会强制更新一次不同CPU的缓存,会将屏障前的写入操作,刷到到缓存中,这样试图读取该数据的线程,会得到新值。确保了同步。
C#中,Memory Barrier的API:
Thread.MemoryBarrier();
3.什么是lock和deadlock,如何模拟一个deadlock?
lock语句块用于解决多线程环境下,数据不同步的问题,保证当前只会有一个线程进入到lock语句块中,其它执行线程只能等待lock释放,实际上lock(){ }语句块,隐式的执行了Thread.MemoryBarrier();
deadlock是死锁,这是在使用lock语句块时要特别注意的问题,具体lock(xx)所以获取对象锁,有具体的使用规则,可以见详情的解释
模拟deadlock:
Thread thread = new Thread(new ThreadStart(DeadLock1));
thread.Name = "thread_1";
Thread thread1 = new Thread(new ThreadStart(DeadLock2));
thread1.Name = "thread_2";
thread.Start();
thread1.Start();
public void DeadLock1()
{
lock ("A")
{
Debug.Log(Thread.CurrentThread.Name + " get lock A");
lock ("B")
{
Debug.Log(Thread.CurrentThread.Name + " get lock B");
}
Debug.Log(Thread.CurrentThread.Name + " release lock B");
}
Debug.Log(Thread.CurrentThread.Name + " release lock A");
}
public void DeadLock2()
{
lock ("B")
{
Debug.Log(Thread.CurrentThread.Name + " get lock B");
lock ("A")
{
Debug.Log(Thread.CurrentThread.Name + " get lock A");
}
Debug.Log(Thread.CurrentThread.Name + " release lock A");
}
Debug.Log(Thread.CurrentThread.Name + " release lock B");
}
创建两个线程,分别执行DeadLock1,DeadLock2两个方法,运行结果是:
thread_1 get lock A
thread_2 get lock B
产生死锁!
解释下死锁的产生:
假设线程一先执行,线程一执行了DeadLock1,进入方法内部,lock("A")//获取引用对象“A"的锁,这时候,另外一个线程也执行了DeadLock2,lock("B")//获取引用对象“B"的锁
A和B均被锁住(locking)
假设A先继续向下执行,执行到lock("B"),但此时B被线程二锁住,线程一处于等待,
线程二继续执行,执行到lock("A"),但A被线程一锁住,尴尬情况就出现了,线程一在等待
线程二释放B,而线程二在等待线程一释放A,就这么僵持,死锁!
打麻将的时候,也是会出现死锁的情况,我胡三条,他胡八万,我这里有3个八万,不可能拆,他家里有三个儿三条,也不可能拆,这也是一种死锁
5.lazy和greedy的区别
这是指类构造器的初始化
lazy initilizer,可以翻译为延时初始化,或是懒汉初始化,相应的也会有lazy load
lazy的意思是:只有在我调用的时候,我才去初始化它!
不调用的时候,他就一直处于未初始化的状态。
Greedy饿汉式,只要我引用类中任意一个静态成员,调用之前,静态字段就会分配内存,占用内存。lazy是只有使用到我的时候,我才会创建分配。
6.beforeFieldInit是什么?
beforeFieldInit可以对问题5有更明显的解释
这是一个.Net中关于类型构造器执行时机的问题,有两种方案:
beforefiledinit(默认)
precise
这两个模式的切换只需要添加一个static构造函数即可,存在静态构造函数则是precise方案,
没有static构造函数则是beforefieldinit方案
7.single-lock check和double-lock check的区别是什么,为什么要double-check?
single check lock是比较常见的多线程环境下线程安全的Singleton模式,double-check lock只是single check lock的一种优化版本,避免每次获取实例都要lock.
double-check lock(DCL)问题,在java下是无法执行的,需要使用voliate,也是因为指令重排导致,但C#也有指令重排,但可以正常的运行,这里我没有去深入研究,先记得在Java中使用时,要注意DCL问题
其实使用中的注意事项:
1)单例模式的实现,建议使用泛型,避免创建重复的代码
2)具体选择哪种方案,not quite as lazy就可以了,但实际的使用中,每一种
都是可以的,性能差别微乎期微,因为你不可能将他们放在update中按帧执行,如果是这样,为什么不cached呢
================================
3.工厂方法设计模式 Factory Method
概念:
定义一个用于创建对象的接口,让子类决定,实例化哪一个类。Factory Method 使一个类的实例化,延迟到子类。
讲解工厂方法,必须要提到,参数化的工厂方法(有些资料叫简单工厂)
参数化的工厂方法是通过定义一个单独的工厂类,提供一个统一的方法,通过参数,可以是string,enum....通过switch case / if else 返回不同的对象实例。
缺点是耦合性高,每次新增或是修改新的对象都要对工厂类进行修改。(有些复杂的需求也是需要用到的,但可以进行优化,比如使用哈希表,配置表,尽量不要使用字符串(没有安全检查)等等)
通常采用面向接口的工厂方法,将对象的实例化,放在不同的工厂子类中实现,使用时,面向接口编程,修改和新增都不会影响到其它对象
================================
4.抽象工厂设计模式 Abstract Factory
概念:
提供一个创建”一系列“相关或相互依赖对象的接口。而无需指定它们具体的类。
工厂方法是提供一个创建对象(一个)的接口,而抽象工厂则是提供的是一系列产品创建的接口
可以说,抽象工厂是工厂方法的集合,这些工厂方法所创建的对象之间是相关的,通常是一个产品的完整系列
比如不同的UI显示风格,室内的装修风格,样式style等等
一个抽象工厂创建了一个产品的完整系列,如果我们需要改变风格,只需要替换
具体的抽象工厂派生类,这样整个系列的产品都会被改变,系列中的相关的每一部分,都定义在抽象工厂类中。
Abstract Factory抽象工厂在实现中一些说明:
1.一般每个系列的产品只需要有一个ConcreteFactory,对于这种独一无二的对象,我们可以设置他为Singleton单例
2.抽象工厂中,只声明一系列创建产品的接口,具体的创建是由ConcreteProduct子类实现的,这里注是FactoryMehod 工厂方法的应用。
3.抽象工厂中,不会指明具体的类,你看不到ConcreteProduct,有的只是抽象或是基类Product,具体由抽象工厂的派生类指定哪种产品!
在之前文章的例子中,有提到开一家咖啡馆,要决定装修的风格,风格决定了内部的很多布局,设施,装潢的改变,通过定义抽象工厂(里面包含了创建这些对象的工厂方法),由不同风格的派生类实现,改变风格只需要替换具体的子类即可!
如果我现在需要将风格的实现配置化,这种情况可以将风格存放在一个哈希表中,通过字符串可以快速的读取,避免的使用反射,这种形式类似于参数化的工厂方法
当需求变得多变复杂的时候,每种设计模式的缺点都会暴露出来,所以要合理的去使用它们。
================================
5.建造者设计模式 Builder
通常是用于”构建“复杂的对象,并强制一步一步构建的过程,来生成复杂的对象。
概念:
将一个复杂的对象构建和它的表示分离。使得同样的构建过程,可以创建不同的表示。
Builder类是建造者模式的核心,里面包括了构建产品的所有接口(每一个环节)。但Builder通常并不生成最终的构建结果,最终的构建我们通常是放在Director(主管或导演)中
可以说是将构建过程和构建结果再次分离。
需要注意的是:
Builder 只提供构造一个成品的每一步操作,但并不包含构建最终有产品,比如你想要组装一个自行车,Builder提供了组装一台自行车所需要的所有部件,但至于你如何组装,如何变速,车身结构,颜色等等,这是由Director负责的。
这里举一个例子:
我有一个怪物Monster的对象(Product),他可以包含头,眼睛,嘴,耳朵,手,脚等等很多部分,但怪物的设定,可以是很随意的组合,可以像人,也可以是四不像
比如我需要一个似人的怪物,一个长着三只眼睛两条腿的,一只眼睛一只胳膊一条腿的,两个头三张嘴四只脚的......你会发现,组合是多样化的
通过建立Builder类,我们将实现构建Monster的每一个环节(添加头,脚,眼睛,腿等),上面提到过,Builder只包含怪物的每一个环节,步骤,但最终构建成什么样子,这需要放在Director中构建。
Builder只提供构建最终产品所需要的每一个环节。不包含最终构建的结果。
Android里AlertDialog是最为典型的Builder模式的应用,通过不同的组合,一步一步的构建出最后的对话框,并且AlerrDialog代码的设计风格非常的直观,并且组合是自由的,参数之间没有特定的顺序关系,先设置title,后设置title都是不影响的。
Builder和Abstract Factory有什么区别?
主要区别是:应用场景
两者十分相似,但Builder通常是用来构建复杂的对象,并且强调是一步一步的构建,而抽象工厂着重于创建多个系列的产品对象,没有复杂的构造过程。比如不同的UI显示风格,室内的装修风格,游戏的换皮:),主题theme,样式style等
Builder建造者模式,则是用于创建一些比如对话框,插花,关卡设计,涉及多种不同组合,而且组合复杂多变化的情况
温故而知新,5种创建型模式虽然已经介绍完,但还需要在实际的项目中,多去使用,建立设计模式的意识,多应用,才会有更好的了解,比如思考在你当前的项目里,如何更好的应用到设计模式?
如果后面有设计模式比较好的应用,我也会分享上来~
感谢您的阅读, 如文中有误,欢迎指正,共同提高
欢迎关注我的技术分享的微信公众号,Paddtoning帕丁顿熊,期待和您的交流