抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

记录设计模式学习摘要。

一、创建型模式

创建型模式将对象的创建和使用分离,在使用对象时无需关心对象的创建细节,从而降低系统的耦合度,让设计方案更易于修改和扩展。每一个创建型模式都在视图回答 3 个问题:3W-> 创建什么(What)、由谁创建(Who)和何时创建(When)。

1.单例模式 singleton

1.1 要点

确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建模式。

单例模式有 3 个要点:

  • 某个类只能有一个实例
  • 它必须自行创建这个实例
  • 它必须自行向整个系统提供这个实例

1.2 结构图

img

Singleton(单例):

  • 在单例类的内部实现只生成一个实例,同时它提供一个静态的 GetInstance()方法,让客户可以访问它的唯一实例;
  • 为了防止在外部对单例类实例化,它的构造函数被设为 private
  • 在单例类的内部定义了一个 Singleton类型的静态对象,作为提供外部共享的唯一实例。

情景模拟

1
假设M公司成都分公司的IT开发部门承接了一个服务器负载均衡器(Load Balance)软件的开发,该软件运行在一台负载均衡服务器上面,可以将并发访问和数据流量分发到服务器集群中的多台设备上进行并发处理,提高系统的整体处理能力,缩短响应时间。由于集群中的服务器需要动态增减,且客户端请求需要统一分发,因此需要确保负载均衡器的唯一性,即只能有一个负载均衡器实例来管理服务器和分发请求,否则会带来服务器状态的不一致以及请求的分配冲突等问题。如何确保负载均衡器的唯一性成为了这个软件成功地关键。

1.3 饥汉式与懒汉式单例

饥汉式单例

在 C#中,我们可以利用静态构造函数来实现。

C#的语法中有一个函数能够确保只调用一次,那就是静态构造函数。由于 C#是在调用静态构造函数时初始化静态变量,.NET 运行时(CLR)能够确保只调用一次静态构造函数,这样我们就能够保证只初始化一次 instance。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LoadBalancer
{
// 私有静态变量,存储唯一实例
private static readonly LoadBalancer instance = new LoadBalancer();

......

// 公共静态成员方法,返回唯一实例
public static LoadBalancer GetLoadBalancer()
{
return instance;
}
}

饿汉式是在 .NET 中实现 Singleton 的首选方法。但是,由于在 C#中调用静态构造函数的时机不是由程序员掌控的,而是当.NET 运行时发现第一次使用该类型的时候自动调用该类型的静态构造函数(也就是说在用到 LoadBalancer时就会被创建,而不是用到 LoadBalancer.GetLoadBalancer()时),这样会过早地创建实例,从而降低内存的使用效率。此外,静态构造函数由 .NET Framework 负责执行初始化,我们对对实例化机制的控制权也相对较少。

懒汉式单例

除了饿汉式之外,还有一种懒汉式。最开始我们实现的方式就是一种懒汉式单例,也就是说,在第一个调用 LoadBalancer.GetLoadBalancer()时才会实例化对象,这种技术又被称之为延迟加载Lazy Load)。同样,我们的目标还是为了避免多个线程同时调用 GetLoadBalancer方法,在 C#中,我们可以使用关键字 lock/Moniter.Enter+Exit等来实现,这里采用关键字语法糖 lock来改写代码段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class LoadBalancer
{
// 私有静态变量,存储唯一实例
private static LoadBalancer instance = null;
private static readonly object syncLocker = new object();

......

// 公共静态成员方法,返回唯一实例
public static LoadBalancer GetLoadBalancer()
{
if (instance == null)
{
lock (syncLocker)
{
instance = new LoadBalancer();
}
}
return instance;
}
}

问题貌似得以解决,但事实并非如此。如果使用以上代码来创建单例对象,还是会存在单例对象不一致。假设线程 A 先进入 lock 代码块内,执行实例化代码。此时线程 B 排队吃瓜等待,必须等待线程 A 执行完毕后才能进入 lock 代码块。但当 A 执行完毕时,线程 B 并不知道实例已经被创建,将继续创建新的实例,从而导致多个单例对象。因此,开发人员需要进一步改进,于是就有了双重检查锁定 (Double-Check Locking),其改写代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class LoadBalancer
{
// 私有静态变量,存储唯一实例
private static LoadBalancer instance = null;
private static readonly object syncLocker = new object();

......

// 公共静态成员方法,返回唯一实例
public static LoadBalancer GetLoadBalancer()
{
// 第一重判断
if (instance == null)
{
// 锁定代码块
lock (syncLocker)
{
// 第二重判断
if (instance == null)
{
instance = new LoadBalancer();
}
}
}
return instance;
}
}
一种更好的单例实现

