设计模式详解之创建型模式——单例、原型和工厂


前言:相信作为程序开发,或多或少都接触甚至使用过设计模式,甚至对于有些设计模式的概念都已经很熟悉了,但是在实际开发项目的时候是否有使用过这些模式呢,可能比较少甚至没有。有些设计模式确实在架构中更实用一些,这也是部分原因。但不管怎样,最起码常用的几种设计模式还是需要了解的,本文介绍几种常见的设计模式,希望读者能从中有所收获并学以致用。

什么是设计模式

设计模式(Design Pattern)是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码 可复用性 、 可维护性 、可读性、稳健性以及安全性的解决方案。

假设有一个空房间,我们要日复一日地往里 面放一些东西。最简单的办法当然是把这些东西 直接扔进去,但是时间久了,就会发现很难从这 个房子里找到自己想要的东西,要调整某几样东 西的位置也不容易。所以在房间里做一些柜子也 许是个更好的选择,虽然柜子会增加我们的成 本,但它可以在维护阶段为我们带来好处。使用 这些柜子存放东西的规则,或许就是一种模式。

在 1994 年,由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 四人合著出版了一本名为 Design Patterns - Elements of Reusable Object-Oriented Software(中文译名:设计模式 - 可复用的面向对象软件元素) 的书,该书首次提到了软件开发中设计模式的概念。
四位作者合称 GOF(四人帮,全拼 Gang of Four)。他们所提出的设计模式主要是基于以下的面向对象设计原则。

  • 对接口编程而不是对实现编程——依赖倒置原则
  • 优先使用对象组合而不是继承——合成复用原则。

设计模式的六大原则:

  1. 单一职责原则(Single Responsibility Principle)。职责清晰
  2. 里氏替换原则(Liskov Substitution Principle)——任何使用基类的地方,都可以透明的使用其子类
  3. 迪米特法则 (Law Of Demeter)—— 一个对象应该对其他对象保持最少的了解,即高聚合低耦合
  4. 依赖倒置原则(Dependence Inversion Principle)—— 依赖抽象,而不是依赖细节
  5. 接口隔离原则(Interface Segregation Principle)—— 客户端不应该依赖它不需要的接口; 一个类对另一个类的依赖应该建立在最小的接口上;
  6. 开闭原则 (Open Closed Principle) —— 对扩展开发,对修改关闭

创建型模式

单例模式(Singleton Pattern)

单例模式想必大家都已经耳熟能详了,这是很常见的一种设计模式,也是最简单的一种设计模式。它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

使用单例模式的实例:.NET Core依赖注入生命周期中的Singleton。生命周期为Singleton的服务全局唯一,每次调用都是调用的同一个服务.

单例模式有两种写法,懒汉式饿汉式

懒汉式,顾名思义,比较懒,一开始的时候不会创建对象,要用到了才会想起来去创建对象,写法如下:

//懒汉式单例写法
public class SingletonPattern
{
    private static SingletonPattern _singletonInstance;
    //构造函数私有化是关键
    private SingletonPattern()
    {
 
    }
    //双重检验,提升性能
    public static SingletonPattern GetInstance(bool useLock = true)
        {
         if (_singletonInstance is null)
            {
                lock (singleton_lock)
                {
                    if (_singletonInstance is null)
                    {
                        _singletonInstance = new SingletonPattern();
                    }
                }
            }
            return _singletonInstance;
       }
}

注意:懒汉式要考虑多线程安全问题,这里使用双重检验锁以确保线程安全并提升性能

饿汉式比较简单,只需要做个是否已经创建的判断即可,写法如下:

//饿汉式单例写法
public class SingletonPattern
{
    private static SingletonPattern _singletonInstance = new SingletonPattern();
    //构造函数私有化是关键
    private SingletonPattern()
    {
 
    }
 
    public static SingletonPattern GetInstance(bool useLock = true)
        {          
            if (_singletonInstance is null)
            {
                _singletonInstance = new SingletonPattern();
            }            
            return _singletonInstance;
       }
}

该模式的适用场景:

  • 一个全局类,频繁创建和销毁,如服务类、工具类等
  • 需要控制实例数量,节约系统资源的时候

原型模式(Prototype Pattern)

原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。

原型模式的难点在于对对象的克隆,如果对象比较复杂,嵌套的属性对象比较多,自己实现克隆方法会比较麻烦。不过还好目前主流语言都支持深度克隆对象,如Java的Serializable, javascript的cloneDeep等等,深度克隆也可以通过序列化和反序列化来实现,因此支持序列化和反序列化的语言都可以用这种方式实现深度克隆。

原型的实现方式与单例相差不大,重点是返回的实例是克隆对象

public class PrototypePattern
   {
       private static PrototypePattern _protetypeInstance = new PrototypePattern();
 
       private PrototypePattern()
       {
 
       }
 
       public static PrototypePattern GetInstance()
       {
           //重点在于返回克隆的对象
           PrototypePattern clone = GetDeepCloneObj();
           return clone;
       }
   }

原型适用场景:

  • 对象创建复杂,消耗资源大,又需要重复创建类似对象

工厂模式(Factory Pattern)

工厂模式在GOF的设计模式中分为工厂方法和抽象工厂,实际上简单工厂、工厂方法和抽象工厂的分类更为普遍一些。

简单工厂模式(Sinple Factory Pattern)

简单工厂主要隔离了使用者和产品,使用者需要使用产品时,直接向工厂请求,而不用知道具体产品的实现。也就是前面说的依赖倒置原则!

简单工厂

上图可以看出,用户要创建一个产品,不需要知道产品的具体实现,只需要知道创建产品的工厂即可。实现代码如下:

