参考文档:
《大话设计模式》
设计模式代表了最佳的实践,是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。这些都是为了提高代码的可读性、可扩展性、可复用性、类的可替换性、组件化、可移植性等等。面向对象的设计模式离不开面向对象的原则:① 对接口编程而不是对实现编程;② 优先使用对象组合而不是继承。
1、设计模式的类型
类型 | 设计模式 | 简介 |
创建型模式 Creational Patterns: 用于解耦对象的实例化过程,提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。 | 工厂模式 Factory Pattern | 一个工厂类根据传入的参量决定创建出哪一种产品类的实例 |
抽象工厂模式 Abstract Factory Pattern | 创建相关或依赖对象的家族,而无需明确指定具体类 | |
单例模式 Singleton Pattern | 某个类智能有一个实例,提供一个全局的访问点 | |
建造者模式 Builder Pattern | 封装一个复杂对象的创建过程,并可以按步骤构造 | |
原型模式 Prototype Pattern | 通过复制现有的实例来创建新的实例 | |
结构型模式 Structural Patterns: 把类或对象结合在一起形成一个更大的结构。关注类和对象的组合,继承的概念被用来组合接口和定义组合对象获得新功能的方式。 | 适配器模式 Adapter Pattern | 将一个类的方法接口转换成客户希望的另一个接口 |
桥接模式 Bridge Pattern | 将抽象部分和它的实现部分分离,使它们都可以独立的变化 | |
组合模式 Composite Pattern | 将对象组合成树形结构以表示“部分-整体”的层次结构 | |
装饰器模式 Decorator Pattern | 动态的给对象添加新的功能 | |
外观模式 Facade Pattern | 对外提供一个统一的方法,来访问子系统中的一群接口 | |
享元模式 Flyweight Pattern | 通过共享技术来有效的支持大量细粒度的对象 | |
代理模式 Proxy Pattern | 为其它对象提供一个代理以便控制这个对象的访问 | |
过滤器模式 Filter、Criteria Pattern | ||
行为型模式Behavioral Patterns: 特别关注对象之间的通信。类和对象如何交互,及划分责任和算法。 | 责任链模式 Chain of Responsibility Pattern | 将请求的发送者和接收者解耦,使的多个对象都有处理这个请求的机会 |
命令模式 Command Pattern | 将命令请求封装为一个对象,使得可以用不同的请求来进行参数化 | |
解释器模式 Interpreter Pattern | 给定一种语言,定义它的语法的一种表示,并定义一个解释器 | |
迭代器模式 Iterator Pattern | 一种遍历访问聚合对象中各个元素的方法,不暴露该对象的内部结构 | |
中介者模式 Mediator Pattern | 用一个中介对象来封装一系列的对象交互 | |
备忘录模式 Memento Pattern | 在不破坏封装的前提下,保持对象的内部状态 | |
观察者模式 Observer Pattern | 对象间的一对多的依赖关系 | |
状态模式 State Pattern | 允许一个对象在其对象内部状态改变时改变它的行为 | |
策略模式 Strategy Pattern | 定义一系列算法,把他们封装起来,并且使它们可以相互替换 | |
模板模式 Template Pattern | 定义一个算法结构,而将一些步骤延迟到子类实现 | |
访问者模式 Visitor Pattern | 不改变数据结构的前提下,增加作用于一组对象元素的新功能 |
2、设计模式的原则
2.1、单一职责
单一职责原则:就一个类而言,应该仅有一个引起它变化的原因。例如,定义一个类,一开始就把它所有的方法都写在类中。
类的职责是唯一的,且这个职责是唯一引起其他类变化的原因。如果一个类承担的职责过多,就等于把这些职责耦合在一起。一个职责的变化,可能会削弱或抑制这个类完成其他职责的能力。这种过多的职责耦合会对程序设计造成破坏。
程序设计真正要做的大部分工作,就是发现职责并分离。如果有其他动机去修改一个类,它就多于一个职责。
2.2、开放-封闭
开放-封闭原则:对于扩展是开放的,对于修改是封闭的。例如,定义一个类时,要足够好、足够全,写好后就不用修改了。发生新的需求,只需要增加一些类就行了,原本的代码轻易不修改。
面对需求,对程序的改动是增加代码,而不是更改现有代码。然而一般都存在无法完全封闭的情况,因此在设计程序时,应该对哪一种变化封闭做出选择。即需要对最有可能变化的部分,尽可能去构造抽象来封闭它们。想要达到这样的效果,我们需要使用接口和抽象类。变化发生时,就创建抽象来隔离以后发生的同类变化。
开闭原则是面向对象程序设计的核心,可以带来可维护、可扩展、可复用、灵活性好。然而应该仅对程序中频繁出现的部分做出抽象,而不是刻意抽象全部内容。
2.3、依赖倒转
依赖倒转原则:针对接口编程,而不对实现编程,依赖于抽象而不依赖于具体。抽象不应该依赖细节,细节应该依赖抽象。高层模块不应该依赖底层模块,都应该依赖抽象。例如,如果一段程序利用库函数去访问数据库,这就是高层模块依赖底层模块。当需求改变要访问其他类型的数据库时,这段程序和底层的库函数耦合太紧,无法复用。
依赖倒转原则就是要求高层模块和底层模块都依赖于抽象,降低需求与实现间的耦合,即只要之间的接口/抽象类是稳定的,高层、底层修改都不会影响另一方。
依赖倒转原则是面向对象的标志,程序设计考虑的是针对抽象,而不是针对细节过程,即程序中的所有依赖关系都是终止于接口或抽象类。
2.4、里氏代换
里氏代换原则:子类必须能够完全替换掉父类。程序中使用了父类,那一定可以适用于子类,而且没有区别,行为上没有变化。例如,如果定义一个鸟类,里面有可以飞的行为,那么企鹅就不能继承这个类,这个鸟类就无法完全复用。
只用当子类替换父类,程序功能也不受影响时,父类才能真正被复用,子类也能在父类的基础上增加新的行为。正因子类的可替换性,才使得在不修改父类时程序就可以扩展。
里氏代换原则是面向对象的基本原则,正因如此才使得开闭原则得以实现,高层模块、底层模块都需要依赖于抽象。
2.5、迪米特法则
迪米特法则(最少知识原则):如果两个类不必直接通信,那么这两个类不应该发生直接的相互作用。如果其中一个类需要调用另一个类的某个方法的话,可以通过第三方转发这个调用。例如,定义类的成员时,不想被外界知道的属性/方法都可以用private修饰。
在类的结构设计上,每一个类都应该尽量降低成员的访问权限。降低类之间的耦合,耦合越松,月有利于复用,被修改时波及越小。
2.6、合成复用
合成复用原则:尽量使用合成/聚合的方式,而不是使用继承。
防止类的体系庞大。如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。
继承复用破坏了类的封装性,将父类的实现细节暴露给子类,并且子类和父类的耦合度高,限制了复用的灵活性。继承关系在编译时就定义好了,无法在运行时改变从父类继承的实现。子类的实现与父类关系密切,父类的任何变化必将导致子类的变化。如果复用的子类无法解决问题,只能重写父类或用其他类替换,这种依赖关系最终破坏了复用的灵活性。
3、设计模式
3.0、UML类图
UML类图:类图显示了模型的静态结构,特别是模型中存在的类、类的内部结构,以及一组类、接口、协作以及它们之间的关系。在UML中问题域最终要被逐步转化,通过类来建模,通过编程语言构建这些类从而实现系统。类加上他们之间的关系就构成了类图,类图中还可以包含接口、包等元素,也可以包括对象、链等实例。
类使用带有分割线的三层矩形来表示,分别是类名、属性(field) 和方法(method) 。抽象类的类名用斜体表示。类的属性、操作中的可见性使用+、#、-分别表示public、protected、private。属性的完整表示方式是: 可见性 名称 :类型 [ = 缺省值]。方法的完整表示方式是: 可见性 名称(参数列表) [ : 返回类型]。
接口图与类图类似,但类名上有<<interface>>。接口图还有棒棒糖表示法,表示某个类实现了某个接口,类图上有一个棒棒糖符号,通过虚线连接到接口介绍。
类与类,类与接口之间的关系:
泛化 | 继承 | 实线表示,空心三角形指向父类 |
实现 | 接口 | 虚线表示,空心三角形指向接口 |
关联 | 类之间的通信,一个类知道另一个类的属性/方法。通过实例实现。可以一对一、多对多。例如,大雁的迁徙()与气候的温度()关联。 | 实线表示,箭头指向被关联的类 |
聚合 | 一种弱拥有关系,A类对象包含B类对象,但某个B类对象不一定属于A类对象。松散的整体与个体之间的关系,通过实例实现。例如,雁群->大雁。 | 实线表示,箭头指向被聚合对象,空心菱形表示聚合对象 |
合成 | 一种强拥有关系,整体和个体的生命周期一致,严格的整体和部分的关系。例如,大雁->翅膀。 | 实线表示,箭头指向被合成对象,空心菱形表示合成对象 |
依赖 | 一个类的实现要依赖另一个类。例如,作为属性、方法,大雁类的飞翔()依赖于空气类。 | 虚线表示,箭头指向被依赖的对象 |
创建型模式
3.1、工厂方法
简单工厂模式:简单工厂类根据需要输出对应的实例化对象。
下例是简单工厂实现的运算器,只有加法运算。增加其他运算时,需要新增运算符类,并修改简单工厂类,不符合开闭原则。
public class Operation{ // 封装,将运算封装到Operation类
private double numberA = 0;
private double numberB = 0;
public double NumberA{
get{return numberA;}
set{numberA = value;}
}
public double NumberB{
get{return numberB;}
set{numberB = value;}
}
public virtual double GetResult(){ // 虚函数
double result = 0;
return result;
}
} // 加法类
class AddOperation:Operation{ // 继承Operation类,实现松耦合,分离各运算
public override double GetResult(){ // 重载
double result = 0;
result = NumberA + NumberB;
return result;
}
}
public class OperatorFactory{ // 简单工厂类,根据需要生成实例对象
public static Operation CreateOperator(string operate){
Operation oper = null;
switch(operate){
case "+":
oper = new AddOperation();
break;
}
return oper;
}
}
//--------------------------------
Operation oper;
oper = OperatorFactory.CreateOperator("+");
oper.NumberA = 1;
oper.NumberB = 2;
double result = oper.GetResult();
工厂方法模式:定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。降低了耦合性,提高了扩展性,但是每增加一个功能就需要增加一个类,提高了系统复杂性。
下例是将简单工厂模式的简单工厂类改造为接口,增加运算符工厂类实现该接口。增加一个功能只需要增加一个工厂接口及对应工厂类。例如,增加一个幂运算只需要增加一个幂运算类、一个幂运算工厂。
interface IFactory{ // 接口,工厂接口
Operation CreateOperation(); // 依赖倒转原则,对工厂进行抽象
}
class AddFactory:IFactory{ // 加法工厂类
public Operation CreateOperation(){
return new AddOperation();
}
}
class SubFactory:IFactory{ // 减法工厂类(同时新增减法类)
public Operation CreateOperation(){
return new SubOperation();
}
}
//------------------------
IFactory operFactory = new AddFactory();
Operation oper = operFactory.CreateOperation();
oper.NumberA = 1;
oper.NumberB = 2;
double result = oper.GetResult();
简单工厂模式中的工厂类包含逻辑判断,不依赖于客户端;工厂方法模式中,客户端需要决定使用哪一个工厂类来实现运算。逻辑判断的过程还是存在,只不过从内部移动到了客户端。
3.2、抽象工厂
抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。用于解决涉及多个系列的产品问题,方便增加产品的种类。同时,创建具体实例的过程与客户端分离,客户端是通过抽象接口操纵实例。
抽象工厂的核心是工厂抽象类,其中包含了创建所有产品的抽象方法。每个具体工厂都继承自抽象工厂,创建具有特定实现的产品对象。
使用工厂方法模式,若增加一系列,就要增加该系列的多个类、一个工厂接口、该系列的多个工厂类。使用抽象工厂模式,若要增加一个系列,只需要增加一个工厂类;但若要增加一个产品,需要增加一个产品父类、每个系列增加该产品。比工厂模式管理简单,但缺点也如此,新增产品的改动较多。
abstract class AbstractProductA{ // 抽象产品A
abstract public void CreateProduct();
}
class ProductA1 : AbstractProductA{ // 具体产品A1
public override void CreateProduct(){
Console.WriteLine("ProductA1");
}
}
class ProductA2 : AbstractProductA{ // 具体产品A2
public override void CreateProduct(){
Console.WriteLine("ProductA2");
}
}
abstract class AbstractProductB{ // 抽象产品B
abstract public void CreateProduct();
}
class ProductB1 : AbstractProductB{ // 具体产品B1
public override void CreateProduct(){
Console.WriteLine("ProductB1");
}
}
class ProductB2 : AbstractProductB{ // 具体产品B2
public override void CreateProduct(){
Console.WriteLine("ProductB2");
}
}
abstract class AbstractFactory{ // 抽象工厂,可以生产所有产品
public abstract AbstractProductA CreateProductA();
public abstract AbstractProductB CreateProductB();
}
class Factory1:AbstractFactory{ // 具体工厂1
public override AbstractProductA CreateProductA(){ // 产品A1
return new ProductA1();
}
public override AbstractProductB CreateProductB(){ // 产品B1
return new ProductB1();
}
}
class Factory2 : AbstractFactory{ // 具体工厂2
public override AbstractProductA CreateProductA(){ // 产品A2
return new ProductA2();
}
public override AbstractProductB CreateProductB(){ // 产品B2
return new ProductB2();
}
}
//------------------------
Factory1 factory1 = new Factory1();
factory1.CreateProductA().CreateProduct();
从上例中可以看到,如果要增加一个系列Factory3,只需要新增一个具体工厂3,其中包含产品A3、B3。但是,如果要增加一个产品ProductC,需要修改抽象工厂类、修改每个具体工厂,改动很大。若使用简单工厂模式对抽象工厂进行改造,将全部工厂类合并为一个类,使用switch语句来判断调用哪个工厂。但是,新增产品种类时仍需要修改代码。
问题的关键是需要判别要生产什么产品,应该去调用什么工厂。这里可以用依赖注入的编程技术,而在C#中可以用更简单的反射技术。在C#中,反射技术是在System.Reflection中。调用方法为Assembly.Load("程序集名称").CreateInstance("命名空间.类名")。
using System.Reflection; // 引入反射
namespace AbstractFactotyReflection
{
class FactoryAgg
{
private static readonly string AssemblyName = "CsharpLearning"; // 程序集名称
private static readonly string NameSpace = "AbstractFactory"; // 命名空间名称(类所在)
private static readonly string factory = "1"; // 通过修改工厂名来调用不同的工厂类
public static AbstractProductA CreateProductA()
{
string className = NameSpace + ".ProductA" + factory; // 命名空间.类名
return (AbstractProductA)Assembly.Load(AssemblyName).CreateInstance(className); // 反射
}
public static AbstractProductB CreateProductB() {
string className = NameSpace + ".ProductB" + factory;
return (AbstractProductB)Assembly.Load(AssemblyName).CreateInstance(className);
}
}
}
//--------------------
AbstractProductA a = FactoryAgg.CreateProductA();
a.CreateProduct();
AbstractProductB b = FactoryAgg.CreateProductB();
b.CreateProduct();
利用反射后,要增加一个产品只需要增加相应的产品类,再在合并工厂FactoryAgg中增加生产产品的方法,改动较小。要生产其他工厂的产品,只需要修改factory变量。为了避免修改代码,可以使用配置文件.config。
从NuGet程序包安装System.Configuration并using,新增应用程序配置文件App.config。在源代码中将factory的值设为配置文件的值。
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="factory" value="1"/>
</appSettings>
</configuration>
using System.Configuration; // 引入配置
···
private static readonly string factory = ConfigurationManager.AppSettings["factory"];
3.3、单例
单例模式:属于创建型模式,它提供了一种创建对象的最佳方式。涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式主要解决全局频繁创建/销毁的类,例如,多线程/进程访问某一个文件、按钮创建一个窗口。可以避免资源的过多开销。调用这个单例类不用考虑是否要实例化,而是类本身去考虑是否实例化。单例类存储了其唯一的实例,并提供了一个方法访问这个实例。因此,对单例类的访问是受控的。但是该类不能继承,因为只创建一个实例。单例类的职责过多,违背单一职责原则。
多线程时,需要使用lock来控制某一时刻只有一个线程进入单例类。lock:确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(被阻止),直到该对象被释放。但是不能每次进入都要lock一次,而是只有实例没被创建时才lock处理。这种判断实例是否存在后再判断lock的方法被称为双重校验锁(double-check locking)。
下例是一个懒汉式单例(在第一次被引用时,才会将自身实例化)。利用双重校验锁,防止多个onlyInstance==null的线程同时进入而创建多个实例,是线程安全的并且在加锁下保持高效。
namespace Singleton_ {
class Singleton { // 懒汉式单例
private static Singleton onlyInstance; // 声明唯一实例
private Singleton() { } // private构造方法,外部无法创建其他实例
private static readonly object synRoot = new object(); // 进程辅助对象
public static Singleton GetInstance() // 静态方法,只能通过类调用
{ // 获取唯一实例的方法
if (onlyInstance == null) // 双重校验锁
{
lock (synRoot) // 锁
{
if (onlyInstance == null) // 防止两个实例为null的线程同时进入
{
onlyInstance = new Singleton();
}
}
}
return onlyInstance;
}
}
}
下例是一个饿汉式单例(通过静态初始化,自身被加载时就实例化),同时用sealed关键字修饰为密封类,防止该类被继承。这种方式易实现,不加锁效率高,但在类加载式就初始化,浪费内存。
public sealed class SingletonHungry // 饿汉式单例
{ // sealed防止发生派生(派生可能会增加实例)
private static readonly SingletonHungry onlyInstance = new(); // 静态初始化
private SingletonHungry() { }
public static SingletonHungry GetInstance()
{
return onlyInstance;
}
}
下例是一个登记式单例(通过静态内部类,延迟初始化)。通过将实例化方法封装在一个静态内部类中,防止加载时就初始化。加载登记式单例时,内部类没有被主动调用,因此不会在加载时初始化。这种可以避免资源消耗。
public class SingletonRegister // 登记式单例
{
private static class SingletonHolder // 静态内部类
{
public static readonly SingletonRegister onlyInstance = new();
}
private SingletonRegister() { }
public static SingletonRegister GetInstance()
{
return SingletonHolder.onlyInstance; // 调用内部类来实例化
}
}
3.4、建造者
建造者模式:将一个复杂对象的构建与他的表示分离,使得同样的构建过程可以创建不同的表示。使用建造者模式,只需要指定需要建造的类型就可以得到它们,而具体的过程和细节不需要知道。
建造者模式包含Director、Builder、ConcreteBuilder和Product。Director是聚合Builder的一个对象,根据需求建造产品。Builder是创建一个Product各个部分指定的抽象接口。ConcreteBuilder是继承Builder的具体建造者,实现建造Product各个部分的功能。Product就是具体产品。
由于有Director和Builder,可以创建复杂对象,虽然对象各部分的构建复杂多变,但是对象内部的构造顺序是稳定的。若要改变一个产品内部的表示,定义新的Builder就可以了。产品本身与产品建造解耦,各具体建造者相对独立,能更精细地控制创建过程。建造者模式处理地对象内部一般差异性不大,但是如果产品内部复杂多变,将产生大量建造者,就不适用了。
下例是建造者模式地通用方式:
namespace Builder_
{
class Product { // 产品
IList<string> parts = new List<string>();
public void Add(string part) { parts.Add(part); }
public void Show() {
Console.WriteLine("Create Product:\n");
foreach (var part in parts) { Console.WriteLine(part); }
}
}
abstract class Builder { // 建造者
public abstract void ProcessA(); // 产品各部分的抽象接口
public abstract void ProcessB();
public abstract Product GetResult(); // 返回建造好的产品
}
class ConcreteBuilderA:Builder { // 具体建造者A
private Product product = new();
public override void ProcessA() { product.Add("partA"); }
public override void ProcessB() { product.Add("partB"); }
public override Product GetResult() { return product;}
}
class Director { // 指导者
public void Construct(Builder builder){
builder.ProcessA();
builder.ProcessB();
}
}
}
//------------------------
Director director = new Director();
Builder b1 = new ConcreteBuilderA();
director.Construct(b1);
Product product = b1.GetResult();
product.Show();
3.5、原型
原型模式:用原型实例指定创建对象的种类,并通过拷贝这些原型来创建新的对象。即从一个对象去创建另一个对象,这个新对象可以自定义,不需要知道创建的细节。用于创建重复对象,又能保证性能(类初始化消耗数据、硬件资源),缺点就是浅拷贝。
下例是浅拷贝的原型模式。在.NET中提供了ICloneable接口,实现其中的Clone()方法就可以完成原型模式,但这仅仅是浅拷贝。因为MemberwiseClone()对值是逐位复制,对对象是复制对该对象的引用。
namespace Prototype_
{
class Number{
private int num;
public int Num { get { return num; } set { this.num = value; }
}
}
class Prototype:ICloneable // 实现ICloneable接口
{
private string str;
private Number n = new();
public Prototype(string str, int num) { this.str = str; this.n.Num = num; }
public string Str { get { return str; } set { this.str = value; } }
public void SetNumber(int num) { n.Num = num; }
public object Clone() { // 实现Clone()方法
return (object)this.MemberwiseClone();
}
}
}
下例是深拷贝的原型模式。构建一个私有构造调用Clone()完成引用字段的克隆,再把当前对象的引用字段的数据拷贝给新的对象,最后返回这个对象。
namespace PrototypeDeep
{
class Number:ICloneable{ // 复制对象字段中的数据
private int num;
public int Num { get { return num; } set { this.num = value; } }
public object Clone() // 实现Clone()
{
return (object)this.MemberwiseClone();
}
}
class Prototype : ICloneable{
private string str;
private Number n = new();
public Prototype(string str, int num) { this.str = str; this.n.Num = num; }
public string Str { get { return str; } set { this.str = value; } }
public void SetNumber(int num) { n.Num = num; }
private Prototype(Number num)
{ // 私有构造调用Clone(),用于拷贝对象的数据
this.n = (Number)n.Clone();
}
public object Clone() { // 不使用MenberwsieClone()
Prototype obj = new(this.n); // 调用私有构造,创建新对象
obj.n.Num = this.n.Num; // 复制原字段的值
return obj; // 返回新对象
}
}
}
结构型模式
3.6、适配器
适配器模式:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。系统的数据和行为都正确,但是接口不对时,可以使用适配器模式,使原本使用的某个对象与接口匹配。适配器模式的目的就是要复用现存的类,分为类适配器、对象适配器。
适配器继承或依赖已有的对象,实现想要的目标接口。提高类的复用性、灵活性好;但是,过多的适配器会使用系统非常杂乱,因此,只有当两个类的接口重构困难时,才考虑用适配器。(非面向对象的真谛,尽量避免使用适配器!!!)
下例是一个适配器模式:
namespace Adapter_
{
class Target // 目标
{
public virtual void Request() { Console.WriteLine("Common Request!"); }
}
class NeedAdapted // 待适配的类
{
public void SepcialRequest() { Console.WriteLine("Special Request!"); }
}
class NewAdapter : Target // 适配器(继承原有的目标类)
{
private NeedAdapted needAdapted = new();
public override void Request()
{ // 表面调用Requset(),实际上为SpecialRequset()
needAdapted.SepcialRequest();
}
}
}
在.NET中DataAdapter适配器用于DataSet和源数据之间的检索和保存数据,Fill用于将DataSet数据适配数据库,Update用于将数据库数据适配DataSet。
3.7、桥接
桥接模式:将抽象部分与实现部分分离,使它们都可以独立的变化。实现指抽象类和它的派生类用来实现自己的对象。实现可能有多种方式,根据这些不同的方式,分离出不同的类,让它们可以独立变化,降低耦合。桥接模式就是合成复用原则的使用,抽象和实现分离,拥有强的扩展能力,但是设计难度高、理解系统难。
例如对电脑配件分类,按品牌分,分为不同的品牌;按硬件分,又有不同的硬件种类。品牌和硬件的组合过多。桥接的核心就是把硬件这些实现与电脑品牌这个抽象分离(抽象不是抽象类),使品牌、硬件可以独立变化。
下例是桥接模式,抽象出品牌,实现硬件,二者分离,品牌聚合硬件。若要增加一个品牌或硬件,只要增加一个品牌子类或硬件子类就可以,二者不受影响。(抽象出品牌还是硬件并没有实际区别)
namespace Bridge_
{
abstract class PCHardware { // 实现部分***
public abstract void Assemble(string b);
}
class CPU : PCHardware { // 具体实现1
public override void Assemble(string b) { Console.WriteLine(b + "处理器!"); }
}
class GPU : PCHardware // 具体实现2
{
public override void Assemble(string b) { Console.WriteLine(b + "显卡!"); }
}
abstract class PCBrand { // 抽象部分***
protected PCHardware pCHardware;
public void AssembleHardware(PCHardware pCHardware) // 聚合(具体实现)
{ this.pCHardware = pCHardware; }
public abstract void Assemble();
}
class BrandAA : PCBrand { // 具体抽象1
private string brand = "AA牌";
public override void Assemble()
{ pCHardware.Assemble(brand); }
}
class BrandBB : PCBrand { // 具体抽象2
private string brand = "BB牌";
public override void Assemble()
{ pCHardware.Assemble(brand); }
}
}
//---------------------
PCBrand pc1;
pc1 = new BrandAA();
pc1.AssembleHardware(new CPU());
pc1.Assemble(); // -> AA牌处理器!
3.8、组合
组合模式:将对象组合成树形结构以表示"部分-整体"的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性,这通过整体和部分实现统一的接口来完成。此时,整体和部分有明显的层次结构,但是对整体、部分的操作是统一的。组合模式由三部分组成:Component(实现所有类共有接口的默认行为,接口/抽象类)、Leaf(叶子节点对象,实现接口行为,可用异常处理操作子节点的行为)、Composite(容器节点对象,存储子部件,实现与子部件操作相关动作,如增删)。
组合模式定义了基本对象和组合对象的层次结构,基本对象可以被组合成复杂的组合对象,而组合对象也可以被组合,但对这些对象的操作是统一的。例如,处理文件时,有不同层级的文件夹,但是对各个文件夹的处理动作没用区别;或者处理XML文件时,各节点有层次结构,但是处理节点的方式一致;或者公司的员工组织架构。
由于Leaf和Composite的行为不同,组合模式又分为透明模式、安全模式。
透明模式:在统一接口Component中声明所有用于管理子对象的方法,Leaf、Composite有完全一致的行为接口,完全透明,但Leaf明显有多余的方法。
安全模式:在Component中不声明管理子对象的方法,而是在Composite中声明。这样Leaf和Composite具有不同的方法,不够透明,在操作时需要辨别。
namespace Composite_
{
abstract class Component{ // 统一接口
protected string name;
public Component(string name) { this.name = name; }
public abstract void Add(Component c);
public abstract void Display(int depth);
}
class Leaf : Component{ // 叶节点
public Leaf(string name) : base(name) { }
public override void Add(Component c) { throw new Exception("Leaf Failed"); }
public override void Display(int depth) { Console.WriteLine(new String('-', depth) + name); }
}
class Composite : Component{ // 容器节点
private List<Component> children = new List<Component>(); // 存储下级
public Composite(string name) : base(name) { }
public override void Add(Component c) { children.Add(c); }
public override void Display(int depth)
{
Console.WriteLine(new String('-', depth) + name);
foreach (Component c in children) { c.Display(depth + 1); }
}
}
}
3.9、装饰器
装饰器模式:动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。装饰器模式可以在为原系统增加新功能的时候,不增加系统的复杂度,是一种动态的添加功能的方式。原系统和装饰器是非耦合的,可以各自发展。把类的核心职责和装饰功能分离,可以根据需要有选择地包装对象。装饰器模式主要包含三部分:Component(对象接口,可以为这些对象动态的添加职责)、Decorator(装饰抽象类,继承Component,但扩展Component的功能)、ConcreteDecorator(具体装饰对象,为Component添加实际功能)。
例如,礼物的包装有包装纸、彩带、纸盒等,这些都可以有选择地来装饰礼物。装饰器中的SetComponent进行对象的包装,装饰器只需要完成自身的功能,不需要关心装饰的顺序。对象接口和具体对象要合理规划,根据需要合并成一个/定义多个具体对象。
namespace Decorator_
{
abstract class Component { // 对象接口
protected string name;
public string Name { get { return name; } }
public Component(string name){ this.name = name; }
public abstract void Operation();
}
class ConcreteComponent1 : Component{ // 具体对象
public ConcreteComponent1(string name):base(name) { }
public override void Operation() { // 具体对象的职责
Console.WriteLine("礼物" + name);
}
}
abstract class Decrotor : Component{ // 装饰器的抽象类,用于定义多个装饰器
protected Component component;
public Decrotor(string name):base(name) { }
public override void Operation(){ // 重写
component.Operation(); // 执行原系统的功能
}
public void SetComponent(Component component) { // 设置被装饰的对象
this.component = component;
}
}
class ConcreteDecrotor1 : Decrotor{
public ConcreteDecrotor1(string name):base (name) { }
public override void Operation(){
base.Operation(); // 执行原系统的功能
Console.WriteLine("用" + name + "装饰" + component.Name); // 再执行装饰器功能
}
}
}
3.10、外观
外观模式:为子系统中的一组接口提供一个一致的高层接口,这个接口使得这一子系统更加容易使用。在设计初期就将业务与用户分离,建立外观接口可以降低复杂子系统之间的依赖,降低用户访问内部子系统的难度。或者,为旧系统设计一个可供新系统访问的交互接口,降低新系统开发的工作复杂度。但是修改麻烦,不符合开闭原则。
例如,对多个股票的买卖比较复杂,但是融合为一个基金,可以降低操作难度。
namespace Facade_
{
class SubSystem1{ // 子系统1
public void Func() { Console.WriteLine("SubSystem1_Func"); }}
class SubSystem2{ // 子系统2
public void Func() { Console.WriteLine("SubSystem2_Func"); }}
class SubSystem3{ // 子系统3
public void Func() { Console.WriteLine("SubSystem3_Func"); } }
class Facade{ // 外观类,了解子系统的所有方法,以便调用
private SubSystem1 one;
private SubSystem2 two;
private SubSystem3 three;
public Facade() {
one = new SubSystem1(); two = new SubSystem2(); three = new SubSystem3();
}
public void UniInterface1() {
one.Func(); two.Func(); three.Func();
}
}
}
3.11、享元
享元模式:运用共享技术有效地支持大量细粒度的对象。创建大量有相同内容的对象将造成浪费,将其中共同的部分抽象出来,有相同请求时,直接返回内存中已有对象的引用,避免重复创建。在享元对象内部并且不会随环境改变而改变的共享部分就是内部状态;随着环境改变而改变的就是外部状态。内部状态存储在享元对象中,而外部状态由外部对象存储,调用享元时传入。如果程序中使用了大量的对象,消耗量大量的内存,就需要使用享元模式,或者对象的大部分状态是外部状态,剔除外部状态后,可以用几个享元对象代替。但是,享元对象分出内部状态、外部状态会提高系统复杂度。
享元模式主要包含Flyweight(所有具体享元类的抽象类/接口,通过接口可以接收外部状态)、ConcreteFlyweight(继承Flyweight类/实现Flyweight接口,存储内部状态)、UnsharedConcreteFlyweight(不需要共享的Flyweight类,不强制共享)、FlyweightFactory(享元工厂,创建并管理Flyweight,通过hashtable来判断并存储享元对象)
例如,池技术就是享元模式,如string常量池,.NET的string类就是享元模式,相同的字符串是相同的对象。或者,在围棋游戏中,将棋子对象减少到几个,而不是一个位置一个棋子。
namespace Flyweight_
{
abstract class Flyweight { // 享元类的抽象类/接口
public abstract void Operation(int a);
}
class ConcreteFlyweight: Flyweight { // 具体的享元类
public override void Operation(int a) {
Console.WriteLine("共享的具体方法:" + a);
}
}
class UnsharedConcreteFlyweight: Flyweight { // 不共享的对象
public override void Operation(int a) {
Console.WriteLine("不共享的具体方法:" + a);
}
}
class FlyweightFactory
{
private Hashtable hashtable = new Hashtable(); // 存储共享对象
public FlyweightFactory() { }
public ConcreteFlyweight GetFlyweight(string name) { // 获取实例/创建不存在实例并返回
if (!hashtable.ContainsKey(name))
{
hashtable.Add(name, new ConcreteFlyweight()); // 创建
}
return (ConcreteFlyweight)hashtable[name]; // 返回共享对象
}
}
}
3.12、代理
代理模式:为其他对象提供一种代理以控制对这个对象的访问。定义一个公共接口,真实主体和代理都实现这个接口,从而使真实主体在任何地方都可以使用代理。代理模式的应用:远程代理(为一个对象在不同的地址空间提供局部代理,可以因此该对象存在于不同的地址空间)、虚拟代理(通过代理实例化开销打的对象来优化性能)、安全代理(用来控制真实对象访问时的权限)、智能指引(调用真实主体时,代理处理另外事务)。代理模式包括:Subject(真实主体和代理的公共接口)、RealSubject(真实主体)、Proxy(代理,保存一个引用使得代理可以访问实体,实现与主体相同的接口来替代主体)。
例如,.NET中WebService的远程代理、HTML网页使用图片框代理真实的大图片、使用代理计算实体的引用次数/检查对象是否锁定好/引用持久对象装入内存等。
namespace Proxy_
{
public interface ISubject{ // 公共接口
public void Display() { }
}
class RealSubject: ISubject{ // 真实对象
private string name;
public RealSubject(string name) { this.name=name;}
public void Display() { Console.WriteLine("Display " + name); }
}
class Proxy : ISubject{ // 代理
private string name;
private RealSubject subject;
public Proxy(string name) { this.name = name;}
public void Display()
{
if (subject == null) { this.subject = new RealSubject(name); }
Console.WriteLine("Display proxy_pic");
subject.Display(); // ->Display real_pic
}
}
}
代理与适配器的区别:适配器模式改变所考虑对象的接口,而代理模式不能改变所代理类的接口。
代理与装饰器的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。
行为型模式
3.13、责任链
责任链模式:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。请求沿链传递,发送者和接收者都无需对方的信息,链上的对象只需要保持一个指向后继者的引用。可以随时增加/修改请求,提高了职责指派的灵活性。关键是指定后继者以及判断是否处理请求。但是,请求有可能无法被处理(无法处理或请求配置错误)。
例如,提交加薪申请需要层层审批,每一层都知道自身的上级,以及能否处理该请求。
namespace ChainofResponsibility_ {
class Request{ // 请求
private string tissue;
public string Tissue { get { return tissue; } }
private int num;
public int Num { get { return num; } }
public Request(string tissue, int num) { this.tissue = tissue; this.num = num; }
}
abstract class Handler{ // 处理者的抽象类
protected string name;
protected Handler superior;
public Handler(string name) { this.name = name; }
public void SetSuperior(Handler handler) { this.superior = handler; } // 设置后续
abstract public void Handle(Request request); // 处理请求
}
class Manager: Handler{ // 具体处理者1
public Manager(string name):base(name) { }
public override void Handle(Request request) {
if(request.Tissue == "加薪" && request.Num <= 500) {
Console.WriteLine("{0}同意{1}{2}元!", name, request.Tissue, request.Num);
} else {
Console.WriteLine("经理无权处理!转上级领导!");
if(superior != null) { superior.Handle(request); } // superior?.Handle(request);
}
}
}
class GeneralManager: Handler{ // 具体处理者2
public GeneralManager(string name) : base(name) { }
public override void Handle(Request request){
if (request.Tissue == "加薪" && request.Num <= 2000){
Console.WriteLine("{0}同意{1}{2}元", name, request.Tissue, request.Num);
} else {
Console.WriteLine("{0}答复:不同意!", name);
if (superior != null) { superior.Handle(request); } // superior?.Handle(request);
}
}
}
}
3.14、命令
命令模式:将一个请求封装为一个对象,从而使不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持撤销操作。请求封装为对象后,调用者和接收者耦合降低,即请求操作的对象与执行操作的对象分离。容易设计一个命令队列,可以将命令计入日志。命令很容易扩展,新的命令容易加入新系统,并不影响其他类。请求也很容易撤销和重做。命令模式包括:Command(操作命令的接口)、ConcreteCommand(将命令绑定Receiver的动作,调用Receiver的操作以执行命令)、Invoker(请求执行命令的调用者)、Receiver(执行所请求的命令的任意类)。
例如,点菜过程式将服务员与厨师分离,服务员负责记录/撤销/修改/通知命令等,厨师负责烧烤,菜单(命令)如何变化与厨师无关。
namespace Command_
{
public class Receiver { // 接收者(执行命令)
public void RoastMutton() { Console.WriteLine("烤羊肉!"); }
public void RoastBeef() { Console.WriteLine("烤牛肉!"); }
public void RoastXXX() { Console.WriteLine("烤XXX!"); }
}
abstract class Command { // 命令(抽象类)
protected Receiver receiver;
public Command(Receiver receiver) { this.receiver = receiver; }
public abstract void Execute();
}
class ConcreteCommand1: Command { // 具体命令
public ConcreteCommand1 (Receiver receiver): base(receiver) { } // 绑定Receiver
public override void Execute() { receiver.RoastMutton(); } // 调用Receiver的操作
}
class ConcreteCommand2: Command {
public ConcreteCommand2 (Receiver receiver): base(receiver) { }
public override void Execute() { receiver.RoastBeef(); }
}
class ConcreteCommand3 : Command{
public ConcreteCommand3(Receiver receiver) : base(receiver) { }
public override void Execute() { receiver.RoastXXX(); }
}
class Invoker{ // 调用者
private IList<Command> commands = new List<Command>(); // 存储命令
public void SetOrder(Command command) { // 记录
if (command.ToString() != "Command_.ConcreteCommand1" && command.ToString() != "Command_.ConcreteCommand2") {
Console.WriteLine(command.ToString());
Console.WriteLine("只能烤羊肉、烤牛肉!");
} else {
commands.Add(command);
Console.WriteLine("增加订单:" + command.ToString() + " 时间:" + DateTime.Now.ToString());
}
}
public void CancelOrder(Command command){ // 取消
if (commands.Contains(command)){
commands.Remove(command);
Console.WriteLine("取消订单:" + command.ToString());
} else { Console.WriteLine("没点这个!"); }
}
public void Notify() { // 通知
foreach (Command command in commands) { command.Execute(); }
}
}
}
基于敏捷开发原则,不要为代码添加基于猜测的、实际不需要的功能。如果不清楚一个系统是否需要命令模式,一般就不用急于实现。只有真正需要如撤销/恢复等功能时,才值得把代码重构为命令模式。
3.15、解释器
解释器模式:给定一个语言,定义它的语法的一种表达式,并定义一个解释器,这个解释器使用该表达式来解释语言中的句子。当一个语言需要解释执行,并且可以将语法中的句子表示为一个抽象语法树时,就可以使用解释器模式。解释器模式可以容易地改变/扩展语法,因为语法规则是用类表示,可以通过继承来改变/扩展语法,通过实现语法树的各个节点来实现语法。但是,解释器模式中的每个规则定义至少一个类,若语法过多会引起类膨胀,难以维护,此时需要通过语法分析器/编译器生成器来解决。解释器模式包括:AbstractExpression(抽象语法树中所有节点所共享的解释操作的抽象类)、TerminalExpression(实现与终结符相关的操作的终结符表达式)、NonterminalExpression(实现与所有非终结符相关的操作,每一条语法都需要一个类)、Content(解释器之外的全局信息)。
如果一种特定类型的问题发生的频率足够高,那么就值得将这问题的各个实例表述为一个语言简单的句子,同时构建一个解释器来解释这些句子,来解决该问题。例如,正则表达式的文本匹配。
例如,将音符转换为五线谱(全部用终结符表达式)。
namespace Interpreter_
{
class Context { // 全局信息
private string text;
public string Text { get { return text; } set { text = value; } }
}
abstract class Expression { // 抽象表达式,表达式树上节点的实现
public void Interprete(Context context)
{ // 翻译按空格为分隔符的字符串,因此所有节点都是终结符表达式
if (context.Text.Length == 0) { return; }
else { // "T 500 O 2 E 0.5" 变为 "O 2 E 0.5"
string playKey = context.Text.Substring(0, 1); // 第一个字符
context.Text = context.Text.Substring(2); // 去除字符、空格的后续string
double playValue = Convert.ToDouble(context.Text.Substring(0, context.Text.IndexOf(" "))); // 字符后的数字
context.Text = context.Text.Substring((context.Text.IndexOf(" ") + 1)); // 去除数字、空格的后续string
Execute(playKey, playValue); // 执行翻译,Execute("T", "500")
}
}
public abstract void Execute(string playKey, double playValue); // 真正进行翻译的方法,各个节点表达式进行实现
}
class TerminalExpNote : Expression { // 终结符表达式(音符类)
public override void Execute(string playKey, double playValue) {
string note = "";
switch (playKey) {
case "C": note = "1"; break;
case "D": note = "2"; break;
case "E": note = "3"; break;
case "F": note = "4"; break;
case "G": note = "5"; break;
case "A": note = "6"; break;
case "B": note = "7"; break;
}
Console.Write("{0} ", note);
}
}
class TerminalExpScale: Expression { // 终结符表达式(音阶类)
public override void Execute(string playKey, double playValue) {
string scale = "";
switch (Convert.ToInt32(playValue)) {
case 1: scale = "低音"; break;
case 2: scale = "中音"; break;
case 3: scale = "高音"; break;
}
Console.Write("{0} ", scale);
}
}
class TerminalExpSpeed: Expression { // 终结符表达式(音速类)
public override void Execute(string playKey, double playValue) {
string speed = "";
if (playValue < 500) { speed = "高速"; }
else if (playValue >= 1000) { speed = "慢速"; }
else { speed = "中速"; }
Console.Write("{0} ", speed);
}
}
}
//---------------------------------
using Interpreter_;
static void Main(string[] args)
{
Console.WriteLine("上海滩:");
Context music = new();
music.Text = "T 500 O 2 E 0.5 G 0.5 A 3 E 0.5 G 0.5 D 3 E 0.5" +
" G 0.5 A 0.5 O 3 C 1 O 2 A 0.5 G 1 C 0.5 E 0.5 D 3";
Expression expression = null;
try {
while (music.Text.Length > 0) {
string str = music.Text.Substring(0, 1);
switch (str) {
default: expression = new TerminalExpNote(); break; // 当str = "A" ~ "P" 时,实例化为TerminalExpNote()
case "O": expression = new TerminalExpScale(); break; // 实例化为TerminalExpScale()
case "T": expression = new TerminalExpSpeed(); break; // 实例化为TerminalExpSpeed()
}
expression.Interprete(music);
}
}
catch (Exception ex) { Console.WriteLine(ex.Message); }
}
Main(args);
3.16、迭代器
迭代器模式:一种遍历访问聚合对象中各个元素的方法,不暴露该对象的内部结构。通过将遍历行为与集合对象分离,抽象出迭代器类,既不暴露集合的内部结构,又可以透明地访问集合内部。但通过一个集合类就需要一个对应的迭代器类。当需要访问一个聚集对象,并且对该对象内的任何元素都可以遍历,或者需要多种遍历方式时,可以使用迭代器模式。迭代器模式的关键是为聚集对象提供开始、下一个、是否结束、当前项等统一接口。迭代器模式主要包括:Iterator(抽象迭代器类,包含统一接口)、Aggregate(抽象集合类,定义迭代器方法)、ConcreteIterator、ConcreteAggregate。
例如,定义一个可以存储任意内容的集合,并实现遍历。
namespace Iterator_
{
abstract class Iterator { // 抽象迭代器类
public abstract object First();//定义各方法的统一接口
public abstract object Next();
public abstract bool IsDone();
public abstract object CurrentItem();
}
abstract class Aggregate { // 抽象集合类
public abstract Iterator CreateIterator();// 创建迭代器
}
class ConcreteIterator: Iterator { // 具体的集合
private ConcreteAgg agg;// 具体集合的对象
private int current = 0;
public ConcreteIterator(ConcreteAgg agg) { // 构造函数
this.agg = agg;// 初始化时传入具体的集合
}
public override object First() { // 开始
return agg[0];
}
public override object Next() { // 下一项
object ret = null;
current++;
if (current < agg.Count) { ret = agg[current]; }
return ret;
}
public override bool IsDone() { // 是否结束
return current <= agg.Count - 1 ? true : false;
}
public override object CurrentItem() { // 当前项
return agg[current];
}
}
class ConcreteAgg : Aggregate{ // 具体的集合
private IList<object> items = new List<object>();// 存储集合的对象
public ConcreteAgg(params object[] agg){ // 构造函数
int index = 0;
foreach (object item in agg) {
items.Insert(index, item);
index++;
}
}
public override Iterator CreateIterator() { return new ConcreteIterator(this); }
public int Count { get { return items.Count; } }
public object this[int index] { // 索引器
get { return items[index]; }
set { items.Insert(index, value); }// 使用Insert方法,而不是[index]赋值,因为此时长度为0
}
}
}
在.NET中,IEnumerator接口支持对非泛型集合的简单迭代(包含获取当前项Current、下一项MoveNext()、恢复初始位置Reset())。IEnumerable公开枚举数支持在非泛型集合上进行简单迭代(包含创建迭代器GetEnumerator())。通过实现IEnumerator接口来创建迭代器,实现IEnumerable接口开创建简单的非泛型集合。
namespace IteratorEnum_
{
class SimpleAgg : IEnumerable<Person> // 简单集合
{
private IList<Person> objects = new List<Person>(); // 存储对象
int current = 0;
public SimpleAgg(params object[] agg) // 构造
{
int index = 0;
foreach (Person item in agg)
{
objects.Insert(index, item);
index++;
}
}
public int Count { get { return objects.Count; } } // 计数
public IEnumerator<Person> GetEnumerator()
{ // 创建迭代器
return new SimpleIterator(this); // 实现GetEnumerator()
}
IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException();
public Person this[int index]
{ // 索引器
get { return objects[index]; }
set { objects.Insert(index, value); }
}
}
class SimpleIterator : IEnumerator<Person> // 简单迭代器
{
private SimpleAgg agg;
private int index = 0;
public SimpleIterator(SimpleAgg agg) { this.agg = agg; }
public Person Current { get { return agg[index]; } } // 实现Current
object IEnumerator.Current => throw new NotImplementedException();
public bool MoveNext() // 实现MoveNext()
{
index++;
return index <= agg.Count - 1 ? true : false;
}
public void Reset() { index = 0; } // 实现Reset()
public void Dispose() { } // 实现Dispose()
}
class Person{
private string name;
private int age;
public string Name { get { return name; } }
public int Age { get { return age; } }
public Person(string name, int age){
this.name = name;
this.age = age;
}
}
}
3.17、中介者
中介者模式:用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。将一个系统分隔成许多对象可以提高复用性,但是对象之间相互连接的数量增加会导致复用性降低。如果系统中的对象过多依赖其他对象,这个系统将修改困难,这时可利用中介者模式,将对象之间的交互改成对象与中介者的交互。中介者模式包括:Mediator(抽象中介者,定义相关接口)、ConcreteMediator(具体中介者,实现抽象类,知道所有的同事类,从同事类接收消息,并对之发出命令)、Colleague(抽象同事类)、ConcreteColleague(每个具体同事类只知道自己的行为,不知道其他同事类,但都知道中介者。)
将互相交织的网状结构重构为星型结构,居中的中介者将变得庞大复杂,难以维护。因此,中介者模式一般用于对象定义好但通信复杂的情景,或者为多个类定义一个行为而不产生子类的情景。
例如,各个国家以联合国为中介者进行事务处理。
namespace Mediator_
{
abstract class Mediator { // 抽象中介者
public abstract void Declare(string msg, Colleague colleague);
}
abstract class Colleague { // 抽象同事类
protected Mediator unitedNations;
public Colleague(Mediator mediator) { // 构造,获得中介者对象
this.unitedNations = mediator;
}
}
class UNSC: Mediator{ // 具体中介者,联合国安理会
private USA countryUSA; // Mediator知道所有Colleague
private Iraq countryIraq;
public USA CountryUSA { set { countryUSA=value; } }
public Iraq CountryIraq { set { countryIraq=value; } }
public override void Declare(string msg, Colleague country) { // 转发消息
if (country.Equals(countryUSA)) {
countryIraq.GetMsg(country.ToString() + " declared: " + msg);
} else {
countryUSA.GetMsg(country.ToString() + " declared: " + msg); }
}
}
class USA: Colleague { // 具体同事1:美国
public USA(Mediator mediator): base(mediator) { }
public void Declare(string msg){ // 发消息,由中介者发出
unitedNations.Declare(msg, this);
}
public void GetMsg(string msg) { // 接收消息,由中介者转发
Console.WriteLine($"USA ignored that {msg}!");
}
}
class Iraq : Colleague { // 具体同事2:伊拉克
public Iraq(Mediator mediator) : base(mediator) { }
public void Declare(string msg) {
unitedNations.Declare(msg, this); }
public void GetMsg(string msg) {
Console.WriteLine($"Iraq got: {msg}"); }
}
}
//--------------------------
using Mediator_;
UNSC unsc = new(); // Mediator
USA usa = new(unsc); // 所有Colleague知道Mediator
Iraq iraq = new(unsc);
unsc.CountryUSA = usa; // Mediator知道所有Colleague
unsc.CountryIraq = iraq;
usa.Declare("我们要维护伊拉克和平!");
iraq.Declare("你不要过来啊!");
3.18、备忘录
备忘录模式:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将这个对象恢复到原先保存的状态。备忘录模式适用于需要维护/记录历史状态的类或者保存部分属性。备忘录只能由对象自身读取,因此将内部信息保护起来。但是,如果状态数据很大会消耗内存。备忘录模式包括:Originator(发起人,即需要保存/恢复状态的对象,创建Memento)、Memento(备忘录,负责存储内部状态,并防止其他对象访问,Caretaker只能调用窄接口传递备忘录信息,Originator可以访问原状态的所有数据)、Caretaker(只负责保存Memento,不能修改或检查等操作)。
例如,保存游戏进度、命令模式中撤销操作,就需要备忘录模式。
namespace Memento_
{
class Originator { // 发起者(游戏角色)
private int vit, atk, def;
public int Vit { get { return vit; } set { vit = value; } }
public int Atk { get { return atk; } set { atk = value; } }
public int Def { get { return def; } set { def = value; } }
public void DisplayState() { Console.WriteLine($"State: VIT:{vit} ATK:{atk} DEF:{def}"); }
public void InitState() { vit = atk = def = 100; }
public void FightBoss() { vit = 0; Console.WriteLine("Wasted!"); }
public Memento SaveState() { return new Memento(vit, atk, def); } // 存储状态,返回Memento
public void RecoveryState(Memento mementoState) { // 恢复状态,自身可访问所有数据
Console.WriteLine("Recovery!");
vit = mementoState.Vit; atk = mementoState.Atk; def = mementoState.Def;
}
}
class Memento { // 备忘录(存储对象的内部状态)
private int vit, atk, def;
public Memento(int vit, int atk, int def) {
this.vit = vit; this.atk = atk; this.def = def;
}
public int Vit { get { return vit; } set { vit = value; } }
public int Atk { get { return atk; } set { atk = value; } }
public int Def { get { return def; } set { def = value; } }
}
class Caretaker{ // 备忘录管理者
private Memento memo; // 直接操作备忘录,而不知道内部状态
public Memento Memo { get { return memo; } set { memo = value; } }
}
}
3.19、观察者
观察者模式:定义了一种一对多的依赖关系,让多个观察者同时监听某一个主体。这个主体在状态发生变化时,会通知所有观察者。观察者模式适用于系统有两个方面(主体/观察者),且一方面依赖于另一方面,同时一方面变化时,另一方面的不知数量的对象也随之变化。观察者模式是将该系统进行解耦,让该系统的两个方面都依赖于抽象,使得某一方变化时不会影响另一方。观察者模式包括:Subject(主体,保存/增删相关的观察者,并可以通知)、Observer(观察者,在主体通知发生变化时,进行更新)。
例如,工作摸鱼的员工观察上司来没来。
namespace Observer_
{
abstract class Observer { // 抽象观察者
protected string name;
protected ISubject subject;
public Observer(string name, ISubject subject) {
this.name = name;
this.subject = subject;
}
public abstract void Update(); // 更新
}
class Colleague: Observer { // 具体观察者(员工)
public Colleague(string name, ISubject subject):base(name, subject) { }
public override void Update() { Console.WriteLine($"{subject.SubjectState} {name} return to working!"); }
}
interface ISubject { // 主体(接口)
string SubjectState { get; set; } // 主体的状态
void Attach(Observer observer); // 增加观察者
void Detach(Observer observer); // 删除观察者
void Notify(); // 通知观察者
}
class SubjectBoss: ISubject { // 具体主体(老板)
private IList<Observer> observers = new List<Observer>(); // 存储Observer
private string subjectState;
public string SubjectState {
get { return subjectState; }
set { subjectState = value; Notify(); } } // 状态变化时通知
public void Attach(Observer obs) { observers.Add(obs); } // 增加
public void Detach(Observer obs) { observers.Remove(obs); } // 删除
public void Notify() { // 通知
foreach (Observer obs in observers) { obs.Update(); }
}
}
}
当Observer被封装时,各个具体观察者无法继承抽象观察者,具体观察者的Update()操作可能不同(方法名不同)。根据依赖倒转原则,主体Subject不能依赖抽象观察者Observer来完成通知,因此要删除主体中的增加/删除操作。原本通知Notify()中对观察者的遍历并调用更新Update(),可以使用 .NET 中的事件委托来完成。
委托是一种引用方法的类型。一旦为委托分配了方法,委托的行为与被分配的方法一致。委托方法的使用可以像其他任何方法一样,可以具有参数和返回值。委托可以看作是对函数的抽象,是函数的“类”,委托的实例代表一个具体的函数。一个委托可以承载多个方法,所有方法被依次调用。并且,委托所承载的方法可以属于不同的类。但是,委托所承载的方法必须有相同的原型,即承载的方法必须有相同的参数列表和返回值类型。
下例是使用委托完成的员工观察老板。
namespace ObserverDelegate_
{
class Colleague1 { // 具体观察者1
private string name;
private ISubject sub;
public Colleague1(string name, ISubject sub) { this.name = name; this.sub = sub; }
public void ReturnToWorking1() { Console.WriteLine($"{sub.SubjectState} {name} return to working!"); }
}
interface ISubject { // 主体(通知者接口)
private void Notify() { }
string SubjectState { get; set; }
}
delegate void Handler(); // 委托
class SubjectBoss: ISubject {
public event Handler Update; // 事件
private string subjectState;
public string SubjectState {
get { return subjectState; }
set { subjectState = value; Notify(); }
}
private void Notify() { Update(); } // 通知时调用更新
}
}
//-------------------------
using ObserverDelegate_;
SubjectBoss boss = new();
Colleague1 tom = new("Tom", boss);
Colleague1 jerry = new("Jerry", boss);
boss.Update += new Handler(tom.ReturnToWorking1);
boss.Update += new Handler(jerry.ReturnToWorking1);
boss.SubjectState = "Boss is coming!";
3.20、状态
状态模式:当一个对象的内部状态改变时,允许改变其行为,这个对象看起来类似于改变了类。主要用于解决一个对象状态转换的条件表达式过于复杂的情况。把状态的判断逻辑转移到表示不同状态的类中,减少相互间的依赖,可以把复杂的判断逻辑简化。这种枚举状态的类必然会导致系统中类、对象的增加,结构复杂、实现困难,增加新状态需要修改状态切换方法。状态模式主要包括:State(抽象状态类,封装特定状态的行为)、ConcreteState(具体状态类,每一个子类实现一个特定状态的行为)、Context(上下文,存储当前状态的实例,调用State的方法)。
当一个对象的行为取决于它的状态,并且必须在运行时根据状态改变行为,就可以使用状态模式。例如,工作时不同的工作状态下工作效率不同。
namespace State_
{
abstract public class State { // 抽象状态类
abstract public void Programming(Context work);
}
public class ForenoonState: State { // 具体状态1
public override void Programming(Context work) { // 重写行为
if (work.Time < 12) {
Console.WriteLine($"当前{work.Time}点,工作高效!");
} else {
work.SetState(new NoonState()); work.Programming();
}
}
}
public class NoonState: State{ // 具体状态2
public override void Programming(Context work) { // 重写行为
if (work.Time < 13) {
Console.WriteLine($"当前{work.Time}点,午休!");
} else {
work.SetState(new AfternoonState()); work.Programming();
}
}
}
public class AfternoonState : State{ // 具体状态3
public override void Programming(Context work){ // 重写行为
if (work.Time < 18) {
Console.WriteLine($"当前{work.Time}点,下午犯困!");
} else {
work.SetState(new EveningState()); work.Programming();
}
}
}
public class EveningState : State { // 具体状态4
public override void Programming(Context work) { // 重写行为
if (work.IsDone) {
Console.WriteLine($"当前{work.Time}点,工作完成,已经下班了!");
} else {
Console.WriteLine($"当前{work.Time}点,工作没完成,正在加班!");
}
}
}
public class Context { // 存储当前的状态实例,并根据状态执行操作
private State current;
public Context() { current = new ForenoonState(); } // 初始化状态
private double time;
public double Time { get { return time; } set { time = value; } }
private bool isDone = false;
public bool IsDone { get { return isDone; } set { isDone = value; } }
public void SetState(State state) { current = state; } // 设置状态(进入下一个判断逻辑)
// 实际执行State类的Programming(),其中包括逻辑判断和对应的行为
public void Programming() { current.Programming(this); }
}
}
//------------------
using State_;
Context work = new();
work.Time = 21;
work.Programming(); // 实际执行State类的Programming()
work.Time = 22;
work.IsDone = true;
work.Programming();
3.21、策略
策略模式:定义了算法家族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化不会影响到使用算法的客户。策略模式通过定义一系列算法来解决相同任务有不同执行方式的问题。这样可用相同的方法调用所有算法,减少了算法类与调用者的耦合。策略模式可以减少if...else或switch语句。策略模式包括:Strategy(定义支持所有算法的公共接口)、ConcreteStrategy(继承Strategy,封装具体的算法)、Context(上下文,存储一个策略的实例,调用该策略的方法,可结合简单工厂,进行逻辑判断)。
与状态模式的区别:状态模式是在运行过程中,Context中的状态改变,执行不同状态下的动作;策略模式是通过Context的决策来执行不同的策略。
策略模式封装了变化,可以用来封装类的规则,需要在不同条件下应用不同业务时,可以使用其解决。而在Context中的switch语句,可以使用反射解决。例如,超市不同的促销方式。
namespace Strategy_
{
abstract class Strategy{ // 抽象策略,统一接口
abstract public void Cashier(double money);
}
class StrategyNormal: Strategy { // 具体策略1
public override void Cashier(double money) {
Console.WriteLine($"收银:{money},正常找零!");
}
}
class StrategyDiscount : Strategy{ // 具体策略2
public override void Cashier(double money)
{
Console.WriteLine($"收银:{money},打八折!");
}
}
class Context { // 上下文,存储一个策略实例,并调用该策略的方法
Strategy strategy = null;
public Context(string type) { // 策略的判决
switch (type) {
default:
strategy = new StrategyNormal(); break;
case "Discount":
strategy = new StrategyDiscount(); break;
}
}
public void Cashier(double money){
strategy.Cashier(money);
}
}
}
使用反射改写Context类:
using Strategy_;
using System.Reflection;
namespace StrategyReflection_
{
class ContextReflection {
Strategy strategy = null;
private static string _assembly = "CsharpLearning"; // 程序集
private static string _namespace = "Strategy_"; // 命名空间
public ContextReflection(string type) {
string classname = _namespace + ".Strategy" + type; // 命名空间.类名
strategy = (Strategy)Assembly.Load(_assembly).CreateInstance(classname);
}
public void Cashier(double money){
strategy.Cashier(money);
}
}
}
3.22、模板
模板模式:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。在整体一致的过程或一系列步骤中,有某些细节的实现不同时,可以使用模板模式解决。但是,每一个不同的实现都需要一个子类。模板方法包括:AbstractClass(实现了一个模板方法,定义了算法的整体的顶级逻辑框架,实现不同的细节以抽象方法声明,延迟到子类中实现)、ConcreteClass(继承并实现AbstractClass中的抽象方法,不同的子类实现方式不同)。
模板方法提供了一个代码复用的平台,将不变行为搬移到超类,去除子类中的重复代码。
namespace Template_
{
abstract class AbstractGame { // 抽象的系统
public void Start() { Console.WriteLine("Game Start!"); }
abstract public void Play(); // 细节的不同实现延迟到子类
public void Finish() { Console.WriteLine("Game Finish!"); }
}
class Go: AbstractGame { // 细节实现1
public override void Play() { Console.WriteLine("下围棋!"); }
}
class Gobang : AbstractGame { // 细节实现2
public override void Play() { Console.WriteLine("下五子棋!"); }
}
}
//---------------------------
using Template_;
IList<AbstractGame> games = new List<AbstractGame> {
new Go(),
new Gobang()
};
foreach (AbstractGame game in games) {
game.Start();
game.Play();
game.Finish();
}
3.23、访问者
访问者模式:表示一个作用于某对象结构中的各元素的操作,可以在不改变各元素的类的前提下定义作用于这些元素的新操作。访问者模式将结构和对结构的操作分离,降低稳定的结构与异变的操作间的耦合,使得对结构的操作可以相对自由地变化。访问者模式易于增加新的操作,反之,难以增加新的结构。访问者模式包括:Visitor(抽象访问者,为结构ObjectStructure中的每一个元素Element声明一个操作)、ConcreteVisitor(具体访问者,实现Visitor中的操作)、ObjectStructure(结构类,枚举结构中的元素,并提供一个高层接口使Visitor可以访问其中的元素)、Element(抽象的元素类,声明一个访问操作,参数为Visitor)、ConcreteElement(具体的元素类,实现接收的Visitor中声明的操作)。
例如,人类的性别相对稳定,但是不同性别的行为容易变化。
namespace Visitor_
{
abstract class Visitor_Behavior { // 抽象访问者
public abstract void ElementManBehave(ConcreteElement_Man man);
public abstract void ElementWomanBehave(ConcreteElement_Woman woman);
}
class ConcreteVisitor_Behavior1: Visitor_Behavior { // 具体访问者1
public override void ElementManBehave(ConcreteElement_Man man) {
Console.WriteLine($"{man}'s Behavior111"); }
public override void ElementWomanBehave(ConcreteElement_Woman woman){
Console.WriteLine($"{woman}'s Behavior111");
}
}
class ConcreteVisitor_Behavior2 : Visitor_Behavior{ // 具体访问者2
public override void ElementManBehave(ConcreteElement_Man man) {
Console.WriteLine($"{man}'s Behavior222");
}
public override void ElementWomanBehave(ConcreteElement_Woman woman) {
Console.WriteLine($"{woman}'s Behavior222");
}
}
abstract class Element_Person { // 抽象元素类
abstract public void Accept(Visitor_Behavior visitor); // 接收Visitor作为参数
}
class ConcreteElement_Man: Element_Person { // 具体元素类1
public override void Accept(Visitor_Behavior visitor) { // 接收Visitor并执行该Vistior对该元素的操作
visitor.ElementManBehave(this);
}
}
class ConcreteElement_Woman : Element_Person { // 具体元素类2
public override void Accept(Visitor_Behavior visitor) {
visitor.ElementWomanBehave(this);
}
}
class ObjectStructure{ // 结构类
private IList<Element_Person> persons = new List<Element_Person>(); // 枚举元素
public void Add(Element_Person person) { persons.Add(person); } // 增加元素
public void Del(Element_Person person) { persons.Remove(person); }
public void Accept(Visitor_Behavior behave) { // 对结构中的元素执行对应的操作(访问者)
foreach (Element_Person p in persons) {
p.Accept(behave);
}
}
}
}
//-------------------------
using Visitor_;
ObjectStructure s = new(); // 结构
s.Add(new ConcreteElement_Man()); // 元素
s.Add(new ConcreteElement_Woman());
ConcreteVisitor_Behavior1 b1 = new(); // 对结构中元素的操作
ConcreteVisitor_Behavior2 b2 = new();
s.Accept(b1); // 元素接收操作(访问者),并执行对应该元素的操作
s.Accept(b2);
其他模式
3.24、MVC模式
MVC模式:Model-View-Controller(模型-视图-控制器) 模式,用于应用程序的分层开发。
Model:代表一个存取数据的对象,也可以带有逻辑,在数据变化时更新Controller。
View:模型包含的数据的可视化。
Controller:作用于模型和视图上,控制数据流向模型对象,并在数据变化时更新视图。