饿汉式单例不能延迟加载,懒汉式单例安全控制繁琐,而且性能受影响。静态内部类单例则将这两者 有点合二为一。使用这种方式,我们需要在单例类中增加一个静态内部类,在该内部类中创建单例对象,再将该单例对象通过 GetInstance()方法返回给外部使用,于是开发人员又改写了代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class LoadBalancer
{
......

// 公共静态成员方法,返回唯一实例
public static LoadBalancer GetLoadBalancer()
{
return Nested.instance;
}

// 使用内部类+静态构造函数实现延迟初始化
class Nested
{
static Nested() { }
internal static readonly LoadBalancer instance = new LoadBalancer();
}

......
}

该实现方法在内部定义了一个私有类型 Nested。当**第一次用到这个嵌套类型的时候,会调用静态构造函数创建 LoadBalancer的实例 instance**。如果我们不调用属性 LoadBalancer.GetLoadBalancer(),那么就不会触发.NET 运行时 (CLR)调用 Nested,也就不会创建实例,因此也就保证了按需创建实例(或延迟初始化)。可见,此方法既可以实现延迟加载,又可以保证线程安全,不影响系统性能。但其缺点是与具体编程语言本身的特性相关,有一些面向对象的编程语言并不支持此种方式。

1.4 单例模式总结

主要优点
  • 提供了对唯一实例的受控访问。单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
  • 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
  • 允许可变数目的示例。基于单例模式,开发人员可以进行扩展,使用与控制单例对象相似的方法来获得指定个数的实例对象,既节省系统资源,又解决了单例对象共享过多有损性能的问题。(Note:自行提供指定书目的实例对象的类可称之为多例类)例如,数据库连接池,线程池,各种池。
主要缺点
  • 单例模式中没有抽象层,因此单例类的扩展有很大的困难。
  • 单例类的职责过重,在一定程度上违背了单一职责的原则。因为单例类既提供了业务方法,又提供了创建对象的方法(工厂方法),将对象的创建和对象本身的功能耦合在一起。不够,很多时候我们都需要取得平衡。
  • 很多高级面向对象编程语言如 C#和 Java 等都提供了垃圾回收机制,如果实例化的共享对象长时间不被利用,系统则会认为它是垃圾,于是会自动销毁并回收资源,下次利用时又得重新实例化,这将导致共享的单例对象状态的丢失。
适用场景
  • 系统只需要一个实例对象。例如:系统要求提供一个唯一的序列号生成器或者资源管理器,又或者需要考虑资源消耗太大而只允许创建一个对象。
  • 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。比如,在 Flappy Bird游戏中,小鸟这个游戏对象在整个游戏中应该只存在一个实例,所有对于这个小鸟的操作(向上飞、向下掉等)都应该只会针对唯一的一个实例进行。

2.简单工厂模式 Simple Factory

工厂模式是最常用的一种创建型模式,通常所说的工厂模式一般是指工厂方法模式。本篇是是工厂方法模式的“小弟”,我们可以将其理解为工厂方法模式的预备知识,它不属于 GoF 23种设计模式,但在软件开发中却也应用地比较频繁。此外,工厂方法模式还有一位“大哥”—抽象工厂模式,会在后面进行介绍。

简单工厂(Simple Factory)模式:定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。因为在简单工厂模式中用于创建实例的方法是静态(static)方法,因此简单工厂模式又被称为静态工厂方法模式,它属于创建型模式。

2.1 要点

简单工厂模式的要点在于:当你需要什么,只需要传入一个正确的参数,就可以获取你所需的对象,而无须知道其创建细节

2.2 结构图

img

