1. 创建型设计模式
1.1. Singleton单例模式-只有一个实例
从“单例”字面意思上理解为——一个类只有一个实例,所以单例模式也就是保证一个类只有一个实例的一种实现方法。
官方定义:确保一个类只有一个实例,并提供一个全局访问点。
为了帮助大家更好地理解单例模式,大家可以结合下面的类图来进行理解,以及后面也会剖析单例模式的实现思路:
/// <summary>
/// 单例模式的实现
/// </summary>
public class Singleton
{
// 定义一个静态变量来保存类的实例
private static Singleton uniqueInstance;
// 定义私有构造函数,使外界不能创建该类实例
private Singleton()
{
}
/// <summary>
/// 定义公有方法提供一个全局访问点,同时你也可以定义公有属性来提供全局访问点
/// </summary>
/// <returns></returns>
public static Singleton GetInstance()
{
// 如果类的实例不存在则创建,否则直接返回
if (uniqueInstance == null)
{
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
上面的单例模式的实现在单线程下确实是完美的,然而在多线程的情况下会得到多个Singleton实例。如下所示:
public class Singleton
{
private static Singleton uniqueInstance;
private Singleton()
{
Console.WriteLine("产生了新的对象!!!");
}
public static Singleton GetInstance()
{
if (uniqueInstance == null)
{
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
class Program
{
static void Main(string[] args)
{
// 多线程集合
List<Thread> threadList = new List<Thread>();
for (int i = 0; i < 20; i++)
{
// lambda表达式方式,线程添加方法
threadList.Add(new Thread(() =>
{
Singleton.GetInstance();
}));
}
// 启用线程
foreach (var item in threadList)
{
item.Start();
}
}
}
因为在两个线程同时运行GetInstance方法时,此时两个线程判断(uniqueInstance == null)这个条件时都返回真,
此时两个线程就都会创建Singleton的实例,这样就违背了我们单例模式初衷了,既然上面的实现会运行多个线程执行,
那我们对于多线程的解决方案自然就是使GetInstance方法在同一时间只运行一个线程运行就好了,
也就是我们线程同步的问题了(对于线程同步大家也可以参考我线程同步的文章),具体的解决多线程的代码如下:
/// <summary>
/// 单例模式的实现
/// </summary>
public class Singleton
{
// 定义一个静态变量来保存类的实例
private static Singleton uniqueInstance;
// 定义一个标识确保线程同步
private static readonly object locker = new object();
// 定义私有构造函数,使外界不能创建该类实例
private Singleton()
{
}
/// <summary>
/// 定义公有方法提供一个全局访问点,同时你也可以定义公有属性来提供全局访问点
/// </summary>
/// <returns></returns>
public static Singleton GetInstance()
{
// 当第一个线程运行到这里时,此时会对locker对象 "加锁",
// 当第二个线程运行该方法时,首先检测到locker对象为"加锁"状态,该线程就会挂起等待第一个线程解锁
// lock语句运行完之后(即线程运行完之后)会对该对象"解锁"
lock (locker)
{
// 如果类的实例不存在则创建,否则直接返回
if (uniqueInstance == null)
{
uniqueInstance = new Singleton();
}
}
return uniqueInstance;
}
}
上面这种解决方案确实可以解决多线程的问题,但是上面代码对于每个线程都会对线程辅助对象locker加锁之后再判断实例是否存在,
对于这个操作完全没有必要的,因为当第一个线程创建了该类的实例之后,
后面的线程此时只需要直接判断(uniqueInstance==null)为假,此时完全没必要对线程辅助对象加锁之后再去判断,
所以上面的实现方式增加了额外的开销,损失了性能,为了改进上面实现方式的缺陷,
我们只需要在lock语句前面加一句(uniqueInstance==null)的判断就可以避免锁所增加的额外开销,
这种实现方式我们就叫它 “双重锁定”,下面具体看看实现代码的:
/// <summary>
/// 单例模式的实现
/// </summary>
public class Singleton
{
// 定义一个静态变量来保存类的实例
private static Singleton uniqueInstance;
// 定义一个标识确保线程同步
private static readonly object locker = new object();
// 定义私有构造函数,使外界不能创建该类实例
private Singleton()
{
}
/// <summary>
/// 定义公有方法提供一个全局访问点,同时你也可以定义公有属性来提供全局访问点
/// </summary>
/// <returns></returns>
public static Singleton GetInstance()
{
// 当第一个线程运行到这里时,此时会对locker对象 "加锁",
// 当第二个线程运行该方法时,首先检测到locker对象为"加锁"状态,该线程就会挂起等待第一个线程解锁
// lock语句运行完之后(即线程运行完之后)会对该对象"解锁"
// 双重锁定只需要一句判断就可以了
if (uniqueInstance == null)
{
lock (locker)
{
// 如果类的实例不存在则创建,否则直接返回
if (uniqueInstance == null)
{
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
1.1.1. 应用场景
实际应用:
- Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗?
- 网站的计数器,一般也是采用单例模式实现,否则难以同步。
- 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
- Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
- 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
- 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
- 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
- HttpApplication 也是单位例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例.
单例模式应用的场景一般发现在以下条件下:
- 资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。
- 控制资源的情况下,方便资源之间的互相通信。如线程池等。
1.1.2. 练习-单例改造
在下面的TicketMaker类中,每次调用GetNextTicketNumber方法都会返回1000,1001,1002...的数列。
我们可以用它生成票的编号或是其他序列号。
在现在该类的实现方式下,我们可以生成多个该类的实例。
请修改代码,运用Singleton模式确保只能生成一个该类的实例。
【非单例模式的TicketMaker.cs】
public class TicketMaker
{
private int ticket = 1000;
public int GetNextTicketNumber()
{
return ticket++;
}
}
【进行单例的改造】
public class TicketMaker
{
private TicketMaker(){}
private int ticket = 1000;
static TicketMaker ticker;
static readonly object lockObj = new object();
public static TicketMaker Ticket
{
get
{
if (ticker == null)
{
ticker = new TicketMaker();
}
return ticker;
}
}
public int GetNextTicketNumber()
{
return ticket++;
}
}
但是上述代码仍然存在问题,在多线程的情况,仍会有多个实例对象,获取得到的ticket值并不能保证唯一
【多线程的调用】
static void Main(string[] args)
{
Thread th1 = new Thread(Test);
th1.Start();
Test();
}
/// <summary>
/// 测试调用
/// </summary>
static void Test()
{
for (int i = 0; i < 10; i++)
{
//Thread.Sleep(10);
Console.WriteLine(TicketMaker.Ticket.GetNextTicketNumber());
}
}
上述演示中并不能保证每次结果都是一致的,所以需要进一步修改单例的实现。
【TicketMaker.cs多线程的考虑】
public class TicketMaker
{
private TicketMaker(){}
private int ticket = 1000;
static TicketMaker ticker;
static readonly object lockObj = new object();
public static TicketMaker Ticket
{
get
{
考虑多线程,在多线程的情况下仍为单例
if (ticker == null)
{
lock (lockObj)
{
if (ticker == null)
{
ticker = new TicketMaker();
}
}
}
return ticker;
}
}
public int GetNextTicketNumber()
{
// 需要保证多个线程访问过程中不出现重复编号
lock(lockObj)
{
// 将线程挂起模拟过程,先lock后挂起,否则无法保证编号顺序
Thread.Sleep(10);
return ticket++;
}
}
}
1.1.3. 拓展-关于static方法和单例模式:
1.2. Factory Method工厂方法
1.2.1. Simple Factory简单工厂
引用大话设计模式中的计算器示例:
class Program
{
static void Main(string[] args)
{
//通过简单工厂返回运算类对象,调用方法得到运算结果
Operation op = OperationFactory.CreateOperate("+");
op.NumberA = 1.23;
op.NumberB = 7.65;
Console.WriteLine(op.GetResult());
}
}
/// <summary>
/// 运算类
/// </summary>
public class Operation
{
public double NumberA { get; set; }
public double NumberB { get; set; }
/// <summary>
/// 获取运算结果。虚方法,子类可以重写
/// </summary>
/// <returns></returns>
public virtual double GetResult()
{
return 0;
}
}
/// <summary>
/// 简单工厂类,获取具体操作类型
/// </summary>
public class OperationFactory
{
public static Operation CreateOperate(string operate)
{
Operation opt = null;
switch (operate)
{
case "+":
opt = new OperationAdd();
break;
case "-":
opt = new OperationSub();
break;
}
return opt;
}
}
/// <summary>
/// 加法类
/// </summary>
public class OperationAdd : Operation
{
public override double GetResult()
{
return NumberA + NumberB;
}
}
/// <summary>
/// 减法类
/// </summary>
public class OperationSub : Operation
{
public override double GetResult()
{
return NumberA - NumberB;
}
}
//...后续运算省略...
简单工厂最大的优点在于工厂类中包含了必要的逻辑判断,根据客户端的选择条件动态实例化相关类,对于客户端来说,去除了与具体产品的依赖。
1.2.2. Factory Method工厂方法-将实例的生成交给子类
对于上面简单工厂的示例代码来说,Main方法中并不关心具体调用哪个类的实例,只要传入约定的操作符(+-*/)即可得到运算结果。
但如果需要新增运算类型的话,需要修改到原有的【简单工厂类】,违背了开放-封闭原则。
开放-封闭原则OCP:软件实体(类、模块、函数等)应该可以扩展,但不可以修改。对于扩展是开放的,对于更改已有的类、模块、函数等是封闭的。
针对简单工厂的扩展,诞生了工厂方法,即定义一个用于创建对象的接口,让工厂类决定实例化哪一个类。
工厂方法使一个类的实例化延迟到其子类中。
class Program
{
static void Main(string[] args)
{
IFactory fac = new AddFactory();
Operation op = fac.CreateOperate();
op.NumberA = 1.23;
op.NumberB = 7.65;
Console.WriteLine(op.GetResult());
}
}
/// <summary>
/// 运算类
/// </summary>
public class Operation
{
public double NumberA { get; set; }
public double NumberB { get; set; }
/// <summary>
/// 获取运算结果。虚方法,子类可以重写
/// </summary>
/// <returns></returns>
public virtual double GetResult()
{
return 0;
}
}
/// <summary>
/// 工厂接口,工厂类需要实现的方法
/// </summary>
public interface IFactory
{
Operation CreateOperate();
}
/// <summary>
/// 加法类
/// </summary>
public class OperationAdd : Operation
{
public override double GetResult()
{
return NumberA + NumberB;
}
}
/// <summary>
/// 减法类
/// </summary>
public class OperationSub : Operation
{
public override double GetResult()
{
return NumberA - NumberB;
}
}
/// <summary>
/// 加法工厂类
/// </summary>
public class AddFactory : IFactory
{
public Operation CreateOperate()
{
return new OperationAdd();
}
}
/// <summary>
/// 除法工厂类
/// </summary>
public class SubFactory : IFactory
{
public Operation CreateOperate()
{
return new OperationSub();
}
}
当增加新的运算时,不需要修改原有的工厂类。
比如新增乘法操作,新增OperationMul乘法类,新增MulFactory乘法工厂即可,在客户端的调用如下:
IFactory mulFt = new MulFactory();
Operation optMul = mulFt.CreateOperate();
optMul.NumberA = 7;
optMul.NumberB = 8;
Console.WriteLine(optMul.GetResult());
工厂方法符合OCP开放-封闭原则,但面对需求的更改,不可能不修改代码(此说法并不绝对),关键考虑怎么修改,修改量等因素。
简单工厂中我们是通过工厂类中的switch进行判断具体属于什么操作的。
而工厂方法中,我们把switch的部分去除,让调用者来决定进行调用哪个工厂类进行实例化。
以上,工厂方法是简单工厂的进一步抽象和推广。
但缺点是,每新增一个产品,就需要一个产品工厂类,增加了额外的开发量。
1.3. Abstract Factory模式-抽象工厂 将关联零件组装成产品
抽象工厂模式,提供一个创建一系列相关或互相依赖对象的接口,而无需指定它们具体的类。
以周黑鸭为例,上海人偏甜口,湖南人偏辣口,所以两个地方生产的产品虽然都是鸭脖、鸭翅等,但在细节上有一定的区分。
class Program
{
static void Main(string[] args)
{
ZhouDuckFactory shanghaiFt = new ShangHaiFactory();
Yabo shYabo = shanghaiFt.CreateYabo();
shYabo.Show();
ZhouDuckFactory hunanFt = new HuNanFactory();
Yabo hnYabo = hunanFt.CreateYabo();
hnYabo.Show();
}
}
/// <summary>
/// 鸭脖抽象基类,注意和鸭翅是不同产品
/// </summary>
public abstract class Yabo
{
public abstract void Show();
}
/// <summary>
/// 鸭翅抽象基类,注意和鸭脖是不同产品
/// </summary>
public abstract class Yachi
{
public abstract void Show();
}
/// <summary>
/// 上海鸭脖
/// </summary>
public class ShangHaiYabo : Yabo
{
public override void Show() { Console.WriteLine("偏甜鸭脖"); }
}
/// <summary>
/// 上海鸭翅
/// </summary>
public class ShangHaiYachi : Yachi
{
public override void Show() { Console.WriteLine("偏甜鸭翅"); }
}
/// <summary>
/// 湖南鸭脖
/// </summary>
public class HuNanYabo : Yabo
{
public override void Show() { Console.WriteLine("狠辣鸭脖"); }
}
/// <summary>
/// 湖南鸭翅
/// </summary>
public class HuNanYachi : Yachi
{
public override void Show() { Console.WriteLine("狠辣鸭翅"); }
}
/// <summary>
/// 周黑鸭的抽象工厂
/// </summary>
public abstract class ZhouDuckFactory
{
public abstract Yabo CreateYabo();
public abstract Yachi CreateYachi();
}
/// <summary>
/// 上海工厂
/// </summary>
public class ShangHaiFactory : ZhouDuckFactory
{
public override Yabo CreateYabo()
{
return new ShangHaiYabo();
}
public override Yachi CreateYachi()
{
return new ShangHaiYachi();
}
}
/// <summary>
/// 湖南工厂
/// </summary>
public class HuNanFactory : ZhouDuckFactory
{
public override Yabo CreateYabo()
{
return new HuNanYabo();
}
public override Yachi CreateYachi()
{
return new HuNanYachi();
}
}
现在考虑,周黑鸭在四川授权销售,则对应的新增代码需要有:
//新增四川口味的鸭脖和鸭翅产品
public class SiChuanYabo : Yabo
{
public override void Show() { Console.WriteLine("麻辣鸭脖"); }
}
public class SiChuanYachi : Yachi
{
public override void Show() { Console.WriteLine("麻辣鸭翅"); }
}
//新增四川具体工厂类,实现四川产品的实例化
public class SiChuanFactory : ZhouDuckFactory
{
public override Yabo CreateYabo()
{
return new SiChuanYabo();
}
public override Yachi CreateYachi()
{
return new SiChuanYachi();
}
}
//客户端调用时
ZhouDuckFactory sichuanFt = new SiChuanFactory();
Yabo scYb = sichuanFt.CreateYabo();
scYb.Show();
利用抽象工厂可以很方便的实现新增产品系列,如上例中新增四川口味的鸭脖和鸭翅。
但是如果需要新增产品类型,比如新增鸭锁骨的销售,那么需要修改的地方就比较多了,而且违反了OCP原则。
如需要新增鸭锁骨的销售,需要修改的内容见如下伪代码:
//1、新增鸭锁骨抽象类
public abstract class Yasuogu{ //... }
//2、修改现有的抽象工厂类
public abstract class ZhouDuckFactory
{
//新增产品
public abstract Yasuogu CreateYasuogu();
}
//3、实现抽象工厂派生类中的新增抽象方法。。。
1.3.1. .NET中抽象工厂模式实现
抽象工厂模式在实际中的应用也是相当频繁的,然而在我们.NET类库中也存在应用抽象工厂模式的类。
这个类就是System.Data.Common.DbProviderFactory,这个类位于System.Data.dll程序集中,
该类扮演抽象工厂模式中抽象工厂的角色,我们可以用reflector反编译工具查看该类的实现:
/// <summary>
/// 扮演抽象工厂的角色
/// 创建连接数据库时所需要的对象集合,
/// 这个对象集合包括有 DbConnection对象(这个是抽象产品类,如绝味例子中的YaBo类)、DbCommand类、DbDataAdapter类,
/// 针对不同的具体工厂都需要实现该抽象类中方法,
/// </summary>
public abstract class DbProviderFactory
{
// 提供了创建具体产品的接口方法
protected DbProviderFactory();
public virtual DbCommand CreateCommand();
public virtual DbCommandBuilder CreateCommandBuilder();
public virtual DbConnection CreateConnection();
public virtual DbConnectionStringBuilder CreateConnectionStringBuilder();
public virtual DbDataAdapter CreateDataAdapter();
public virtual DbDataSourceEnumerator CreateDataSourceEnumerator();
public virtual DbParameter CreateParameter();
public virtual CodeAccessPermission CreatePermission(PermissionState state);
}
DbProviderFactory类是一个抽象工厂类,该类提供了创建数据库连接时所需要的对象集合的接口,
实际创建的工作在其子类工厂中进行,微软使用的是SQL Server数据库,
因此提供了连接SQL Server数据的具体工厂实现,具体代码可以用反编译工具查看,具体代码如下:
/// 扮演着具体工厂的角色,用来创建连接SQL Server数据所需要的对象
public sealed class SqlClientFactory : DbProviderFactory, IServiceProvider
{
// Fields
public static readonly SqlClientFactory Instance = new SqlClientFactory();
// 构造函数
private SqlClientFactory()
{
}
// 重写抽象工厂中的方法
public override DbCommand CreateCommand()
{ // 创建具体产品
return new SqlCommand();
}
public override DbCommandBuilder CreateCommandBuilder()
{
return new SqlCommandBuilder();
}
public override DbConnection CreateConnection()
{
return new SqlConnection();
}
public override DbConnectionStringBuilder CreateConnectionStringBuilder()
{
return new SqlConnectionStringBuilder();
}
public override DbDataAdapter CreateDataAdapter()
{
return new SqlDataAdapter();
}
public override DbDataSourceEnumerator CreateDataSourceEnumerator()
{
return SqlDataSourceEnumerator.Instance;
}
public override DbParameter CreateParameter()
{
return new SqlParameter();
}
public override CodeAccessPermission CreatePermission(PermissionState state)
{
return new SqlClientPermission(state);
}
}
因为微软只给出了连接SQL Server的具体工厂的实现,我们也可以自定义连接Oracle、MySql的具体工厂的实现。
不足之处: 抽象工厂模式很难支持新种类产品的变化。
这是因为抽象工厂接口中已经确定了可以被创建的产品集合,如果需要添加新产品,此时就必须去修改抽象工厂的接口,
这样就涉及到抽象工厂类的以及所有子类的改变,这样也就违背了“开放—封闭”OCP原则。
1.3.2. 小结
简单工厂
简单工厂模式的工厂类一般是使用静态方法,通过接收的参数的不同来返回不同的对象实例。不修改代码的话,是无法扩展的。
工厂方法
工厂方法是针对每一种产品提供一个工厂类。通过不同的工厂实例来创建不同的产品实例。在同一等级结构中,支持增加任意产品。
抽象工厂
抽象工厂是应对产品族概念的。比如说,每个汽车公司可能要同时生产轿车,货车,客车,那么每一个工厂都要有创建轿车,货车和客车的方法。
应对产品族概念而生,增加新的产品线很容易,但是无法增加新的产品。
对比工厂方法和抽象工厂
工厂方法:
- 一个抽象产品类,可以派生出多个具体产品类。
- 一个抽象工厂类,可以派生出多个具体工厂类。
- 每个具体工厂类只能创建一个具体产品类的实例。
抽象工厂:
- 多个抽象产品类,每个抽象产品类可以派生出多个具体产品类。
- 一个抽象工厂类,可以派生出多个具体工厂类。
- 每个具体工厂类可以创建多个具体产品类的实例。
对比发现:
- 工厂方法模式只有一个抽象产品类,而抽象工厂模式有多个。
- 工厂方法模式的具体工厂类只能创建一个具体产品类的实例,而抽象工厂模式可以创建多个。
参考引用: