设计模式五杰

内容会持续更新,有错误的地方欢迎指正,谢谢!

前言

软件领域中的设计模式为开发人员提供了一种使用专家设计经验的有效途径。设计模式中运用了面向对象编程语言的重要特性:封装、继承、多态,真正领悟设计模式的精髓是可能一个漫长的过程,需要大量实践经验的积累。

单例,工厂,观察者,我认为是设计模式三杰,为何这么说,因为这三种设计模式使用频率最高,变化最多,而且覆盖面最广,当然,也是面试中最容易被问到的。

工厂模式

工厂模式属于创建型模式,大致可以分为三类,简单工厂模式、工厂模式、抽象工厂模式

简单工厂模式就是有一个专门生产某个产品的类。比如下图中的单核处理器工厂专业生产单核处理器。给参数0,就生产因特尔单核处理器;给参数1,就生产AMD单核处理器。有switch case的感觉。。。

这里写图片描述

工厂模式也就是单核处理器工厂,是个父类,有生产单核处理器这个接口,有两个子类继承自他:
英特尔处理器工厂、AMD处理器工厂继承它,可以分别生产英特尔单核处理器、AMD单核处理器。
生产哪种单核处理器不再由参数决定,而是直接调用 XXX处理器工厂类名.生产单核处理器()

这里写图片描述

抽象工厂模式也就是不仅生产单核处理器,还同时生产多核处理器。
也就是处理器工厂是个父类,有生产单核处理器、多核处理器这两个接口。
英特尔处理器工厂、AMD处理器工厂继承它,可以分别生产英特尔单核、多核处理器,AMD单核、多核处理器。

直接调用举例:
AMD处理器工厂类名.生产单核处理器(),则生产AMD单核处理器;
AMD处理器工厂类名.生产多核处理器(),则生产AMD多核处理器;

这里写图片描述

补充1:
在抽象工厂模式中,假设我们需要增加一个工厂:
假设我们增加XYZ处理器工厂,则让它继承处理器工厂。再创建XYZ单核处理器类,继承单核处理器类;创建XYZ多核处理器类,继承多核处理器类。

补充2:
假设我们增加无核处理器(随便举的例子,不要在意)这个产品,则首先我们需要增加无核处理器这个父类,再加上英特尔无核处理器、AMD无核处理器这两个子类。之后在处理器工厂这个父类中,增加生产无核处理器的接口。最后在英特尔处理器工厂、AMD处理器工厂这两个类中,分别实现生产英特尔无核处理器、AMD无核处理器的功能。

关键点如下:

一、三种工厂的实现是越来越复杂的
二、简单工厂通过构造时传入的标识来生产产品,不同产品都在同一个工厂中生产,这种判断会随着产品的增加而增加,给扩展和维护带来麻烦
三、工厂模式无法解决产品族和产品等级结构的问题
四、抽象工厂模式中,一个工厂生产多个产品,它们是一个产品族,不同的产品族的产品派生于不同的产品接口。

你仍然可能面临下面的问题:

1.到底应该怎么选择使用这三种工厂模式

根据具体业务需求。不要认为简单工厂是用switch case就觉得一无是处,也不要觉得抽象工厂比较高大上就到处套。我们使用设计模式是为了解决问题而不是炫技,所以根据三种工厂模式的特质,以及对未来扩展的预期,来确定使用哪种工厂模式。

2.说说你在项目中抽象工厂模式的应用

抽象工厂在游戏中的使用更多的是一些与游戏机制无关的部分,仅有部分游戏机制会用到抽象工厂模式,,比如:

  1. 语言
    假设有中文和英文两种语言,当用户更换语言时,会改变文字的显示和音效等组件。
    具体:有一个语言父类,分别有中文和英文这两个子类;还有文字显示和音效这两个父类,其中,中文的文字显示类和英文的文字显示类分别继承自文字显示类,中文的音效类和英文的音效类分别继承音效类。调用方式举例:中文子类名.中文文字显示()。

  2. 皮肤更换
    当用户选择不同的皮肤会影响图片的显示以及动画效果。
    具体:同上,省略。

为什么不直接用一个二维数组解决问题呢?干嘛还要用类一个一个分隔开?
解答:如果这是一个二维数组,那么每个数组元素都是一个处理单元,是一个具体实现,也就是说,当元素包括操作步骤时,这个粒度就不是单单使用数据结构所能驾驭的了。

只要你理解了,把来龙去脉说清楚了,我想应该是可以令面试官满意的。

单例模式

重要性:如果播放一首歌就新建一个窗口,那我播放100首歌,岂不是有100个窗口,那怎么可以!所以,用单例呀。当然,单例在Unity中也可用于音频管理类等地方,很实用的。
我们先来看看代码。根据单例模式的理论:保证系统中只有一个实例,于是我撸了以下代码

//C#代码
public class Singleton 
{  
    //为了防止外界通过new获得该类的实例
    private Singleton() {}                     //关键点0:构造函数是私有的

    private static Singleton single = null;    //关键点1:声明单例对象是静态的
    public static Singleton GetInstance()      //通过静态方法来构造对象
    {                        
         if (single == null) 
         {                                     //关键点2:判断单例对象是否已经被构造
             single = new Singleton();  
         }    
        return single;  
    }  
}

好了,如果我是面试官,你是候选人,我要你撸个单例给我,你撸以上代码问我资词不资词,我肯定是不资词的。为什么?真的不是因为我想搞个大新闻,而是这段单例的代码一般情况下还是可以勉强运行,但是,遇到多线程的并发条件下就大清药丸了。因为这段代码是线程不安全的,有可能给new出多个单例实例,都多个了,还是屁的“单例”啊。

好,废话不多说,继续撸代码,你可能会说:”不是说线程不安全吗?小爷我加上线程安全判断呗,度娘在手天下我有,来了您呐~~~“

public class Singleton 
{  
    private Singleton() {}                     //关键点0:构造函数是私有的
    private static Singleton single = null;    //关键点1:声明单例对象是静态的
    private static object obj= new object();   //静态方法只能访问静态成员变量
    public static Singleton GetInstance()      //通过静态方法来构造对象
    {                        
         if (single == null)                   //关键点2:判断单例对象是否已经被构造
         {                             
            lock(obj)                          //关键点3:加线程锁
            {
               single = new Singleton();  
             }
         }    
        return single;  
    }  
}

好了,这回该消停了吧,锁也加了,线程也安全了。But,你还是太连清。。。这样依然有问题。问题在哪里?就在关键点2,检测单例是否被构造。虽然这里判断了一次,但是由于某些情况下,可能有延迟加载或者缓存的原因,只有关键点2这一次判断,仍然不能保证系统是否只创建了一个单例,也可能出现多个实例的情况,那么怎么办呢?

//C#代码
public class Singleton 
{  
    private Singleton() {}                     //关键点0:构造函数是私有的
    private static Singleton single = null;    //关键点1:声明单例对象是静态的
    private static object obj= new object();   //lock引用对象obj才是互斥锁
    public static Singleton GetInstance()      //通过静态方法来构造对象
    {                        
         if (single == null)                   //关键点2:判断单例对象是否已经被构造
         {                             
            lock(obj)                          //关键点3:加线程锁
            {
               if(single == null)              //关键点4:二次判断单例是否已经被构造
               {
                  single = new Singleton();  
                }
             }
         }    
        return single;  
    }  
}

这里写图片描述

所以,在判断单例实例是否被构造时,需要检测两次,在互斥锁lock之前判断一次,在互斥锁lock之后判断一次,再去构造实例,这样就万无一失了。

最后,我们来归纳一下。下次面试别人再问你单例模式,你可以这样说:

单例是为了保证系统中只有一个实例,其关键点有

一.私有构造函数
二.声明静态单例对象
三.构造单例对象之前要加锁(lock一个静态的object对象)
四.需要两次检测单例实例是否已经被构造,分别在锁之前和锁之后

如果要你撸代码,你就撸最后这一段,完美~~~面试官要是个女的准想和你生猴子。。。哦,别高兴太早了,面试官要是个男的,也可能会问你下列问题

0.为何要检测两次?

如上面所述,有可能延迟加载或者缓存原因,造成构造多个实例,违反了单例的初衷。

1.构造函数能否公有化?

不行,单例类的构造函数必须私有化,单例类不能被实例化,只能通过静态方法来构造对象

2.lock住的对象为什么要是object对象,可以是int吗?

不行,锁住的必须是个引用类型。如果锁值类型,每个不同的线程在声明的时候值类型变量的地址不一样,而引用的地址是一样的,那么上个线程锁住的东西下个线程进来会认为根本没锁,相当于每次都锁了不同的门,没有任何卵用。每个线程进来判断锁是否被锁的时候都是判断同一个地址,相当于是锁在通一扇门,起到了锁的作用。

好了,这下估计男的都想跟你生猴子了。。。

3.单例模式的优缺点和使用场景

主要优点:
1、提供了对唯一实例的受控访问。
2、由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。

主要缺点:
1、由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
2、单例类的职责过重,在一定程度上违背了“单一职责原则”。
3、滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

适用场景 :
在以下情况下可以考虑使用单例模式:
- (1) 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
- (2) 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。

4.由于单例的脚本通常绑定在一个物体上,所以,当Unity中场景切换了,如何保证单例模式不受影响?

在Awake函数中,加一句代码DontDestroyOnLoad (gameObject);这句话意味着,当我们的场景发生变化时,单例模式将不受任何影响。除此之外,该句代码不能放到Start函数中,这是由两个函数的执行顺序决定的,如果反过来,便可能会造成访问单例不成功。

5.补充:

一定不要在OnDestroy函数中直接访问单例模式!这样很有可能会造成单例无法销毁。这是因为,当程序退出准备销毁单例模式时,我们在其他脚本的OnDestroy函数中再次请求访问它,这样将重新构造一个新的单例而不会被销毁(因为之前已经销毁过一次了)。如果一定要访问的话,一定要先调用IsCreatedInstance,判断该单例是否存在。

观察者模式

观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。它还有两个别名,依赖(Dependents),发布-订阅(Publish-Subsrcibe)。可以举个博客订阅的例子,当博主发表新文章的时候,即博主状态发生了改变,那些订阅的读者就会收到通知,然后进行相应的动作,比如去看文章,或者收藏起来。博主与读者之间存在种一对多的依赖关系。

在一个系统中,实现这种一对多的而且之间有一定关联的逻辑的时候,由于需要保持他们之间的协同关系,所以最简便的方法是采用紧耦合,把这些对象绑定到一起。但是这样一来,一旦有扩展或者修改的时候,开发人员所面对的难度非常大,而且很容易造成Bug。那么观察者模式就解决了这么一个问题,在保持一系列观察者和被观察者对象协同工作的同时,把之间解耦了。

    //被观察者
    public interface IObject
    {
        //定义观察者集合,因为多个观察者观察一个对象,所以这里用集合
        IList<IMonitor> ListMonitor { get; set; } 

        string SubjectState { get; set; }        //被观察者的状态
        void AddMonitor(IMonitor monitor);  //添加一个观察者
        void RemoveMonitor(IMonitor monitor); //移除一个观察者
        void SendMessage(); //向所有观察者发送消息
    }
    public class Subject : IObject
    {
        private IList<IMonitor> listMonitor = new List<IMonitor>();
        public string SubjectState //被观察者的状态
        {
            get;set;
        }
        public IList<IMonitor> ListMonitor //实现具体的观察者列表属性
        {
            get { return listMonitor; }
            set { listMonitor = value; }
        }
        public void AddMonitor(IMonitor monitor)  //实现具体的添加观察者方法
        {
            listMonitor.Add(monitor);     
        }
        public void RemoveMonitor(IMonitor monitor) //实现具体的移除观察者方法
        {           
            listMonitor.Remove(monitor);
        }
        public void SendMessage()    //实现具体的发送消息方法
        {
            //发送给所有添加过的观察者,让观察者执行update方法以同步更新自身状态
            foreach (IMonitor m in listMonitor)              
            {
                m.Update();
            }
        }
    }

    //观察者
    public interface IMonitor //定义观察者接口
    {
        void Update(); 
    }
    public class Monitor : IMonitor   //实现具体观察者
    {
        private string monitorState="Stop!";//观察者初始状态,会随着被观察者变化而变化
        private string name;            //观察者名称,用于标记不同观察者
        private IObject subject;        //被观察者对象

        //在构造观察者时,传入被观察者对象,以及标识该观察者名称
        public Monitor (IObject subject, string name)  
        {
            this.subject = subject;
            this.name = name;
            Console.WriteLine("我是观察者{0},我的初始状态是{1}", name, monitorState);
        }
        public void Update()//当被观察者状态改变,观察者需要随之改变
        {
            monitorState = subject.SubjectState;
            Console.WriteLine("我是观察者{0},我的状态是{1}", name, monitorState);
        }
    }


   //前端调用
   static void Main(string[] args)
        {
            IObject subject = new Subject();
            subject.AddMonitor(new Monitor(subject, "Monitor_1"));
            subject.AddMonitor(new Monitor(subject, "Monitor_2"));
            subject.AddMonitor(new Monitor(subject, "Monitor_3"));
            subject.SubjectState = "Start!";
            subject.SendMessage();
            Console.Read();
        }
    }