简单工厂模式包含 3 个角色:

  • Factory - 工厂角色:该模式的核心,负责实现创建所有产品实例的内部逻辑,提供一个静态的工厂方法 GetProduct(),返回抽象产品类型 Product 的实例。

  • Product - 抽象产品角色:所有产品类的父类,封装了各种产品对象的共有方法,它的引入将提高系统的灵活性,使得在工厂类中只需要定义一个通用的工厂方法,因为所有创建的具体产品对象都是其子类对象。

  • ConcreteProduct - 具体产品角色:简单工厂模式的创建目标,所有被创建的对象都充当这个角色的某个具体类的实例。

    在简单工厂模式中,客户端通过工厂类来创建一个产品类的实例,而无须直接使用 new 关键字来创建对象。(可以看出,它是工厂模式家族中最简单的一员)

情景模拟
1
M公司想要基于C#语言开发一套图表库,该图表库可以为应用系统提供各种不同外观的图标,例如柱状图、饼状图或折线图等。M公司图表库设计开发人员希望为应用系统开发人员提供一套灵活易用的图表库,而且可以较为方便地对图表库进行扩展,以便于在将来增加一些新类型的图表。

2.3 重构图表库的实现

为了将 Chart 类的职责分离,同时将 Chart 对象的创建和使用分离,M 公司开发人员决定使用简单工厂模式对图表库进行重构,重构后的结构图如下所示:

img

代码实现

(1)抽象产品角色:IChartable接口

1
2
3
4
public interface IChartable
{
void Display();
}

(2)具体产品角色:各种图表类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class HistogramChart : IChartable
{
public HistogramChart()
{
Console.WriteLine("创建柱状图...");
}

public void Display()
{
Console.WriteLine("显示柱状图...");
}
}

public class LineChart : IChartable
{
public LineChart()
{
Console.WriteLine("创建折线图...");
}

public void Display()
{
Console.WriteLine("显示折线图...");
}
}

public class PieChart : IChartable
{
public PieChart()
{
Console.WriteLine("创建饼状图...");
}

public void Display()
{
Console.WriteLine("显示饼状图...");
}
}

(3)工厂角色:ChartFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ChartFactory
{
public static IChartable GetChart(string type)
{
IChartable chart = null;

if (type.Equals("histogram", StringComparison.OrdinalIgnoreCase))
{
chart = new HistogramChart();
Console.WriteLine("初始化设置柱状图...");
}
else if (type.Equals("pie", StringComparison.OrdinalIgnoreCase))
{
chart = new PieChart();
Console.WriteLine("初始化设置饼状图...");
}
else if (type.Equals("line", StringComparison.OrdinalIgnoreCase))
{
chart = new PieChart();
Console.WriteLine("初始化设置折线图...");
}

return chart;
}
}

(4)客户端调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void Main()
{
IChartable chart = ChartFactory.GetChart("histogram");
if (chart != null)
{
chart.Display();
}

chart = ChartFactory.GetChart("pie");
if (chart != null)
{
chart.Display();
}
}

在客户端代码中,使用工厂类的静态方法来创建具体产品对象,如果需要更换产品,只需要修改静态工厂方法中的参数即可。例如:将柱状图改为饼状图,只需要将代码:

1
IChartable chart = ChartFactory.GetChart("histogram");

改为:

1
IChartable chart = ChartFactory.GetChart("pie");
改进的方案

M 公司开发人员发现在创建具体 Chart对象时,每次更换一个 Chart对象都需要修改客户端中静态工厂方法的参数,客户端代码需要重新编译,这对于客户端而言,是违反了开闭原则的。于是,开发人员希望有一种方法能够在不修改客户端代码地前提下更换具体产品对象。

因此,他们考虑使用配置文件(XML)来实现:

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="charttype" value="histogram"/>
</appSettings>
</configuration>

客户端因此改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void Main()
{
string type = AppConfigHelper.GetChartType(); // 读取配置文件中的charttype
if (string.IsNullOrEmpty(type))
{
return;
}

IChartable chart = ChartFactory.GetChart(type);
if (chart != null)
{
chart.Display();
}
}

2.4 简单工厂模式总结

主要优点
  • 实现了对象创建和使用的分离:客户端可以免除直接创建产品对象的职责,而仅仅“消费”产品。
  • 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的的参数即可。
  • 通过引入配置文件,可以在不修改任何客户端代码地情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。