public class SimpleFactoryPattern
    {
        public static IRunner CreateRunner(PatternEnum pattern)
        {
            switch (pattern)
            {
                case PatternEnum.Singleton:
                    return new SingletonRunner();
                case PatternEnum.Prototype:
                    return new PrototypeRunner();
                case PatternEnum.Factory_Method:
                    return new FactoryMethodRunner();
                case PatternEnum.Abstract_Factory:
                    return new AbstractFactoryRunner();
                default:
                    return null;
            }
        }
    }
 
//使用
class Program
  {
        static void Main(string[] args)
        {
            var pattern = PatternEnum.Abstract_Factory;
            var prototype = SimpleFactoryPattern.CreateRunner(pattern);
            prototype.Run();
 
            Console.ReadKey();
        }
  }

简单工厂适用于比较简单的情况下,可以屏蔽对象创建细节,对于对象来说符合开闭原则。但是对于工厂来说并不符合开闭原则,因为当需要新增一个对象时,需要修改工厂的内容。另外当需要生成的对象过多时或者经常需要修改时,该模式就显得不够用了。

这时候就需要使用工厂方法了

工厂方法模式(Factory Method Pattern)

工厂方法

如上图,工厂方法相比于简单工厂,改变在于将具体的工厂也屏蔽了,用户不需要知道需要用什么工厂,只要调用抽象工厂的方法即可。实现如下:

 //抽象工厂
 public abstract class MounseFactoryMethod
   {
       public abstract IMouse CreateMouse();
   }
   //具体实现工厂
   public class DellMouseFactory : MounseFactoryMethod
   {
       public override IMouse CreateMouse()
       {
           return new DellMouse();
       }
   }
   //具体实现工厂
   public class HpMouseFactory : MounseFactoryMethod
   {
       public override IMouse CreateMouse()
       {
           return new HpMouse();
       }
   }
   //当具体的实现工厂太多时,可以结合简单工厂,利用简单工厂创建具体工厂
   //也可以利用反射创建工厂
   public class MouseFactory
   {
       public static MounseFactoryMethod CreateMouseFactory(BrandEnum brand)
       {
           switch (brand)
           {
               case BrandEnum.Dell:
                   return new DellMouseFactory();
               case BrandEnum.Hp:
                   return new HpMouseFactory();
               default:
                   return null;
           }
       }
   }
 
   //使用
   class Program
 {
       static void Main(string[] args)
       {                       
           var mouseFactory = MouseFactory.CreateMouseFactory(BrandEnum.Dell);
           var mouse = mouseFactory.CreateMouse();
           mouse.Click();        
       }
 }

工厂方法的工厂是符合开闭原则的,当有新产品时,只需要添加新的工厂即可,不需要改动已有工厂代码。
在上面的代码中,我们抽象了一个鼠标生产工厂MounseFactoryMethod,两个具体生产工厂DellMouseFactory 和HpMouseFactory 。另外还额外使用了一个简单工厂MouseFactory来选择要使用的具体工厂。

工厂方法在实际使用中比较常见,当需要创建的对象种类比较多且新增或删除比较频繁时,工厂方法是不错的选择。

抽象工厂(Abstract Factory)

首先了解下产品族的概念:

产品族

如上图,拥有相同特性的产品称为一个产品等级,同一产品平台的不同产品称为一个产品族

抽象工厂用于处理比较复杂的产品。举个例子,上述工厂方法中的MounseFactoryMethod专门用于生产鼠标,而DellMouseFactory 和HpMouseFactory 则分别用于生产戴尔鼠标和惠普鼠标,它们都是一个产品等级的产品。当我们不仅需要生产鼠标,还要生成键盘时,光是一个MounseFactoryMethod已经不能满足生产需要,这时候工厂生产的就不只是单一产品,而是一个产品族。类图如下:

抽象工厂

上图中,我们定义了一个ComputerFactory,这个工厂能够生产更丰富的产品(鼠标和键盘),戴尔和惠普分别有独立的工厂生产自己的鼠标和键盘。

像上图这种,生产产品族的工厂称为抽象工厂。

抽象工厂和工厂方法的区别在于,工厂方法只能生产单一产品,也就是产品接口只有一个,而抽象工厂的产品可能是来自不同的接口。
实现代码如下:

public abstract class ComputerAbstractFactory
  {
      public abstract IMouse CreateMouse();
      public abstract IKeyboard CreateKeyboard();
  }
 
  public class DellAbstractFactory : ComputerAbstractFactory
  {
      public override IKeyboard CreateKeyboard()
      {
          return new DellKeyboard();
      }
 
      public override IMouse CreateMouse()
      {
          return new DellMouse();
      }
  }
 
  public class HpAbstractFactory : ComputerAbstractFactory
  {
      public override IKeyboard CreateKeyboard()
      {
          return new HpKeyboard();
      }
 
      public override IMouse CreateMouse()
      {
          return new HpMouse();
      }
  }
 
  //使用
  class Program
{
      static void Main(string[] args)
      {                                 
          Console.WriteLine("使用抽象工厂 DellAbstractFactory 创建产品");
          var dellFactory = new DellAbstractFactory();
          dellFactory.CreateKeyboard().Click();
          dellFactory.CreateMouse().Click();
 
          Console.WriteLine("使用抽象工厂 HpAbstractFactory 创建产品");
          var hpFactory = new HpAbstractFactory();
          hpFactory.CreateKeyboard().Click();
          hpFactory.CreateMouse().Click();            
      }
}

工厂模式总结

  1. 简单工厂不符合开闭原则,仅使用于产品种类少、修改不频繁的情况
  2. 工厂方法符合开闭原则,但只适用单一产品等级的情况
  3. 抽象工厂符合开闭原则,适用于生产产品族的情况
posted @ 2022-04-26 14:20  北冥虾  阅读(256)  评论(0编辑  收藏  举报