结果如下:

我是观察者Monitor_1,我的初始状态是Stop!
我是观察者Monitor_2,我的初始状态是Stop!
我是观察者Monitor_3,我的初始状态是Stop!
我是观察者Monitor_1,我的状态是Start!
我是观察者Monitor_2,我的状态是Start!
我是观察者Monitor_3,我的状态是Start!

这样就完成了一个观察者模式。我们回过头来看看,在被观察者中,我定义了一个集合用来存放观察者,并且我写了一个Add方法一个Remove方法来添加和移除观察者,这体现了一对多的关系,也提供了可以控制观察者的方式。所以,我们得到第一个关键点:每个观察者需要被保存到被观察者的集合中,并且被观察者提供添加和删除的方式。

然后我么再看一下,观察者和被观察者之间的交互活动。不难发现,我是在添加一个观察者的时候,把被观察者对象以构造函数的形式给传入了观察者。最后我让被观察者执行sendmessage方法,这时会触法所有观察者的update方法以更新状态。所以我们得到第二个关键点,被观察者把自己传给观察者,当状态改变后,通过遍历或循环的方式逐个通知列表中的观察者

好了,到这里你应该可以把握住观察者模式的关键了。但这里有个问题,被观察者是通过构造函数参数的形式,传给观察者的,而观察者对象是被Add到被观察者的List中。所以,我们得到第三个关键点,虽然解耦了观察者和被观察者的依赖,让各自的变化不大影响另一方的变化,但是这种解耦并不是很彻底,没有完全解除两者之间的耦合。

有很多同学比较怕被问到观察者模式,特别是搞.Net的同学。为什么呢?因为一旦涉及到观察者模式,必然会涉及到2种类型,委托和事件。因为很多人并不理解委托和事件,或者只停留在浅层次。那么,委托,事件,和观察者模式到底有什么关系呢?

首先我们来看委托。委托就是可以把方法当做另一个方法参数来传递的东东,当然方法签名需要注意一下。委托可以看做是方法的抽象,也就是方法的“类”,一个委托的实例可以是一个或者多个方法。我们可以通过+=或者-=把方法绑定到委托或者从委托移除。

再来看事件,实际上事件是一种特殊的委托。怎么说呢,首先事件也是委托,只是在声明事件的时候,需要加上event,如果你用reflector去看一个事件,你会发现里面就3样东西,一个Add_xxxx方法,一个Remove_xxx方法,一个委托。说到这里,是不是觉得和上面我们定义被观察者时的Add方法,Remove方法有些联系?

没错,实际上.Net的事件机制就是观察者模式的一种体现,并且是利用委托来实现。本质上事件就是一种订阅-发布模型也就是观察者模式,这种机制中包含2个角色,一个是发布者,一个是订阅者。发布者类也就类似于被观察者,发布者类包含事件和委托定义,以及其之间的关系,发布者类的对象调用事件通知其他订阅者。而订阅者类也就类似于观察者,观察者接受事件,并且提供处理的逻辑。

也就是说,订阅者对象(观察者)中的方法会绑定到发布者(被观察者)对象的委托中。一旦发布者(被观察者)中事件被调用,发布者(被观察者)就会调用委托中绑定的订阅者(观察者)的处理逻辑或者说是处理程序,这就是通过观察者模式实现的事件。虽然这段话比较拗口,但是我想理解起来应该还是不太难把。。。

好了,你虽然现在已经对面试官逼逼叨了上面一大通,但是贱贱的面试官仍然想challenge你一下,否则不就是太没面子么。。。

1.你刚刚说了,在普通的观察者模式中,解耦并不彻底,那么在事件的发布订阅模型中,解耦彻底吗?为什么?

答案是肯定的。因为在事件中,订阅者和发布者之间是通过把事件处理程序绑定到委托,并不是把自身传给对方。所以解决了观察者模式中不完全解耦的问题。这也是关键点之四

2.通过委托绑定方法来实现观察者模式,会不会有什么隐患?

有的,通过+=去把方法绑定到委托,很容易忘记-=。如果只绑定不移除,这个方法会一直被引用。我们知道GC去回收的时候,只会处理没有被引用的对象,只要是还被引用的对象时不会被回收掉的。所以如果在长期不关闭的系统中(比如监控系统),大量的代码使用+=而不-=,运行时间长以后有可能会内存溢出。

3.事件,委托,观察者模式之间的关系