主要缺点

  • 由于工厂类集中了所有产品的创建逻辑,职责过重,一旦不能正常工作,整个系统都要受影响。
  • 使用简单工厂模式势必会增加系统中类的个数(引入新的工厂类),增加了系统的复杂度和理解难度。
  • 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,有可能会造成工厂逻辑过于复杂,不利于系统的扩展和维护。
  • 简单工厂模式由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构。
适用场景
  • 工厂类负责创建的对象比较少,由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。
  • 客户端只需要知道传入工厂类的参数,对于如何创建对象并不关心。

3.工厂模式 Factory Method

在简单工厂模式中只提供一个工厂类,该工厂类需要知道每一个产品对象的创建细节,并决定合适实例化哪一个产品类。其最大的缺点就是当有新产品加入时,必须修改工厂类,需要在其中加入必要的业务逻辑,这违背了开闭原则。此外,在简单工厂模式中,所有的产品都由同一个工厂创建,工厂类职责较重,业务逻辑较为复杂,具体产品与工厂类之间的耦合度较高,严重影响了系统的灵活性和扩展性。

在工厂方法模式中,不再提供一个统一的工厂类来创建所有的产品对象,而是针对不同的产品提供不同的工厂,系统提供一个与产品等级结构对应的工厂等级结构

工厂方法(Factory Method)模式:定义一个用于创建对象的接口,让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类。工厂方法模式又简称为工厂模式,也可称为多态工厂模式,它是一种创建型模式。

3.1 结构图

工厂方法模式提供一个抽象工厂接口来声明抽象工厂方法,而由其子类来具体实现工厂方法并创建具体的产品对象。

img

从图中可以看出,在工厂方法模式结构图中包含以下 4 个角色:

  • Product(抽象产品):定义产品的接口,是工厂方法模式所创建的对象的超类,也就是产品对象的公共父类。
  • ConcreteProduct(具体产品):它实现了抽象产品接口,某种类型的具体产品由专门的具体工厂创建,具体工厂和具体产品之间一一对应。
  • Factory(抽象工厂):抽象工厂类,声明了工厂方法,用于返回一个产品。
  • ConcreteFactory(具体工厂):抽象工厂的子类,实现了抽象工厂中定义的工厂方法,并可由客户端调用,返回一个具体产品类的实例。
情景模拟
1
M公司欲开发一个系统运行日志记录器(Logger),该记录器可以通过多种途径保存系统的运行日志,例如通过文件记录或数据库记录,用户可以通过修改配置文件灵活地更换日志记录方式。在设计各类日志记录器时,M公司的开发人员发现需要对日志记录器进行一些初始化工作,初始化参数的摄制过程比较复杂,而且某些参数的设置有严格的先后次序,否则可能会发生记录失败。如何封装记录器的初始化过程并保证多种记录器切换的灵活性是M公司开发人员面临的一个难题。

3.2 工厂方法的实现

基本结构图

img

其中,Logger接口充当抽象产品角色,而 FileLoggerDatabaseLogger则充当具体产品角色。LoggerFactory接口充当抽象工厂角色,而 FileLoggerFactoryDatabaseLoggerFactory则充当具体工厂角色。

重构代码

抽象产品:ILogger接口

1
2
3
4
public interface ILogger
{
void WriteLog();
}

具体产品:FileLoggerDatabaseLogger

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class FileLogger : ILogger
{
public void WriteLog()
{
Console.WriteLine("文件日志记录...");
}
}

public class DatabaseLogger : ILogger
{
public void WriteLog()
{
Console.WriteLine("数据库日志记录...");
}
}

抽象工厂:ILoggerFactory接口

1
2
3
4
public interface ILoggerFactory
{
ILogger CreateLogger();
}

具体工厂:FileLoggerFactoryDatabaseLoggerFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class FileLoggerFactory : ILoggerFactory
{
public ILogger CreateLogger()
{
// 创建文件日志记录器
ILogger logger = new FileLogger();
// 创建文件,代码省略
return logger;
}
}

public class DatabaseLoggerFactory : ILoggerFactory
{
public ILogger CreateLogger()
{
// 连接数据库,代码省略
// 创建数据库日志记录器对象
ILogger logger = new DatabaseLogger();
// 初始化数据库日志记录器,代码省略
return logger;
}
}

客户端调用

1
2
3
4
5
6
7
8
9
10
11
public static void Main()
{
ILoggerFactory factory = new FileLoggerFactory(); // 可通过引入配置文件实现
if (factory == null)
{
return;
}

ILogger logger = factory.CreateLogger();
logger.WriteLog();
}

