一个很好的学习设计模式的网站:链接
创建型模式提供了创建对象的机制, 能够提升已有代码的灵活性和可复用性。创建型设计模式包括以下五种:
单例模式:让你能保证一个类只有一个实例,并提供一个访问该实例的全局节点。
工厂模式:在父类中提供一个创建对象的接口以允许子类决定实例化对象的类型。
抽象工厂模式:让你能创建一系列相关的对象,而无需指定其具体类。
生成器模式(建造者模式): 使你能够分步骤创建复杂对象。 该模式允许你使用相同的创建代码生成不同类型和形式的对象。
原型模式:使你能够复制已有对象, 而又无需使代码依赖它们所属的类。
单例模式的主要作用是确保一个类只有一个实例存在。如果你的代码能够访问单例类, 那它就能调用单例类的静态方法,无论何时调用该方法, 它总是会返回相同的对象。
为什么会有人想要控制一个类所拥有的实例数量? 最常见的原因是控制某些共享资源 (例如数据库或文件) 的访问权限。
它的运作方式是这样的: 如果你创建了一个对象, 同时过一会儿后你决定再创建一个新对象, 此时你会获得之前已创建的对象, 而不是一个新对象。
注意, 普通构造函数无法实现上述行为, 因为构造函数的设计决定了它必须总是返回一个新对象。
和全局变量一样, 单例模式也允许在程序的任何地方访问特定对象,它可以保护该实例不被其他代码覆盖。
单例模式禁止通过除特殊构建方法以外的任何方式来创建自身类的对象。 该方法可以创建一个新对象, 但如果该对象已经被创建, 则返回已有的对象。
单例模式与全局变量不同, 它保证类只存在一个实例。 除了单例类自己以外, 无法通过任何方式替换缓存的实例
步骤如下:
1. 将该类的构造函数设为私有,防止其他对象使用该单例类的new 运算符。
建立当前类的一个静态成员变量,该变量用于保存创建的单例类对象。
建立一个静态构建方法用于向外界提供当前类的实例,该方法是外界获取该单例对象的唯一方式。
在客户端代码中, 将对单例的构造函数的调用替换为对其静态构建方法的调用。
代码实现:
根据实例化的时机不同,单例模式可以分为饿汉式和懒汉式:
- 饿汉式:在类加载的时候就进行实例化,代码实现如下:
using System;
using System.Dynamic;
using System.Text;
namespace 单例模式
{
//饿汉式实现,在此处类加载的时候就就进行实例化
public class Singleton
{
private Singleton() //1.将构造函数设为私有
{
}
private static Singleton singleton = new Singleton();//2.建立当前类的一个静态成员变量;
public static Singleton GetInstance()//3.建立一个静态构建方法用于向外界提供当前类的实例
{
return singleton;
}
}
public class Client
{
public static void Main(string[] args)
{
Singleton s1 = Singleton.GetInstance(); //4.在客户端类中,将对单例的构造函数的调用替换为对其静态构建方法的调用。
Singleton s2 = Singleton.GetInstance();
if (s1 == s2)
{
Console.WriteLine("s1和s2是相同的两个对象");
}
else
{
Console.WriteLine("s1和s2不是相同的两个对象");
}
}
}
}
输出结果:
s1和s2是相同的两个对象
- 懒汉式:在刚开始类加载的时候不进行实例化,在第一次使用的时候进行实例化,代码实现如下:
using System;
using System.Dynamic;
using System.Text;
namespace 单例模式
{
//懒汉式实现:在第一次使用的时候进行实例化
public class Singleton
{
private Singleton() //1.将构造函数设为私有
{
}
private static Singleton singleton;//2.建立当前类的一个静态成员变量;
public static Singleton GetInstance()//3.建立一个静态构建方法用于向外界提供当前类的实例
{
if (singleton == null)
{
singleton= new Singleton();
}
return singleton;
}
}
public class Client
{
public static void Main(string[] args)
{
Singleton s1 = Singleton.GetInstance(); //4.在客户端类中,将对单例的构造函数的调用替换为对其静态构建方法的调用。
Singleton s2 = Singleton.GetInstance();
if (s1 == s2)
{
Console.WriteLine("s1和s2是相同的两个对象");
}
else
{
Console.WriteLine("s1和s2不是相同的两个对象");
}
}
}
}
输出结果:
s1和s2是相同的两个对象
由于懒汉式是在方法中进行的初始化,在多线程环境下,可能会出现问题,可以试想一下,如果这个时候有多个线程同时调用了getInstance()
方法,那么会出现什么问题呢?
可以看到,在多线程环境下,如果三条线程同时调用getInstance()
方法,会同时进行INSTANCE == null
的判断,那么此时由于确实还没有进行任何实例化,所以导致三条线程全部判断为true
(而饿汉式由于在类加载时就创建完成,不会存在这样的问题)此时问题就来了,我们既然要使用单例模式,那么肯定是只希望对象只被初始化一次的,但是现在由于多线程的机制,导致对象被多次创建。
所以,为了避免线程安全问题,针对于懒汉式单例,还需要做出一些代码方面的改进,才能避免线程安全问题。
优点:
缺点:
我们知道,如果需要创建一个对象,那么最简单的方式就是直接new一个即可。而工厂方法模式代替了传统的直接new的形式,那么为什么要替代传统的new形式呢?
可以想象一下,如果所有的对象我们都通过new的方式去创建,那么当我们的程序中大量使用此对象时,突然有一天这个对象的构造方法或是类名发生了修改,那我们岂不是得挨个去进行修改?根据迪米特法则,我们应该尽可能地少与其他类进行交互,所以我们可以将那些需要频繁出现的对象创建,封装到一个工厂类中,当我们需要对象时,直接调用工厂类中的工厂方法来为我们生成对象,这样,就算类出现了变动,我们也只需要修改工厂中的代码即可,而不是大面积地进行修改。下面首先讲述简单工厂模式,再讲述工厂模式:
又称静态工厂方法模式,它属于类创建模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都有共同的父类(这个父类一般是抽象类)。
简单举个例子:我们都直到工厂是根据产品的图纸生产产品的,那简单工厂模式中,工厂类就是负责专门创建其他类实例的类,它称之为工厂类,它直到每一个产品,它决定哪一个产品类应该被实例化。
优点:
缺点:
namespace ConsoleApp4
{
///
/// 抽象的产品类
///
public abstract class Product
{
public abstract void Print();
}
///
/// 产品A类
///
public class ProductA : Product
{
public override void Print()
{
Console.WriteLine("产品A");
}
}
///
/// 产品B类
///
public class ProductB : Product
{
public override void Print()
{
Console.WriteLine("产品B");
}
}
///
/// 简单工厂类
///
public class SimpleSingleton
{
public static Product createProduct(string arg)
{
if (arg == "A")
{
return new ProductA();
}else
{
return new ProductB();
}
}
public static void Main()
{
Product product = SimpleSingleton.createProduct("A");
product.Print();
}
}
}
不过这样还是有一些问题,我们前面提到了开闭原则,一个软件实体,比如类、模块和函数应该对扩展开放,对修改关闭,但是如果我们现在需要新增一种水果,比如桃子,那么这时我们就得去修改工厂提供的工厂方法了,但是这样是不太符合开闭原则的,因为工厂实际上是针对于调用方提供的,所以我们应该尽可能对修改关闭。
所以,我们就利用对扩展开放,对修改关闭的性质,将简单工厂模式改进为工厂方法模式,那现在既然不让改,那么我们就看看如何去使用扩展的形式:下面就使用工厂模式来创建对象。
定义一个用于创建对象的接口,让子类决定实例化哪个类,工厂方法使一个类的实例化延迟到其子类。
在工厂模式中,之前简单工厂模式中的工厂变成了一个抽象接口,他负责给出工厂应该实现的方法,它不再负责所有产品的创建,而是将具体的产品创建工作交给子类去做,这样子就诞生了具体的子工厂,子工厂即子类,负责生成具体的产品对象,这样子就可以将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体的实例类。在工厂模式中,如果要新增一个产品,不需要修改原有的工厂类逻辑,而是需要新增一个工厂。
优点:
缺点:
namespace ConsoleApp4
{
///
/// 产品接口
///
public interface IProduct
{
public void Print();
}
public class ProductA : IProduct
{
public void Print()
{
Console.WriteLine("ProductA");
}
}
public class ProductB : IProduct
{
public void Print()
{
Console.WriteLine("ProductB");
}
}
public interface IFactory
{
IProduct CreateProduct();
}
public class ProductAFactory : IFactory
{
public IProduct CreateProduct()
{
return new ProductA();
}
}
public class ProductBFactory : IFactory
{
public IProduct CreateProduct()
{
return new ProductB();
}
}
public class Program
{
public static void Main()
{
IFactory factoryA = new ProductAFactory();
IFactory factoryB = new ProductBFactory();
IProduct productA = factoryA.CreateProduct();
IProduct productB = factoryB.CreateProduct();
productA.Print();
productB.Print();
Console.Read();
}
}
}
这样,我们就简单实现了工厂方法模式,通过工厂来屏蔽对象的创建细节,使用者只需要关心如何去使用对象即可。
工厂方法将创建产品的代码与实际使用产品的代码分离, 从而能在不影响其他代码的情况下扩展产品创建部分代码。
例如, 如果需要向应用中添加一种新产品, 你只需要开发新的创建者子类, 然后重写其工厂方法即可。
继承可能是扩展软件库或框架默认行为的最简单方法。 但是当你使用子类替代标准组件时, 框架如何辨识出该子类?
解决方案是将各框架中构造组件的代码集中到单个工厂方法中, 并在继承该组件之外允许任何人对该方法进行重写。
让我们看看具体是如何实现的。 假设你使用开源 UI 框架编写自己的应用。 你希望在应用中使用圆形按钮, 但是原框架仅支持矩形按钮。 你可以使用 圆形按钮
RoundButton子类来继承标准的 按钮
Button类。 但是, 你需要告诉 UI框架
UIFramework类使用新的子类按钮代替默认按钮。
为了实现这个功能, 你可以根据基础框架类开发子类 圆形按钮 UI
UIWithRoundButtons , 并且重写其 createButton
创建按钮方法。 基类中的该方法返回 按钮
对象, 而你开发的子类返回 圆形按钮
对象。 现在, 你就可以使用 圆形按钮 UI
类代替 UI框架
类。 就是这么简单!
在处理大型资源密集型对象 (比如数据库连接、 文件系统和网络资源) 时, 你会经常碰到这种资源需求。
让我们思考复用现有对象的方法:
这些代码可不少! 而且它们必须位于同一处, 这样才能确保重复代码不会污染程序。
可能最显而易见, 也是最方便的方式, 就是将这些代码放置在我们试图重用的对象类的构造函数中。 但是从定义上来讲, 构造函数始终返回的是新对象, 其无法返回现有实例。
因此, 你需要有一个既能够创建新对象, 又可以重用现有对象的普通方法。 这听上去和工厂方法非常相像。
抽象工厂模式是一种创建型设计模式, 它能创建一系列相关的对象, 而无需指定其具体类。
前面我们介绍了工厂方法模式,通过定义顶层抽象工厂类,通过继承的方式,针对于每一个产品都提供一个工厂类用于创建。
不过这种模式只适用于简单对象,当我们需要生产许多个产品族的时候,这种模式就有点乏力了,比如,有这样一个需求:某4S店需要订制一批普通轿车和SUV,针对低端客户准备以比亚迪为主,针对高端用户准备以保时捷为主,以后还可能订制其他品牌的车型;
这里我们就可以开始思考了,首先比亚迪和保时捷制造轿车和SUV是不会变得,变的是客户的需求,可能今天要比亚迪,明天又需要保时捷,后面又需要宝马、奔驰等汽车了:
实际上这些产品都是成族出现的,比如比亚迪的生产线上有汉和宋两种产品,保时捷的产品线上有帕拉梅拉和卡宴两种产品, 但是如果按照我们之前工厂方法模式来进行设计,那就需要单独设计4个工厂来生产上面这些产品,显然这样就比较浪费时间的。
但是现在有什么方法能够更好地处理这种情况呢?我们就可以使用抽象工厂模式,我们可以将多个产品,都放在一个工厂中进行生成,按不同的产品族进行划分,比如比亚迪,那么我就可以安排一个比亚迪工厂,而这个工厂里面就可以生产整条产品线上的内容,包括汉和宋等;比对于保时捷,那么我就可以安排一个保时捷工厂,而这个工厂里面就可以生产整条产品线上的内容,包括帕拉梅拉和卡宴等。代码如下:
public interface ICar
{
void ShowCarName();
}
public interface ISuv
{
void ShowSuvName();
}
public class BYDCar : ICar
{
public void ShowCarName()
{
Console.WriteLine("比亚迪-汉");
}
}
public class BYDSuv : ISuv
{
public void ShowSuvName()
{
Console.WriteLine("比亚迪-宋");
}
}
public class PorscheCar : ICar
{
public void ShowCarName()
{
Console.WriteLine("保时捷-帕拉梅拉");
}
}
public class PorscheSuv : ISuv
{
public void ShowSuvName()
{
Console.WriteLine("保时捷-卡宴");
}
}
public interface IFactory
{
ICar CreateCar();
ISuv CreateSuv();
}
///
/// 比亚迪工厂,将生产小汽车和SUV
///
public class BYDFactory : IFactory
{
public ICar CreateCar()
{
return new BYDCar();
}
public ISuv CreateSuv()
{
return new BYDSuv();
}
}
///
/// 保时捷工厂,将生产小汽车和SUV
///
public class PorscheFactory : IFactory
{
public ICar CreateCar()
{
return new PorscheCar();
}
public ISuv CreateSuv()
{
return new PorscheSuv();
}
}
public class Client
{
public static void Main()
{
//比亚迪工厂制作的小汽车和SUV
IFactory bYDFactory = new BYDFactory();
bYDFactory.CreateCar().ShowCarName();
bYDFactory.CreateSuv().ShowSuvName();
Console.WriteLine("-------------------");
//保时捷工厂制作的小汽车和SUV
IFactory porscheFactory = new PorscheFactory();
porscheFactory.CreateCar().ShowCarName();
porscheFactory.CreateSuv().ShowSuvName();
}
}
输出结果:
比亚迪-汉
比亚迪-宋
-------------------
保时捷-帕拉梅拉
保时捷-卡宴
这样,假如后续4S店需要订制其他品牌的车型,我们就只需要抽象其他品牌的车辆工厂及对应的车辆即可。
一个工厂可以生产同一个产品族的所有产品,这样按族进行分类,显然比之前的工厂方法模式更好。
不过,缺点还是有的,如果产品族新增了产品,那么我就不得不去为每一个产品族的工厂都去添加新产品的生产方法,违背了开闭原则。
建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
一个 Builder 类会一步一步构造最终的对象。该 Builder 类是独立于其他对象的。
意图: 将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
主要解决: 主要解决在软件系统中,有时候面临着"一个复杂对象"的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。
何时使用: 一些基本部件不会变,而其组合经常变化的时候。
如何解决: 将变与不变分离开。
关键代码: 建造者:创建和提供实例,导演:管理建造出来的实例的依赖关系。
应用实例: 1、去肯德基,汉堡、可乐、薯条、炸鸡翅等是不变的,而其组合是经常变化的,生成出所谓的"套餐"。 2、JAVA 中的 StringBuilder。
优点: 1、建造者独立,易扩展。 2、便于控制细节风险。
缺点: 1、产品必须有共同点,范围有限制。 2、如内部变化复杂,会有很多的建造类。
使用场景: 1、需要生成的对象具有复杂的内部结构。 2、需要生成的对象内部属性本身相互依赖。
注意事项: 与工厂模式的区别是:建造者模式更加关注与零件装配的顺序。
代码如下:
using System;
using System.Collections.Generic;
namespace StrategyPattern
{
///
/// 抽象的Builder,ICarBuilder接口提供步骤的实现
///
public interface ICarBuilder
{
void BuilderWheels();
void BuilderWindows();
}
//普通汽车的Builder
public class BasicBuilder : ICarBuilder
{
public void BuilderWheels()
{
Console.WriteLine("建造普通轮子");
}
public void BuilderWindows()
{
Console.WriteLine("建造普通窗子");
}
}
//高级汽车的Builder
public class AdvancedBuilder : ICarBuilder
{
public void BuilderWheels()
{
Console.WriteLine("建造高级轮子");
}
public void BuilderWindows()
{
Console.WriteLine("建造高级窗子");
}
}
///
/// 创建汽车建造工厂,它使用Builder来创建不同种类的汽车
///
public class CarFactory
{
public ICarBuilder Builder { get; set; }
public CarFactory(ICarBuilder builder)
{
this.Builder = builder;
}
public void CreateCar()
{
Builder.BuilderWheels();
Builder.BuilderWindows();
}
}
///
/// 客户端类调用
///
public class Client
{
public static void Main()
{
CarFactory carFactory = new CarFactory(new BasicBuilder());
carFactory.CreateCar();
carFactory.Builder=new AdvancedBuilder();
carFactory.CreateCar();
Console.ReadKey();
}
}
输出结果:
建造普通轮子
建造普通窗子
建造高级轮子
建造高级窗子
原型模式其实就是从一个对象再创建另外一个可定制的对象,而且不需知道任何创建的细节。
因为克隆比较常用,所以.NET在System命名空间中提供了ICloneable接口,其中就是唯一的一个方法Clone();只需要实现这个接口就可以完成原型模式了。
区分 引用拷贝、浅拷贝和深拷贝:
浅拷贝代码举例,代码如下:
using System;
namespace PrototypePattern
{
public class Person : ICloneable
{
private string _name="小明";
public string Name
{
get { return _name; }
set { _name = value; }
}
private string _sex="男";
public string Sex
{
get { return _sex; }
set { _sex = value; }
}
private int _age=18;
public int Age
{
get { return _age; }
set { _age = value; }
}
private List<string> _hobbies=new List<string>() { "唱歌","跳舞","做饭"};
public List<string> Hobbies
{
get { return _hobbies; }
set { _hobbies = value; }
}
public void Show()
{
string showHobbies = "";
foreach (var item in Hobbies)
{
showHobbies += item.ToString();
}
Console.WriteLine("姓名:{0},性别:{1},年龄:{2},爱好:{3}", Name, Sex, Age, showHobbies);
}
//克隆信息
public object Clone()
{
return (object)this.MemberwiseClone();//MemberwiseClone()这个方法就是创建当前对象的浅表副本
}
}
public class Client
{
public static void Main()
{
Person person1 = new Person();
person1.Hobbies.Add("爬山");
person1.Show();
Person person2 = (Person)person1.Clone();//将person1的信息拷贝至person2上
person2.Show();
}
}
}
输出结果:
姓名:小明,性别:男,年龄:18,爱好:唱歌跳舞做饭爬山
姓名:小明,性别:男,年龄:18,爱好:唱歌跳舞做饭爬山
上面Clone()方法就是实现的接口的方法,用来克隆对象。
MemberwiseClone()这个方法就是创建当前对象的浅表副本。
深拷贝例子:
针对上面代码进行修改,
using System;
namespace PrototypePattern
{
public class Person : ICloneable
{
private string _name="小明";
public string Name
{
get { return _name; }
set { _name = value; }
}
private string _sex="男";
public string Sex
{
get { return _sex; }
set { _sex = value; }
}
private int _age=18;
public int Age
{
get { return _age; }
set { _age = value; }
}
private List<string> _hobbies=new List<string>() { "唱歌","跳舞","做饭"};
public List<string> Hobbies
{
get { return _hobbies; }
set { _hobbies = value; }
}
public void Show()
{
string showHobbies = "";
foreach (var item in Hobbies)
{
showHobbies += item.ToString();
}
Console.WriteLine("姓名:{0},性别:{1},年龄:{2},爱好:{3}", Name, Sex, Age, showHobbies);
}
//克隆信息
public object Clone()
{
Person p= (Person)this.MemberwiseClone();//MemberwiseClone()这个方法就是创建当前对象的浅表副本
p.Hobbies = new List<string>();
if (Hobbies != null)
{
foreach (var item in Hobbies)
{
p.Hobbies.Add(item);
}
}
return p;
}
}
public class Client
{
public static void Main()
{
Person person1 = new Person();
person1.Hobbies.Add("爬山");
person1.Show();
Person person2 = (Person)person1.Clone();//将person1的信息拷贝至person2上
person2.Show();
}
}
}
输出结果:
姓名:小明,性别:男,年龄:18,爱好:唱歌跳舞做饭爬山
姓名:小明,性别:男,年龄:18,爱好:唱歌跳舞做饭爬山
在C#中,string类型和数组也属于引用类型,而string类型比较特殊(具体可查资料),因此只需要手动克隆List类型的Hobbies属性即可实现深克隆。
开闭原则:程序对于扩展是开放的,对于修改是封闭的。
举个生活中的例子:比如我有一个生产拖拉机的工厂,现在我要引入一个新的产品:口罩,那我是直接修改拖拉机的生产线使其能生产口罩好呢,还是直接新建一个用于生产口罩的工厂线呢。举这个例子,也是表达面向对象程序设计的一个观点:扩展优于修改。想要达到这样的效果,我们需要使用接口和抽象类。
一个类只负责一个功能领域中相应的职责。
一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。
任何基类可以出现的地方,子类一定可以出现。
针对接口编程,依赖于抽象而不依赖于具体。
使用多个隔离的接口,比使用单个接口要好,这是一个降低类之间的耦合度的意思。