这个上面已经提到了,委托时一种类型,事件是一种特殊的委托,观察者模式是一种设计模式,事件的机制是观察者模式的一种实现,其中订阅者和发布者通过委托实现协同工作。

关键点有:

一、每个观察者需要被保存到被观察者的集合中,并且被观察者提供添加和删除的方式。
二、被观察者把自己传给观察者,当状态改变后,通过遍历或循环的方式逐个通知列表中的观察者。
三、虽然解耦了观察者和被观察者的依赖,让各自的变化不大影响另一方的变化,但是这种解耦并不是很彻底,没有完全解除两者之间的耦合。
四、在事件中,订阅者和发布者之间是通过把事件处理程序绑定到委托,并不是把自身传给对方。所以解决了观察者模式中不完全解耦的问题

注意点:在使用委托绑定方法时,需要注意移除方法,否则可能会造成内存溢出。

建议:不能理解事件和委托的同学,好好地把事件和委托看一看,并且自己写点代码加深印象。

精华的一句:事件的机制是观察者模式的一种实现,其中订阅者和发布者通过委托实现协同工作。

那么,Unity中如何应用委托的?

    //1.先写一个委托类,所有的委托都写在这个类里。
    public class MyDelegate
    {
        //定义 委托名为LogDelegate,带一个string参数的 委托类型
        public delegate void LogDelegate(string log);   
        //声明委托对象,委托实例为LogEvent
        public static LogDelegate LogEvent;             

        //后面可直接MyDelegate.OnLogEvent("")调用委托,这么写方便管理,还可以扩展这方法  
        public static void OnLogEvent(string log)       
        {
            if (LogEvent != null)
            {
                LogEvent(log);
            }
        }
    }
    //2.添加委托调用事件,调用委托。
    void Start()
    {

        MyDelegate.LogEvent += MyLog;
        MyDelegate.LogEvent += MyLog2;

        MyDelegate.OnLogEvent("给你们这些小函数发回调消息了啊!");
    }

    void MyLog(string log)
    {
        Debug.Log("这种委托方法真是好用的不得了!我收到你的消息了:" + log);
    }

    void MyLog2(string log)
    {
        Debug.Log("可以实现消息触发回调,好方便!我也收到了:" + log);
    }

输出:
这种委托方法真是好用的不得了!我收到你的消息了:给你们这些小函数发回调消息了啊!
可以实现消息触发回调,好方便!我也收到了:给你们这些小函数发回调消息了啊!

相当实用,在工作中用的地方很多,观察者模式就是委托和事件的应用,而且确实好用,特别是接收到比如登录成功消息,这时候就需要分发消息了,通知界面显示名字,更新游戏币,加载任务装备,一个委托搞定!

另外,还可见unity中C#委托的应用,了解更多的应用套路。

MVC模式

数据与界面分离,因为游戏的UI通常是需要经过多次的迭代,如果数据与UI没有分离,每一次数据的改动量就会非常大。使用MVC思想写出的代码,一般不是太大的改动只需要调整UI部分的代码,而数据部分是不用改的。

中介者模式

当我们的多个系统模块之间会有通信,如果系统之间杂乱无章的进行通信数据交互的话,耦合性很高,不容易维护。中介者模式就是解决这种情况,定义一个中介对象来封装系列对象之间的交互。例子:

这里写图片描述

各个子系统之间错中复杂的进行交互,使用中介者模式很好的避免了这种问题,也就是我们开发游戏的时候写的那个GameManager脚本的作用。

这里写图片描述

优点:将系统的各个对象之间的交互关系进行封装,将各个子系统类解耦,提供系统的灵活性,使得各个系统对象独立而易于复用。
缺点:中介者承担了较多的责任,所以当中介者被破坏后,各个系统将可能受到影响;当我们的游戏中需要添加新的子系统时候,这样将要修改中介类,违背了设计原则的开闭原则:在不修改软件实体的基础上去扩展其他功能。

参考

【1】设计模式C++实现(4)——单例模式
http://blog.csdn.net/wuzhekai1985/article/details/6665869
【2】面试被问设计模式?不要怕看这里:单例模式
http://blog.jobbole.com/109449/#article-comment
【3】设计模式C++实现(15)——观察者模式
http://blog.csdn.net/wuzhekai1985/article/details/6674984
【4】面试被问设计模式?不要怕看这里:观察者模式
http://blog.jobbole.com/109845/
【5】unity3d 超好用的委托模式
http://blog.csdn.net/u012322710/article/details/52911937

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值