3.3 借助反射的重构版本

逃离修改客户端的折磨

为了让系统具有更好的灵活性和可扩展性,M 公司程序猿决定对日志记录器客户端代码进行重构,使得可以在不修改任何客户端代码的基础之上更换或是增加新的日志记录方式。

在客户端代码中将不再使用 new 关键字来创建工厂对象,而是将具体工厂类的类名存在配置文件(例如 XML 文件)中,通过读取配置文件来获取类名,再借助.NET 反射机制来动态地创建对象实例。

撸起袖子开始重构

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="LoggerFactory" value="Manulife.ChengDu.DesignPattern.FactoryMethod.v2.DatabaseLoggerFactory, Manulife.ChengDu.DesignPattern.FactoryMethod" />
</appSettings>
</configuration>

封装一个简单的 AppConfigHelper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class AppConfigHelper
{
public static string GetLoggerFactoryName()
{
string factoryName = null;
try
{
factoryName = System.Configuration.ConfigurationManager.AppSettings["LoggerFactory"];
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
return factoryName;
}

public static object GetLoggerFactoryInstance()
{
string assemblyName = AppConfigHelper.GetLoggerFactoryName();
Type type = Type.GetType(assemblyName);

var instance = Activator.CreateInstance(type);
return instance;
}
}

重构客户端代码

1
2
3
4
5
6
7
8
9
10
11
public static void Main()
{
ILoggerFactory factory = (ILoggerFactory)AppConfigHelper.GetLoggerFactoryInstance();
if (factory == null)
{
return;
}

ILogger logger = factory.CreateLogger();
logger.WriteLog();
}

3.4 工厂方法的隐藏

有时候,为了进一步简化客户端的使用,还可以对客户端隐藏工厂方法,此时,在工厂类中将直接调用产品类的业务方法,客户端无须调用工厂方法创建产品,直接通过工厂即可使用所创建的对象中的业务方法。

修改抽象工厂

1
2
3
4
5
6
7
8
9
10
11
public abstract class LoggerFactory
{
// 在工厂类中直接调用日志记录器的业务方法WriteLog()
public void WriteLog()
{
ILogger logger = this.CreateLogger();
logger.WriteLog();
}

public abstract ILogger CreateLogger();
}

修改具体工厂

1
2
3
4
5
6
7
8
9
10
11
public class DatabaseLoggerFactory : LoggerFactory
{
public override ILogger CreateLogger()
{
// 连接数据库,代码省略
// 创建数据库日志记录器对象
ILogger logger = new DatabaseLogger();
// 初始化数据库日志记录器,代码省略
return logger;
}
}

简化的客户端调用

1
2
3
4
5
6
7
8
9
10
public static void Main()
{
LoggerFactory factory = (LoggerFactory)AppConfigHelper.GetLoggerFactoryInstance();
if (factory == null)
{
return;
}

factory.WriteLog();
}

3.5 工厂方法模式总结

主要优点
  • 工厂方法用于创建客户所需要的产品,还向客户隐藏了哪种具体产品类将被实例化这一细节。因此,用户只需要关心所需产品对应的工厂,无须关心创建细节
  • 在系统中加入新产品时,无需修改抽象工厂和抽象产品提供的接口,也无须修改客户端,还无须修改其他的具体工厂和具体产品,而只要加入一个具体工厂和具体产品就可以了。因此,系统的可扩展性得到了保证,符合开闭原则
主要缺点
  • 在添加新产品时,需要编写新的具体产品类,还要提供与之对应的具体工厂类,系统中类的个数将成对增加,一定程度上增加了系统的复杂度
  • 由于考虑到系统的可扩展性,需要引入抽象层,且在实现时可能需要用到反射等技术,增加了系统的实现难度。
适用场景
  • 客户端不知道其所需要的对象的类。在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的的工厂即可,具体的产品对象由具体工厂创建,可将具体工厂的类名存储到配置文件或数据库中。
  • 抽象工厂类通过其子类来指定创建哪个对象。在工厂方法模式中,抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏替换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统易于扩展。

-EOF

查看最新版,请访问本文链接:https://blog.onehat.cn/p/5d0c.html

原创作品,转载请保留出处。

评论