读书笔记-设计模式-可复用版-5种创建型总结

Posted Paddington帕丁顿熊

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了读书笔记-设计模式-可复用版-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#中,均定义成了系统级的接口,用户无需自己定义



关于序列化和反序列化,如果不能够替代,还是要利用他的特性,当然,任何耗时的操作,都不能放在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种创建型模式虽然已经介绍完,但还需要在实际的项目中,多去使用,建立设计模式的意识,多应用,才会有更好的了解,比如思考在你当前的项目里,如何更好的应用到设计模式?


如果后面有设计模式比较好的应用,我也会分享上来~


以上是关于读书笔记-设计模式-可复用版-5种创建型总结的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript设计模式读书笔记=; 创建型设计模式

读书笔记iOS-设计模式

JavaScript设计模式读书笔记=; 行为型设计模式

JavaScript设计模式读书笔记=; 技巧型设计模式

JavaScript设计模式读书笔记=; 技巧型设计模式

大话设计模式读书笔记——原型模式