设计模式简要汇总

news2024/11/29 20:56:37

一、面向对象设计原则

  • 开闭原则:一个软件实体(类、模块、函数)应该对扩展开放,对修改关闭。
  • 依赖倒置原则:高层模块不应该依赖底层模块,它们都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
  • 里氏替换原则:派生类(子类)对象可以在程序中代替其基类(超类)对象。
  • 单一职责原则:一个类或者模块只负责完成一个职责(或者功能)。
  • 接口隔离原则:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
  • 合成复用原则:尽量使用对象组合,而不是继承来达到复用的目的。
  • 迪米特原则:每个软件单位对其他单位都具有最少知识,而且局限于那些与本单位密切相关的软件单位。

二、创建型模式

2.1 简单工厂模式

简单工厂(Simple Factory)模式是属于创建型模式,又叫做静态工厂方法(Static Factory Method)模式,它定义了一个工厂对象决定创建出哪一种产品类的实例。

简单工厂模式的结构图如下。SimpleFactory.CreateProduct()方法根据传入的参数决定具体的实例化对象,且返回的是抽象类Product。这意味着客户端无需关心创建的产品具体是什么类型。

示例代码如下

// 简单工厂
public class SimpleFactory
{
    public static Product? CreateProduct(string param)
    {
        Product? product = null;
        switch (param)
        {
            case "A":
                product = new ProductA();
                break;
            case "B":
                product = new ProductB();
                break;
            case "C":
                product = new ProductC();
                break;
        }
        return product;
    }
}

// 抽象产品类
public abstract class Product
{
    public abstract void Operate();
}
// 具体产品类
public class ProductA:Product
{
    public override void Operate()
    {
        Console.WriteLine("OperationA");
    }
}
public class ProductB:Product
{
    public override void Operate()
    {
        Console.WriteLine("OperationB");
    }
}
public class ProductC:Product
{
    public override void Operate()
    {
        Console.WriteLine("OperationC");
    }
}

客户端调用如下

Product productA = SimpleFactory.CreateProduct("A");  
productA.Operate();  
  
Product productB = SimpleFactory.CreateProduct("B");  
productB.Operate();  
  
Product productC = SimpleFactory.CreateProduct("C");  
productC.Operate();  
  
// 输出结果:  
// OperationA  
// OperationB  
// OperationC

简单工厂模式的优点是工厂类包含了创建产品的逻辑,客户端只需要传入所需的参数即可获得对应的产品。在这个过程中,客户端甚至不需要知道产品具体是什么类型。

但简单工厂模式的缺点是一旦需要增加产品类型,就需要修改工厂类,这并不符合开闭原则。

2.2 工厂方法模式

工厂方法模式(Factory Method),定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。

工厂方法模式的结构图如下。与简单工厂模式不同的是,产品的实例化不再由单一的工厂负责,而是一个工厂生产一种具体的产品。客户端需要哪种类型的产品只需要实例化对应的工厂对象即可。

示例代码如下

// 抽象产品类
public abstract class Product { }
// 具体产品类
public class ProductA:Product { }
public class ProductB:Product { }
public class ProductC:Product { }

// 抽象工厂类
public abstract class Factory
{
    public abstract Product CreateProduct();
}
// 具体工厂类
public class FactoryA : Factory
{
    public override Product CreateProduct()
    {
        return new ProductA();
    }
}
public class FactoryB : Factory
{
    public override Product CreateProduct()
    {
        return new ProductB();
    }
}
public class FactoryC : Factory
{
    public override Product CreateProduct()
    {
        return new ProductC();
    }
}

客户端调用方式如下

Factory factory1 = new FactoryA();  
var product1 = factory1.CreateFruit();  
  
Factory factory2 = new FactoryB();  
var product2 = factory2.CreateFruit();  
  
Factory factory3 = new FactoryC();  
var product3 = factory3.CreateFruit();

工厂方法模式继承了简单工厂模式的优点——避免创建者与具体产品之间的紧密联系,又满足了开闭原则——引入新的产品只需要增加工厂类。由于一个工厂类只负责一种产品的创建,所以同样也满足了单一职责原则。

工厂方法的缺点是需要引入许多新的子类,这会让代码更加复杂。

2.3 抽象工厂模式

抽象工厂模式(Abstract Factory),提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

抽象工厂模式的结构图如下。抽象工厂模式与工厂方法模式有些相似,只不过抽象工厂模式中的工厂可以创建“一系列”产品(如工厂A可以生产A1、B1,工厂B可以生产A2、B2),而工厂方法模式中的工厂只负责单一产品的生产。

示例代码如下

// 抽象工厂类
public abstract class Factory
{
    public abstract ProductA GetProductA();
    public abstract ProductB GetProductB();
}

// 抽象产品类
public abstract class ProductA
{
    public abstract void ShowA();
}
public abstract class ProductB
{
    public abstract void ShowB();
}
// 具体产品类
public class ConcreteProductA1 : ProductA
{
    public override void ShowA()
    {
        Console.WriteLine("ProductA1");
    }
}
public class ConcreteProductA2 : ProductA
{
    public override void ShowA()
    {
        Console.WriteLine("ProductA2");
    }
}
public class ConcreteProductB1 : ProductB
{
    public override void ShowB()
    {
        Console.WriteLine("ProductB");
    }
}
public class ConcreteProductB2 : ProductB
{
    public override void ShowB()
    {
        Console.WriteLine("ProductB2");
    }
}
// 具体工厂类
public class FactoryA : Factory
{
    // 生产A1
    public override ProductA GetProductA()
    {
        return new ConcreteProductA1();
    }

    // 生产B1
    public override ProductB GetProductB()
    {
        return new ConcreteProductB1();
    }
}
public class FactoryB : Factory
{
    // 生产A2
    public override ProductA GetProductA()
    {
        return new ConcreteProductA2();
    }
    // 生产B2
    public override ProductB GetProductB()
    {
        return new ConcreteProductB2();
    }
}

客户端调用

// 确定实例化哪个工厂  
// Factory factory = new FactoryA();  
Factory factory = new FactoryB();  
  
var productA = factory.GetProductA();  
productA.ShowA();  
  
var productB = factory.GetProductB();  
productB.ShowB();  
  
// 输出结果:  
// ProductA2  
// ProductB2

抽象工厂模式的好处便是易于交换产品系列,由于具体工厂类在一个应用中只需要在初始化的时候出现一次,这就使得改变一个应用的具体工厂变得非常容易,它只需要改变具体工厂即可使用不同的产品配置。

其次它让具体的创建实例过程与客户端分离,客户端是通过它们的抽象接口操纵实例,产品的具体类名也被具体工厂的实现分离,不会出现在客户代码中。

它的缺点是每增加一种产品就需要进行大量改动。可以通过反射+简单工厂模式对其进行改进来解决这一问题。只需要用下面这个工厂代替其他工厂即可

public class FactoryPlus
{
	// 改变产品系列只需要修改这个字段
    // private static readonly string ProductType = "1";
    private static readonly string ProductType = "2";
    
    public static ProductA GetProductA()
    {
        // 通过反射避免了手动添加分支
        string className = "Namespace.ConcreteProductA" + ProductType;
        return (ProductA) Assembly.Load("CSharpPractice").CreateInstance(className);
    }
    
    public static ProductB GetProductB()
    {
        string className = "Namespace.ConcreteProductB" + ProductType;
        return (ProductB) Assembly.Load("CSharpPractice").CreateInstance(className);
    }
}

2.4 建造者模式

建造者模式(Builder),将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

建造者模式的结构图如下。对于不同的产品需要不同的生产线,BuilderABuilderB就是两条不同的生产线。而Director则负责确定组装顺序的工作。

示例代码如下

// 产品类,一个产品有多个部件
public class Product
{
    private readonly List<string> _parts = new();

    // 添加部件
    public void AddPart(string part)
    {
        _parts.Add(part);
    }
    // 展示产品
    public void Show()
    {
        Console.WriteLine("产品包含如下部件:");
        foreach (var part in _parts)
        {
            Console.WriteLine(part);
        }
    }
}
// 抽象建造者类,具体建造什么部件由子类决定
public abstract class Builder
{
    public abstract void BuildPartA();
    public abstract void BuildPartB();
    public abstract Product GetResult();
}
// 建造者1
public class BuilderA : Builder
{
    private readonly Product _product = new Product();
    
    public override void BuildPartA()
    {
        _product.AddPart("部件A");
    }

    public override void BuildPartB()
    {
        _product.AddPart("部件B");
    }

    public override Product GetResult()
    {
        return _product;
    }
}
// 建造者2
public class BuilderB : Builder
{
    private readonly Product _product = new Product();
    
    public override void BuildPartA()
    {
        _product.AddPart("部件X");
    }

    public override void BuildPartB()
    {
        _product.AddPart("部件Y");
    }

    public override Product GetResult()
    {
        return _product;
    }
}
// 指挥者,用来指挥建造过程
public class Director
{
    public void Construct(Builder builder)
    {
        builder.BuildPartA();
        builder.BuildPartB();
    }
}

客户端调用如下

Director director = new Director();  
Builder builderA = new BuilderA();  
Builder builderB = new BuilderB();  
  
director.Construct(builderA);  
var productA = builderA.GetResult();  
productA.Show();  
  
director.Construct(builderB);  
var productB = builderB.GetResult();  
productB.Show();  
  
// 输出结果:  
// 产品包含如下部件:  
// 部件A  
// 部件B  
// 产品包含如下部件:  
// 部件X  
// 部件Y

建造者模式是在当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时适用的模式。

建造者模式的好处就是使得建造代码与表示代码分离,由于建造者隐藏了该产品是如何组装的,所以若需要改变一个产品的内部表示,只需要再定义一个具体的建造者就可以了。

2.5 原型模式

原型模式(Prototype),用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

原型模式的结构图如下。IPrototype接口声明了Clone()克隆方法,实现类ConcretePrototypeClone()方法进行实现,将值类型的数据拷贝到克隆体中。如果数据中包含引用类型,还需要考虑递归克隆的情况。

代码示例如下

// 原型接口,声明克隆方法
public interface IPrototype
{
    public IPrototype Clone();
}
// 具体原型类
public class ConcretePrototype:IPrototype
{
    public string Id;

    public ConcretePrototype(string id)
    {
        Id = id;
    }
    public IPrototype Clone()
    {
        // 浅复制
        return (IPrototype)this.MemberwiseClone();
    }
}

客户端调用方式如下

ConcretePrototype p1 = new ConcretePrototype("123456");  
ConcretePrototype p2 = (ConcretePrototype)p1.Clone();  
  
Console.WriteLine(p1.Id);  
Console.WriteLine(p2.Id);  
// 输出结果:  
// 123456  
// 123456

原型模式其实就是从一个对象再创建另外一个可定制的对象,而且不需知道任何创建的细节。一般在初始化的信息不发生变化的情况下,克隆是最好的办法。这既隐藏了对象创建的细节,又对性能是大大的提高。

2.6 单例模式

单例模式(Singleton),保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式的结构图如下。Singleton持有了一个自身的静态实例,同时将构造方法私有化。当外接需要获取Singleton的实例时,就返回已经持有的静态实例(如果没有就实例化)。这样外部获取的Singleton的实例永远只是固定的一个。

示例代码如下

public class Singleton
{
    // 持有一个自己的实例
    private static Singleton _instance;
    
    // 构造函数私有化防止外部创建实例
    private Singleton()
    {
        
    }

    // 获取唯一实例
    public static Singleton GetInstance()
    {
        if (_instance == null)
        {
            _instance = new Singleton();
        }
        return _instance;
    }
}

客户端调用如下

Singleton s1 = Singleton.GetInstance();  
Singleton s2 = Singleton.GetInstance();  
  
Console.WriteLine(s1 == s2);
// 输出结果:
// True

上面这种方式只在需要实例时才进行初始化,被成为懒汉式单例类,还有一种方式是在类加载时就进行实例化(静态初始化),这种方式称为饿汉式单例类。

// 饿汉式单例类,加sealed关键字防止派生增加实例
public sealed class Singleton2
{
    // 第一次引用类的任何成员时创建实例
    private static readonly Singleton2 _instance = new Singleton2();
    
    // 构造函数私有化防止外部创建实例
    private Singleton2()
    {
        
    }

    // 获取唯一实例
    public static Singleton2 GetInstance()
    {
        return _instance;
    }
}

单例模式因为Singleton类封装它的唯一实例,这样它可以严格地控制客户怎样访问它以及何时访问它。简单地说就是对唯一实例的受控访问。

三、结构型模式

3.1 适配器模式

适配器模式(Adapter),将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

适配器模式结构图如下。当客户端需要持有一个Target对象,但现在只有一个Origin对象。为了将Origin对象“伪装”成Target类型,我们需要一个中间的适配器AdapterAdapter持有Origin对象,同时又继承了Target类。在Adapter类重写的方法中调用Origin对象的对应方法。现在,客户端只需要持有Adapter对象即完成了适配。

示例代码如下

// 客户端期待的接口或类
public class Target
{
    public virtual void TargetRequest()
    {
        Console.WriteLine("Target请求");
    }
}
// 需要适配的接口或类
public class Origin
{
    public virtual void OriginRequest()
    {
        Console.WriteLine("Origin请求");
    }
}
// 适配器
public class Adapter : Target
{
    private Origin _origin = new Origin();
    
    // 表面调用TargetRequest(),实际调用OriginRequest()
    public override void TargetRequest()
    {
        _origin.OriginRequest();
    }
}

客户端调用如下

Target target = new Adapter();  
target.TargetRequest();  
// 输出结果:  
// Origin请求

系统的数据和行为都正确,但接口不符时,我们应该考虑用适配器,目的是使控制范围之外的一个原有对象与某个接口匹配。适配器模式主要应用于希望复用一些现存的类,但是接口又与复用环境要求不一致的情况。

3.2 桥接模式

桥接模式(Bridge),将抽象部分与它的实现部分分离,使它们都可以独立地变化。这里的实现指的是抽象类和它的派生类用来实现自己的对象。

这里的“抽象”与“实现”有些让人摸不着头脑。我们通过一个具体的例子来加以理解。假设有一个形状类Shape,它可以扩展出两个子类:圆形Circle和方形Square。现在,我们要对类层次进行扩展,使其包含颜色,比如红色Red和蓝色Blue。那么这两个颜色就需要和之前的形状进行组合,最后形成:红色圆形RedShape、红色方形RedSquare、蓝色圆形BlueCircle、蓝色方形BlueSquare四个类。显然这是个笨办法,因为每扩展一种维度,类的数量就会呈指数增长。

造成这种现象的原因是因为我们试图从多个独立的维度上扩展类。而更明智的做法是抽取其中的某些维度作为独立的类,从而我们就可以在初始类中引用这些维度的对象。这就是桥接模式的基本思想。

桥接模式结构图如下。这里的抽象Abstraction可以看做形状类,Implementor就相当于颜色类。Abstraction通过持有Implementor对象,使自己拥有了Implementor的状态和行为。

示例代码如下

// 实现  
public abstract class Implementor  
{  
    public abstract void Operation();  
}
// 具体实现
public class ImplementorA : Implementor
{
    public override void Operation()
    {
        Console.WriteLine("具体实现A");
    }
}
public class ImplementorB : Implementor
{
    public override void Operation()
    {
        Console.WriteLine("具体实现B");
    }
}
// 抽象
class Abstraction
{
    protected Implementor Implementor;
 
    public void  SetImplementor(Implementor implementor)
    {
        Implementor = implementor;
    }
 
    public virtual void Operation()
    {
        Implementor.Operation();
    }
}
// 被提炼的抽象
class RefinedAbstraction : Abstraction
{
    public override void Operation()
    {
        Implementor.Operation();
    }
}

客户端调用如下

Abstraction ab = new RefinedAbstraction();  
  
// 设置具体的实现  
ab.SetImplementor(new ConcreteImplementorA());  
ab.Operation();  
  
ab.SetImplementor(new ConcreteImplementorB());  
ab.Operation();  
  
// 输出结果:  
// 具体实现A  
// 具体实现B

如果你想要拆分或重组一个具有多重功能的庞杂类 (例如能与多个数据库服务器进行交互的类),或希望在几个独立维度上扩展一个类,又或者需要在运行时切换不同实现方法就可以尝试使用这个模式。

3.3 组合模式

组合模式(Composite),将对象组合成树形结构以表示‘部分-整体’的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

假设我们需要统计公司的人员信息,最笨的办法就是挨个人员统计。一般都会想到将统计任务交给下级部门,要求下级部门完成后直接上报结果。从公司级->院级->部门层层下放。然后最底层的层级将本层的人员信息统计完成后上交。这其实就是组合模式的思想。

组合模式的结构图如下。Component描述了叶节点和容器节点的共有行为。Leaf是叶节点,叶节点不会再包含其他叶节点或容器。Composite是容器节点,它可以包含其他容器或叶节点。事实上叶节点只能执行Display()操作,之所以将添加和删除的方法也写进抽象类,是为了消除叶节点和容器节点在抽象层次的区别。

示例代码如下

// 组件抽象类
public abstract class Component
{
    protected string Name;

    public Component(string name)
    {
        Name = name;
    }

    public abstract void Add(Component c);
    public abstract void Remove(Component c);
    public abstract void Display(int depth);
}
// 叶节点类
public class Leaf : Component
{
    public Leaf(string name) : base(name)
    {
    }
    // 叶节点本身无法添加或删除节点,这样做是为了消除叶节点和容器节点在抽象层次的区别
    public override void Add(Component c)
    {
        Console.WriteLine("无法添加节点");
    }

    public override void Remove(Component c)
    {
        Console.WriteLine("无法删除节点");
    }
    // 显示叶节点
    public override void Display(int depth)
    {
        Console.WriteLine(new string('-',depth)+Name);
    }
}
// 容器节点类
public class Composite : Component
{
    // 用来存储子对象
    private readonly List<Component> _children = new();
    
    public Composite(string name) : base(name)
    {
    }

    public override void Add(Component c)
    {
        _children.Add(c);
    }

    public override void Remove(Component c)
    {
        _children.Remove(c);
    }

    public override void Display(int depth)
    {
        Console.WriteLine(new string('-',depth)+Name);
        foreach (var child in _children)
        {
            child.Display(depth+2);
        }
    }
}

客户端调用如下

Composite root = new Composite("Root");  
root.Add(new Leaf("C"));  
root.Add(new Leaf("D"));  
  
var compositeA = new Composite("A");  
compositeA.Add(new Leaf("E"));  
compositeA.Add(new Leaf("F"));  
root.Add(compositeA);  
  
var compositeB = new Composite("B");  
compositeB.Add(new Leaf("G"));  
compositeB.Add(new Leaf("H"));  
root.Add(compositeB);  
  
root.Display(1);  
  
// 输出结果:  
// -Root  
// ---C  
// ---D  
// ---A  
// -----E  
// -----F  
// ---B  
// -----G  
// -----H

当你发现需求中是体现部分与整体层次的结构时,以及你希望用户可以忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象时,就应该考虑用组合模式了。

组合模式的优点是它就定义了基本对象组合对象的类层次结构。基本对象可以被组合成更复杂的组合对象,而这个组合对象又可以被组合,这样不断地递归下去,客户代码中,任何用到基本对象的地方都可以使用组合对象。用户是不用关心到底是处理一个叶节点还是处理一个组合组件,也就用不着为定义组合而写一些选择判断语句了。

3.4 装饰模式

装饰模式(Decorator),通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。

装饰模式就像是俄罗斯套娃,可以通过装饰器对原本的内容层层包装。就像是我们在家可以只穿一身秋衣,如果觉得冷的话还可以套一身保暖衣,如果要出门的话还可以再套一层羽绒服。这就是装饰模式的基本思想。这种模式在数据的加密和压缩中很有用。

装饰模式的结构图如下。ConcreteComponent就是最原始的内容,ConcreteDecoratorAConcreteDecoratorB都可以对其进行装饰。

示例代码如下

public abstract class Component
{
    public abstract void Operation();
}
// 原始组件
public class ConcreteComponent : Component
{
    public override void Operation()
    {
        Console.WriteLine("ConcreteComponent");
    }
}
// 抽象装饰类
public abstract class Decorator : Component
{
    protected Component? Component;

    public void SetComponent(Component component)
    {
        Component = component;
    }

    public override void Operation()
    {
        // 执行的是被装饰对象的Operation()
        Component?.Operation();
    }
}
// 具体装饰类
public class ConcreteDecoratorA : Decorator
{
    public override void Operation()
    {
        base.Operation();
        Console.WriteLine("ConcreteDecoratorA");
    }
}
public class ConcreteDecoratorB : Decorator
{
    public override void Operation()
    {
        base.Operation();
        Console.WriteLine("ConcreteDecoratorB");
    }
}

客户端调用如下

// 原始数据  
ConcreteComponent c = new ConcreteComponent();  
// 装饰类对象  
ConcreteDecoratorA a = new ConcreteDecoratorA();  
ConcreteDecoratorB b = new ConcreteDecoratorB();  
  
// 开始套娃(装饰)  
a.SetComponent(c);  
b.SetComponent(a);  
b.Operation();  
  
// 输出结果:  
// ConcreteComponent  
// ConcreteDecoratorA  
// ConcreteDecoratorB

装饰模式是利用SetComponent来对对象进行包装的。这样每个装饰对象的实现就和如何使用这个对象分离开了,每个装饰对象只关心自己的功能,不需要关心如何被添加到对象链当中。

装饰模式是为已有功能动态地添加更多功能的一种方式。它可以有效地把类的核心职责和装饰功能区分开。而且可以去除相关类中重复的装饰逻辑。

3.5 外观模式

外观模式(Facade),为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

外观模式很容易理解,就是用一个外观类将原本复杂的功能整合到一起。外观模式的结构图如下

代码示例如下

// 子系统
class SubSystemOne
{
    public void MethodOne()
    {
        Console.WriteLine(" 子系统方法一");
    }
}
 
class SubSystemTwo
{
    public void MethodTwo()
    {
        Console.WriteLine(" 子系统方法二");
    }
}
 
class SubSystemThree
{
    public void MethodThree()
    {
        Console.WriteLine(" 子系统方法三");
    }
}
 
class SubSystemFour
{
    public void MethodFour()
    {
        Console.WriteLine(" 子系统方法四");
    }
}
// 外观类
public class Facade
{
    private SubSystemOne _one;
    private SubSystemTwo _two;
    private SubSystemThree _three;
    private SubSystemFour _four;

    public Facade()
    {
        _one = new SubSystemOne();
        _two = new SubSystemTwo();
        _three = new SubSystemThree();
        _four = new SubSystemFour();
    }

    public void Method1()
    {
        _one.MethodOne();
        _two.MethodTwo();
        _three.MethodThree();
    }

    public void Method2()
    {
        _one.MethodOne();
        _four.MethodFour();
    }
}

客户端调用如下

Facade facade = new Facade();  
facade.Method1();  
Console.WriteLine("————————————");  
facade.Method2();  
  
// 输出结果:  
// 子系统方法一  
// 子系统方法二  
// 子系统方法三  
// ————————————  
// 子系统方法一  
// 子系统方法四

在下面几种情况下可以考虑使用外观模式:
首先,在设计初期阶段,应该要有意识的将不同的两个层分离,比如经典的三层架构,就需要考虑在数据访问层和业务逻辑层、业务逻辑层和表示层的层与层之间建立外观Facade,这样可以为复杂的子系统提供一个简单的接口,使得耦合大大降低。

其次,在开发阶段,子系统往往因为不断的重构演化而变得越来越复杂,大多数的模式使用时也都会产生很多很小的类,这本是好事,但也给外部调用它们的用户程序带来了使用上的困难,增加外观Facade可以提供一个简单的接口,减少它们之间的依赖。

第三,在维护一个遗留的大型系统时,可能这个系统已经非常难以维护和扩展了,但因为它包含非常重要的功能,新的需求开发必须要依赖于它。此时用外观模式Facade也是非常合适的。你可以为新系统开发一个外观Facade类,来提供设计粗糙或高度复杂的遗留代码的比较清晰简单的接口,让新系统与Facade对象交互,Facade与遗留代码交互所有复杂的工作。

3.6 享元模式

享元模式(Flyweight),运用共享技术有效地支持大量细粒度的对象。

假设我们要开发一款飞机大战的游戏。游戏中的飞机可以发射多种弹药。假如每发射一枚弹药就实例化一枚子弹,那么当屏幕中的子弹数量较多时,就会消耗大量的性能。事实上对于一枚子弹来说,改变的只有其在场景中的坐标。而子弹的贴图、颜色等对于同一种弹药来说是完全相同的。所以我们完全可以将这些不变的属性抽象出来,形成享元类,通过引用的方式添加到子弹类中。然后通过一个享元工厂生产这些享元类。事实上,享元工厂中只需要为每种类型的子弹创建一个享元对象就足够了。因为享元对象的数据并不会被修改,所以所有的子弹都可以共享这几个享元类对象。这样就节省了许多性能。

享元模式的结构图如下。

示例代码如下

// 享元类
public class Flyweight
{
    public int Shared1 { get; }
    public int Shared2 { get; }

    public Flyweight(int shared1, int shared2)
    {
        Shared1 = shared1;
        Shared2 = shared2;
    }
}
// 享元工厂
public class FlyweightFactory
{
    private readonly Dictionary<string,Flyweight> _flyweights = new();
    
    public FlyweightFactory()
    {
        // 初始化时生成三个实例
        _flyweights.Add("X",new Flyweight(1,2));
        _flyweights.Add("Y",new Flyweight(3,4));
        _flyweights.Add("Z",new Flyweight(5,6));
    }

    public Flyweight GetFlyweight(string key)
    {
        return _flyweights[key];
    }
}
// 包含享元的上下文类
public class Context
{
    // 外在状态
    public int Unique;
    // 享元队先后
    private Flyweight _shared;

    public Context(int unique,Flyweight shared)
    {
        Unique = unique;
        _shared = shared;
    }

    public void Operation()
    {
        Console.WriteLine($"外在状态:{Unique} 共享状态:{_shared.Shared1} {_shared.Shared2}");
    }
}

客户端调用如下

FlyweightFactory factory = new FlyweightFactory();  
Context context1 = new Context(1, factory.GetFlyweight("X"));  
Context context2 = new Context(2, factory.GetFlyweight("Y"));  
Context context3 = new Context(3, factory.GetFlyweight("Z"));  
  
context1.Operation();  
context2.Operation();  
context3.Operation();  
  
// 输出结果:  
// 外在状态:1 共享状态:1 2  
// 外在状态:2 共享状态:3 4  
// 外在状态:3 共享状态:5 6

享元模式可以避免大量非常相似类的开销。在程序设计中,有时需要生成大量细粒度的类实例来表示数据。如果能发现这些实例除了几个参数外基本上都是相同的,有时就能够受大幅度地减少需要实例化的类的数量。如果能把那些参数移到类实例的外面,在方法调用时将它们传递进来,就可以通过共享大幅度地减少单个实例的数目。

使用享元模式需要维护一个记录了系统已有的所有享元的列表,而这本身需要耗费资源,另外享元模式使得系统更加复杂。为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化。因此,应当在有足够多的对象实例可供共享时才值得使用享元模式。

3.7 代理模式

代理模式(Proxy),为其他对象提供一种代理以控制对这个对象的访问。

代理模式的结构图如下。代理类通过持有实体类对象,调用真正的服务方法,并对外暴露一个代理的服务方法。在代理方法中,可以进行一些额外的操作。

代码示例

public interface ISubject
{
    public void Request();
}
// 真实实体
public class RealSubject : ISubject
{
    public void Request()
    {
        Console.WriteLine("真实的请求");
    }
}
// 代理
public class Proxy : ISubject
{
    private RealSubject? _realSubject;
    
    public void Request()
    {
        if (_realSubject == null)
        {
            _realSubject = new RealSubject();
        }
        _realSubject.Request();
    }
}

客户端调用

Proxy proxy = new Proxy();  
proxy.Request();  
// 输出结果:  
// 真实的请求

应用场景:

  • 远程代理,也就是为一个对象在不同的地址空间提供局部代表。这样可以隐藏一个对象存在于不同地址空间的事实。
  • 虚拟代理,是根据需要创建开销很大的对象。通过它来存放实例化需要很长时间的真实对象。
  • 安全代理,用来控制真实对象访问时的权限。
  • 智能指引,是指当调用真实的对象时,代理处理另外一些事。

代理模式其实就是在访问对象时引入一定程度的间接性,因为这种间接性,可以附加多种用途。

四、行为型模式

4.1 职责链模式

职责链模式(Chain of Responsibility):使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

职责链模式结构图如下。处理者可以通过SetSuccessor()设置下一位处理者,在HandleRequest()时决定是自己处理或交给下一个处理者进行处理。

示例代码如下

// 处理者接口
public abstract class Handler
{
    // 继任者
    protected Handler Successor;

    public void SetSuccessor(Handler successor)
    {
        Successor = successor;
    }
    // 处理请求
    public abstract void HandleRequest(int request);
}
// 具体处理者
public class ConcreteHandler1 : Handler
{
    public override void HandleRequest(int request)
    {
        if (request < 10)
        {
            Console.WriteLine("ConcreteHandler1处理请求:"+request);
        }
        else
        {
            // 交给下一位处理
            Successor.HandleRequest(request);
        }
    }
}
public class ConcreteHandler2 : Handler
{
    public override void HandleRequest(int request)
    {
        if (request >= 10)
        {
            Console.WriteLine("ConcreteHandler2处理请求:"+request);
        }
        else
        {
            // 交给下一位处理
            Successor.HandleRequest(request);
        }
    }
}

客户端调用如下

Handler handler1 = new ConcreteHandler1();  
Handler handler2 = new ConcreteHandler2();  
  
handler1.SetSuccessor(handler2);  
  
handler1.HandleRequest(1);// handler1处理  
handler1.HandleRequest(11);// handler2处理  
  
// 输出结果:  
// ConcreteHandler1处理请求:1  
// ConcreteHandler2处理请求:11

对于职责链模式来说,当客户提交一个请求时,请求是沿链传递直至有一个ConcreteHandler对象负责处理它。这就使得接收者和发送者都没有对方的明确信息,且链中的对象自己也并不知道链的结构。结果是职责链可简化对象的相互连接,它们仅需保持一个指向其后继者的引用,而不需保持它所有的候选接受者的引用,这也就大大降低了耦合度。

另外,由于是在客户端来定义链的结构,所以可以随时地增加或修改处理一个请求的结构。增强了给对象指派职责的灵活性。但这也可能导致一个请求极有可能到了链的末端都得不到处理,或者因为没有正确配置而得不到处理。

4.2 命令模式

命令模式(Command),可将请求转换为一个包含与请求相关的所有信息的独立对象。 该转换让你能根据不同的请求将方法参数化、 延迟请求执行或将其放入队列中, 且能实现可撤销操作。

命令模式的结构图如下。Invoker是命令的触发者,它无需直接向Receiver发送请求,而是通过触发一个命令来执行这个请求。Command类中包含了对Receiver的引用,当命令被触发时,就去请求Receiver

示例代码如下

// 命令执行所需的类
public class Receiver
{
    public void Action()
    {
        Console.WriteLine("执行命令");
    }
}
// 命令抽象类
public abstract class Command
{
    protected Receiver Receiver;

    public Command(Receiver receiver)
    {
        Receiver = receiver;
    }
    // 执行命令
    public abstract void Execute();
}
// 具体命令
class ConcreteCommand : Command
{
    public ConcreteCommand(Receiver receiver) : base(receiver)
    { }
 
    public override void Execute()
    {
        Receiver.Action();
    }
}
// 命令发起者
class Invoker
{
    private Command _command;
 
    public void SetCommand(Command command)
    {
        _command = command;
    }
 
    public void ExecuteCommand()
    {
        _command.Execute();
    }
}

客户端调用如下

Command command = new ConcreteCommand(new Receiver());
Invoker invoker = new Invoker();

invoker.SetCommand(command);
invoker.ExecuteCommand();

命令模式的优点如下:

  • 它能较容易地设计一个命令队列。
  • 在需要的情况下,可以较容易地将命令记入日志。
  • 允许接收请求的一方决定是否要否决请求。
  • 可以容易地实现对请求的撤销和重做。
  • 由于加进新的具体命令类不影响其他的类,因此增加新的具体命令类很容易。
  • 最重要的是命令模式把请求一个操作的对象与知道怎么执行一个操作的对象分割开。

4.3 迭代器模式

迭代器模式(Iterator),提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。

迭代器模式结构图如下。迭代器类集成了遍历集合所需的所有操作。集合对象可以通过CreateIterator()方法创建一个自己的迭代器,并将自己作为参数传入。创建出的迭代器对象持有了集合对象的引用,从而可以进行对这个集合对象的遍历操作。

示例代码如下

// 迭代器抽象类
public abstract class Iterator
{
    // 得到起始元素
    public abstract object? First();
    // 得到下一元素
    public abstract object? Next();
    // 判断是否到达结尾
    public abstract bool IsDone();
    // 返回当前元素
    public abstract object? CurrentItem();
}
// 聚集抽象类
public abstract class Aggregate
{
    // 创建迭代器
    public abstract Iterator CreateIterator();
}
// 具体迭代器类
public class ConcreteIterator : Iterator
{
    private readonly ConcreteAggregate _aggregate;
    private int _current = 0;

    public ConcreteIterator(ConcreteAggregate aggregate)
    {
        _aggregate = aggregate;
    }
    
    public override object? First()
    {
        if (_aggregate.Count == 0) return null;
        return _aggregate[0];
    }

    public override object? Next()
    {
        object? res = null;
        _current++;
        if (_current < _aggregate.Count)
        {
            res = _aggregate[_current];
        }
        return res;
    }

    public override bool IsDone()
    {
        return _current >= _aggregate.Count;
    }

    public override object? CurrentItem()
    {
        if (_aggregate.Count == 0) return null;
        return _aggregate[_current];
    }
}
// 具体聚集类
public class ConcreteAggregate : Aggregate
{
    private readonly IList<object> _items = new List<object>();

    public int Count => _items.Count;

    public object this[int index]
    {
        get => _items[index];
        set => _items.Insert(index, value);
    }
    public override Iterator CreateIterator()
    {
        return new ConcreteIterator(this);
    }
}

客户端调用如下

// 聚集对象
ConcreteAggregate aggregate = new ConcreteAggregate();
aggregate[0] = "A";
aggregate[1] = "B";
aggregate[2] = "C";
aggregate[3] = "D";

Iterator iterator = new ConcreteIterator(aggregate);

while (!iterator.IsDone())
{
	Console.WriteLine(iterator.CurrentItem());
	iterator.Next();
}
// 输出结果:
// ABCD

迭代器模式就是分离了集合对象的遍历行为,抽象出一个迭代器类来负责,这样既可以做到不暴露集合的内部结构,又可让外部代码透明地访问集合内部的数据。

当需要访问一个聚集对象,而且不管这些对象是什么都需要遍历的时候,或是需要对聚集有多种方式遍历时,可以考虑用迭代器模式。

4.4 中介者模式

中介者模式(Mediator),用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。

中介者模式结构图如下。Mediator作为中介者,需要事先“认识”两个组件对象。当两个组件需要进行通讯时,就通过中介者进行消息的传达。

示例代码如下

// 抽象中介者类
public abstract class Mediator
{
    public abstract void Send(string message, Component component);
}
// 抽象组件
public abstract class Component
{
    protected Mediator Mediator;

    public Component(Mediator mediator)
    {
        Mediator = mediator;
    }
    // 通过中介者发送消息
    public abstract void Send(string message);
    // 通过中介者接收消息
    public abstract void Notify(string message);
}
// 具体中介者类
public class ConcreteMediator : Mediator
{
    public ConcreteComponent1 Component1 { get; set; }
    public ConcreteComponent2 Component2 { get; set; }
    public override void Send(string message, Component component)
    {
        if(component != Component1) Component1.Notify(message);
        else Component2.Notify(message);
    }
}
// 具体组件
public class ConcreteComponent1 : Component
{
    public ConcreteComponent1(Mediator mediator) : base(mediator)
    {
    }

    public override void Send(string message)
    {
        Mediator.Send(message,this);
    }

    public override void Notify(string message)
    {
        Console.WriteLine("组件1接收到消息:"+message);
    }
}
public class ConcreteComponent2 : Component
{
    public ConcreteComponent2(Mediator mediator) : base(mediator)
    {
    }

    public override void Send(string message)
    {
        Mediator.Send(message,this);
    }

    public override void Notify(string message)
    {
        Console.WriteLine("组件2接收到消息:"+message);
    }
}

客户端调用

ConcreteMediator mediator = new ConcreteMediator();  
  
// 让组件认识中介者  
ConcreteComponent1 component1 = new ConcreteComponent1(mediator);  
ConcreteComponent2 component2 = new ConcreteComponent2(mediator);  
  
// 让中介者认识组件  
mediator.Component1 = component1;  
mediator.Component2 = component2;  
  
component1.Send("吃了吗您内");// 组件2接收到消息:吃了吗您内  
component2.Send("吃了");// 组件1接收到消息:吃了

中介者模式的优点首先是Mediator的出现减少了各个Component的耦合,使得可以独立地改变和复用各个Component类和Mediator。其次由于把对象如何协作进行了抽象,将中介作为一个独立的概念并将其封装在一个对象中,这样关注的对象就从对象各自本身的行为转移到它们之间的交互上来,也就是站在一个更宏观的角度去看待系统。

但它也有缺点。由于ConcreteMediator控制了集中化,于是就把交互复杂性变为了中介者的复杂性,这就使得中介者会变得比任何一个ConcreteComponent都复杂。

4.5 备忘录模式

备忘录(Memento):在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。

备忘录模式结构图如下。这个模式比较容易理解,就是通过一个外部类保存指定类的某些状态,然后交给一个统一的类进行管理。这个管理类可以帮助原发器进行状态回溯。

示例代码如下

// 备忘录的创建者
public class Originator
{
    // 需要保存的属性
    public string State { get; set; }

    // 创建备忘录
    public Memento CreateMemento()
    {
        return new Memento(State);
    }
    // 恢复备忘录
    public void SetMemento(Memento memento)
    {
        State = memento.State;
    }

    public void Show()
    {
        Console.WriteLine("State:"+State);
    }
}

// 备忘录
public class Memento
{
    public string State { get; }

    public Memento(string state)
    {
        State = state;
    }
}
// 备忘录管理者
public class CareTaker
{
    public Memento Memento { get; set; }
}

客户端调用如下

Originator originator = new Originator();  
originator.State = "状态A";  
originator.Show();  
  
// 保存状态  
CareTaker careTaker = new CareTaker();  
careTaker.Memento = originator.CreateMemento();  
  
originator.State = "状态B";  
originator.Show();  
  
// 恢复状态  
originator.SetMemento(careTaker.Memento);  
originator.Show();  
// 输出结果:  
// State:状态A  
// State:状态B  
// State:状态A

Memento模式比较适用于功能比较复杂的,但需要维护或记录属性历史的类,或者需要保存的属性只是众多属性中的一小部分时,Originator可以根据保存的Memento信息还原到前一状态。但如果状态数据很大很多,那么备忘录对象会非常耗内存。

4.6 观察者模式

观察者模式(Observer)定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。

观察者模式的结构图如下。这个模式也很好理解,通知者就像一个群聊,负责将消息通知到群聊里的每一个观察者。

示例代码如下

// 抽象观察者
public abstract class Observer
{
    // 更新
    public abstract void Update();
}
// 抽象通知者
public abstract class Subject
{
    private readonly List<Observer> _observers = new();

    // 添加观察者
    public void Add(Observer observer)
    {
        _observers.Add(observer);
    }
    // 删除观察者
    public void Delete(Observer observer)
    {
        _observers.Remove(observer);
    }
    // 通知观察者
    public void Notify()
    {
        foreach (var observer in _observers)
        {
            observer.Update();
        }
    }
}
// 具体观察者
public class ConcreteObserver:Observer
{
    private string _name;
    private string? _observeState;
    private ConcreteSubject _subject;

    public ConcreteObserver(string name,ConcreteSubject subject)
    {
        _name = name;
        _subject = subject;
    }
    
    public override void Update()
    {
        _observeState = _subject.SubjectState;
        Console.WriteLine($"观察者 {_name} 的新状态为 {_observeState}");
    }
}
// 具体通知者
public class ConcreteSubject : Subject
{
    public string? SubjectState { get; set; }
}

客户端调用如下

ConcreteSubject subject = new ConcreteSubject();  
  
subject.Add(new ConcreteObserver("A",subject));  
subject.Add(new ConcreteObserver("B",subject));  
subject.Add(new ConcreteObserver("C",subject));  
  
subject.SubjectState = "ABC";  
subject.Notify();  
  
// 输出结果:  
// 观察者 A 的新状态为 ABC
// 观察者 B 的新状态为 ABC
// 观察者 C 的新状态为 ABC

当一个对象的改变需要同时改变其他对象,而且它不知道具体有多少对象有待改变时,应该考虑使用观察者模式。当一个抽象模型有两个方面,其中一方面依赖于另一方面,这时用观察者模式可以将这两者封装在独立的对象中使它们各自独立地改变和复用。

4.7 状态模式

状态模式(State),当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。

状态模式主要解决的是当控制一个对象状态转换的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类当中,可以把复杂的判断逻辑简化。

状态模式的结构图如下。每个状态中的Handle()方法都包含让Context转换到下一个状态的逻辑。Context则主要负责当前状态逻辑的执行。

示例代码如下

// 抽象状态类
public abstract class State
{
    public abstract void Handle(Context context);
}
// 状态管理类
public class Context
{
    public State CurrentState { get; set; }
    // 初始化时指定初始状态
    public Context(State state)
    {
        CurrentState = state;
    }
    // 处理当前状态逻辑
    public void Request()
    {
        CurrentState.Handle(this);
    }
}
// 状态A
public class StateA : State
{
    public override void Handle(Context context)
    {
        Console.WriteLine("当前状态:StateA");
        // 下一个状态为状态B
        context.CurrentState = new StateB();
    }
}
// 状态B
public class StateB : State
{
    public override void Handle(Context context)
    {
        Console.WriteLine("当前状态:StateB");
        // 下一个状态为状态A
        context.CurrentState = new StateA();
    }
}

客户端调用如下

Context context = new Context(new StateA());  
context.Request();  
context.Request();  
context.Request();  
context.Request();  
  
// 输出结果:  
// 当前状态:StateA  
// 当前状态:StateB  
// 当前状态:StateA  
// 当前状态:StateB

状态模式的好处是将特定的状态相关的行为都放入一个对象中,由于所有与状态相关的代码都存在于某个State中,所以通过定义新的子类可以很容易地增加新的状态和转换。当一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为时,就可以考虑使用状态模式。

4.8 策略模式

策略模式(Strategy):它定义了算法家族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化,不会影响到使用算法的客户。

假设你要开发一款导航应用。使用导航的用户可能是开车,也可能是骑行,也可能是步行。如果将不同出行方式的导航算法集中在一个类中,这个类将会越来越臃肿。一种明智的解决方式是将不同的出行方式的算法分别封装到一个类中。然后通过上下文视情况调用。这就是策略模式的核心思想。

策略模式的结构图如下。

示例代码

public class Context
{
    private readonly IOperation _operation;

    public Context(IOperation operation)
    {
        _operation = operation;
    }

    public void Operate()
    {
        _operation.Operate();
    }
}

public interface IOperation
{
    public void Operate();
}

public class OperationA:IOperation
{
    public void Operate()
    {
        Console.WriteLine("OperationA");
    }
}
public class OperationB:IOperation
{
    public void Operate()
    {
        Console.WriteLine("OperationB");
    }
}
public class OperationC:IOperation
{
    public void Operate()
    {
        Console.WriteLine("OperationC");
    }
}

对Context类进行改造,结合简单工厂模式。此时客户端无需依赖IOperate接口。

public class Context
{
    private readonly IOperation _operation;

    public Context(string type)
    {
        switch (type)
        {
            case "A":
                _operation = new OperationA();
                break;
            case "B":
                _operation = new OperationB();
                break;
            case "C":
                _operation = new OperationC();
                break;
        }
    }
    public void Operate()
    {
        _operation.Operate();
    }
}

策略模式的优点:

  • 策略模式是一种定义一系列算法的方法,从概念上来看,所有这些算法完成的都是相同的工作,只是实现不同,它可以以相同的方式调用所有的算法,减少了各种算法类与使用算法类之间的耦合。
  • 策略模式的Strategy类层次为Context定义了一系列的可供重用的算法或行为。继承有助于析取出这些算法中的公共功能。
  • 简化了单元测试,因为每个算法都有自己的类,可以通过自己的接口单独测试。

4.9 模板方法模式

模板方法模式(Template Method),定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

模板方法模式的结构图如下。实现类通过实现不同的模板方法,使最终组装出的“产品”具有不同的“零件”。

代码示例如下

public abstract class AbstractClass
{
    public abstract void Method1();
    public abstract void Method2();

    // 模板方法
    public void TemplateMethod()
    {
        Method1();
        Method2();
        Console.WriteLine("模板方法");
    }
}

public class ConcreteClassA : AbstractClass
{
    public override void Method1()
    {
        Console.WriteLine("ConcreteClassA Method1");
    }

    public override void Method2()
    {
        Console.WriteLine("ConcreteClassA Method2");
    }
}
public class ConcreteClassB : AbstractClass
{
    public override void Method1()
    {
        Console.WriteLine("ConcreteClassB Method1");
    }

    public override void Method2()
    {
        Console.WriteLine("ConcreteClassB Method2");
    }
}

客户端调用如下

AbstractClass a;  
a = new ConcreteClassA();  
a.TemplateMethod();  
  
a = new ConcreteClassB();  
a.TemplateMethod();  
  
// 输出结果:  
// ConcreteClassA Method1  
// ConcreteClassA Method2  
// 模板方法  
// ConcreteClassB Method1  
// ConcreteClassB Method2  
// 模板方法

模板方法模式是通过把不变行为搬移到超类,去除子类中的重复代码来体现它的优势。当不变的和可变的行为在方法的子类实现中混合在一起的时候,不变的行为就会在子类中重复出现。我们通过模板方法模式把这些行为搬移到单一的地方,这样就帮助子类摆脱重复的不变行为的纠缠。

4.10 解释器模式

解释器模式(Interpreter),给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。

如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。比如正则表达式就是一个很好的例子,它可以将原本复杂但常用的算法通过一串特定的字符串表示。

解释器模式结构图如下。对于一个表达式R=R1+R2,R1和R2就是终结符,+就是非终结符。

示例代码如下

// 抽象表达式
abstract class AbstractExpression
{
    // 解释
    public abstract int Interpret(Context context);
}
// 上下文类
class Context
{
    private Dictionary<string, int> _map = new();

    public Context()
    {
        _map.Add("1",1);
        _map.Add("2",2);
        _map.Add("3",3);
    }

    public int Interpret(string key)
    {
        return _map[key];
    }
}
// 终结符表达式
class TerminalExpression : AbstractExpression
{
    private string _key;
    public TerminalExpression(string key)
    {
        _key = key;
    }
    public override int Interpret(Context context)
    {
        return context.Interpret(_key);
    }
}
// 加法非终结符表达式
class PlusNonTerminalExpression : AbstractExpression
{
    private AbstractExpression _exp1;
    private AbstractExpression _exp2;

    public PlusNonTerminalExpression(AbstractExpression exp1, AbstractExpression exp2)
    {
        _exp1 = exp1;
        _exp2 = exp2;
    }
    public override int Interpret(Context context)
    {
        return _exp1.Interpret(context) + _exp2.Interpret(context);
    }
}

客户端调用如下

Context context = new Context();  
var exp1 = new TerminalExpression("1");  
var exp2 = new TerminalExpression("2");  
var res = new PlusNonTerminalExpression(exp1, exp2);  
  
Console.WriteLine("1+2="+res.Interpret(context));  
// 输出结果:  
// 1+2=3

当有一个语言需要解释执行,并且你可将该语言中的句子表示为一个抽象语法树时,可使用解释器模式。使用解释器模式可以很容易地改变和扩展文法,因为该模式使用类来表示文法规则,你可使用继承来改变或扩展该文法。也比较容易实现文法,因为定义抽象语法树中各个节点的类的实现大体类似,这些类都易于直接编写。

解释器模式的缺点是为文法中的每一条规则至少定义了一个类,因此包含许多规则的文法可能难以管理和维护。建议当文法非常复杂时,使用其他的技术如语法分析程序或编译器生成器来处理。

4.11 访问者模式

访问者模式(Visitor),表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

访问者模式结构图如下。访问者模式提供了在不同视角下对元素进行不同操作的方法。比如在项目经理的视角下,可能更关心员工的工作效率;而在HR的视角下,可能更关心员工的薪资。这里的项目经理和HR就是访问者,而员工就是具体的元素。如果员工类将不同视角访问自己的操作集成在内部,那么每当多一种视角,就需要修改员工类。而如果将操作定义在访问者类中,就能避免修改员工类,只需要新增访问者类。

示例代码如下

// 访问者抽象类
public abstract class Visitor
{
    public abstract void VisitConcreteElementA(ConcreteElementA concreteElementA);
 
    public abstract void VisitConcreteElementB(ConcreteElementB concreteElementB);
}
// 被访问的元素抽象类
public abstract class Element
{
    public abstract void Accept(Visitor visitor);
}
// 具体被访问的元素
public class ConcreteElementA:Element
{
    public override void Accept(Visitor visitor)
    {
        // 双分派技术实现处理与数据结构分离
        visitor.VisitConcreteElementA(this);
    }
    // 其他专有方法
    public void OperationA(){}
}
public class ConcreteElementB:Element
{
    public override void Accept(Visitor visitor)
    {
        // 双分派技术实现处理与数据结构分离
        visitor.VisitConcreteElementB(this);
    }
    // 其他专有方法
    public void OperationB(){}
}
// 具体访问者类
public class ConcreteVisitor1 : Visitor
{
    public override void VisitConcreteElementA(ConcreteElementA concreteElementA)
    {
        Console.WriteLine($"{concreteElementA.GetType().Name} 被 ConcreteVisitor1 访问");
    }

    public override void VisitConcreteElementB(ConcreteElementB concreteElementB)
    {
        Console.WriteLine($"{concreteElementB.GetType().Name} 被 ConcreteVisitor1 访问");
    }
}

public class ConcreteVisitor2 : Visitor
{
    public override void VisitConcreteElementA(ConcreteElementA concreteElementA)
    {
        Console.WriteLine($"{concreteElementA.GetType().Name} 被 ConcreteVisitor2 访问");
    }

    public override void VisitConcreteElementB(ConcreteElementB concreteElementB)
    {
        Console.WriteLine($"{concreteElementB.GetType().Name} 被 ConcreteVisitor2 访问");
    }
}
// 枚举被访问元素的类
class ObjectStructure
{
    private readonly IList<Element> _elements = new List<Element>();
 
    public void Attach(Element element)
    {
        _elements.Add(element);
    }
    public void Detach(Element element)
    {
        _elements.Remove(element);
    }
    public void Accept(Visitor visitor)
    {
        foreach (Element e in _elements)
        {
            e.Accept(visitor);
        }
    }
}

客户端调用如下

ObjectStructure obj = new ObjectStructure();  
obj.Attach(new ConcreteElementA());  
obj.Attach(new ConcreteElementB());  
  
ConcreteVisitor1 v1 = new ConcreteVisitor1();  
ConcreteVisitor2 v2 = new ConcreteVisitor2();  
  
obj.Accept(v1);  
obj.Accept(v2);  
  
// 输出结果:  
// ConcreteElementA 被 ConcreteVisitor1 访问  
// ConcreteElementB 被 ConcreteVisitor1 访问  
// ConcreteElementA 被 ConcreteVisitor2 访问  
// ConcreteElementB 被 ConcreteVisitor2 访问

访问者模式的目的是要把处理从数据结构分离出来。很多系统可以按照算法和数据结构分开,如果这样的系统有比较稳定的数据结构,又有易于变化的算法的话,使用访问者模式就是比较合适的,因为访问者模式使得算法操作的增加变得容易。增加新的操作只需要增加一个新的访问者。

五、参考资料

[1].《大话设计模式》
[2]. https://refactoringguru.cn/design-patterns

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/166071.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

语义分割——FCN模型pytorch实现

FCN网络简介 全卷积网络&#xff08;Fully Convolutional Networks&#xff0c;FCN&#xff09;是Jonathan Long等人于2015年在Fully Convolutional Networks for Semantic Segmentation一文中提出的用于图像语义分割的一种框架&#xff0c;是首个端对端的针对像素级预测的全卷…

BIGEMAP APP离线卫星地图数据应用

离线包&#xff08;高清卫星图&#xff09;&#xff1a; 1、一次下载永久可用&#xff0c;访问更快&#xff0c;下载更快 2、离线包数据可自动更新&#xff0c;无需重新下载&#xff0c;更新3到6个月一次 3、离线包可在无网络离线环境下使用&#xff0c;不受网络限制 4、离线包…

Springboot打成JAR包后读取配置文件

Springboot的默认配置文件为&#xff1a;application.properties或者是application.yml 如果这两个配置文件都存在&#xff0c;不冲突的话&#xff0c;就互相补充。冲突的话&#xff0c;则properties优先级高。 当我们使用IDEA创建出一个Springboot项目上时&#xff0c;配置文…

Git从入门到精通

目录 Git 配置 1. 在安装完成 Git 后&#xff0c;开始正式使用前&#xff0c;是需要有一些全局设置的&#xff0c;如用户名、邮箱。 2. 除了用户名、邮箱之外&#xff0c;还有很多的配置可以用来自定义 Git&#xff0c;如&#xff1a; 3. 查看所有的已经做出的配置&#xff…

Python3 常用内置函数解析(共28个函数)

文章目录一&#xff1a;Python3 操作符&#xff08;大全&#xff09;二&#xff1a;函数带括号与不带括号的区别三&#xff1a;不可变数据类型与可变数据类型四&#xff1a;Python3 内置函数&#xff08;大全&#xff09;1、input()&#xff1a;用于获取控制台的输入。2、print…

Django REST framework--渲染器

Django REST framework--渲染器Django REST framework--渲染器自定义接口规范渲染器基本原理Django 项目debug调试技巧异常信息处理配置异常处理模块自定义异常处理Django REST framework–渲染器 自定义接口规范 目前使用的是REST框架默认的返回格式&#xff0c;类似这种 […

【redis6】第十章(事务和锁机制)

Redis的事务定义 Redis事务是一个单独的隔离操作&#xff1a;事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中&#xff0c;不会被其他客户端发送来的命令请求所打断。 Redis事务的主要作用就是串联多个命令防止别的命令插队。 Multi、Exec、discard 从输入Mu…

2023年面试题之Dubbo基础架构

一. Dubbo 的整体架构设计有哪些分层?接口服务层&#xff08;Service&#xff09;&#xff1a;该层与业务逻辑相关&#xff0c;根据 provider 和 consumer 的业务设计对应的接口和实现配置层&#xff08;Config&#xff09;&#xff1a;对外配置接口&#xff0c;以 ServiceCon…

Docker 应用实践-镜像篇

一个 Docker 镜像往往是由多个镜像层&#xff08;可读层&#xff09;叠加而成&#xff0c;每个层仅包含了前一层的差异部分&#xff0c;单个镜像层也往往可以看作镜像使用&#xff0c;当我们启动一个容器的时候&#xff0c;Docker 会加载镜像层并在其上添加一个可写层。容器上所…

C语言学习——字符函数和字符串函数

&#x1f307;个人主页&#xff1a;平凡的小苏 &#x1f4da;学习格言&#xff1a;别人可以拷贝我的模式&#xff0c;但不能拷贝我不断往前的激情 &#x1f6f8;C语言专栏&#xff1a;https://blog.csdn.net/vhhhbb/category_12174730.html 小苏希望大家能从这篇文章中收获到许…

实战Kaggle比赛:预测房价

实战Kaggle比赛&#xff1a;预测房价 目录 下载和缓存数据集访问和读取数据集数据预处理训练K折交叉验证模型选择提交Kaggle预测 本节我们将通过Kaggle比赛&#xff0c;将所学知识付诸实践。 Kaggle的房价预测比赛是一个很好的起点。 此数据集由Bart de Cock于2011年收集 (D…

Linux--线程互斥与同步--0112 13

线程互斥 1.背景概念 临界资源&#xff1a;多线程执行流共享的资源就叫做临界资源。 临界区&#xff1a;每个线程内部&#xff0c;访问临界资源的代码就叫做临界区。 互斥&#xff1a;任何时刻&#xff0c;互斥保证有且只有一个执行流进入临界区 &#xff0c;对临界资源起保…

36岁北邮硕士,四段大厂经历,当初为了涨薪频繁跳槽,被裁后投遍所有公司,基本都不回复!...

今天给大家分享一位36岁北邮硕士的职场经历&#xff1a;2013年北邮硕士毕业后&#xff0c;分别在乐视、字节、阿里、小米待过&#xff0c;2022年被小米裁员&#xff0c;几乎投遍了boss上所有公司&#xff0c;基本都是已读不回。只有一个小公司的hr看了简历后回了一句“加油”。…

绕过某博客查看文章验证码,关注公众号得验证码

之前也写过一篇&#xff0c;当时使用Burpsuite抓包&#xff0c;改包&#xff0c;有点杀鸡用牛刀了。 虽然我挺支持为知识那啥的&#xff0c;但是吧要我去关注公众号太麻烦了 绕过查看文章需要验证码 其实就是改一个返回的字段&#xff0c;既然后端也是改&#xff0c;那我前端…

Google Earth Engine基础使用方法(一)

Google Earth Engine 1、注册账号1.1、设置谷歌账号辅助邮箱1.2、进入Google Earth Engine(如果第一次注册失败怎么办)1.3、进入Google Earth Engine Editor2、Editor主界面2.1、上传自己的矢量数据2.2、分享代码给别人2.3、保存代码2.4、几个有效快捷键2.5、搜索框有什么用3、…

SAP S/4 FAGLGVTR错误解决

本次年结支持过程中&#xff0c;一个客户的年结操作出现问题&#xff0c;问题的解决还颇费周折&#xff0c;稍稍记录一下。客户的SAP 版本是 S/4, 通过 FAA_CMP 事务码切换固定资产年度的时候&#xff0c;提示上一已关闭的会计年度与当前会计年度相同。 这个消息的意思是FI 的会…

【自学Python】Python字符串出现次数

Python字符串出现次数 Python字符串出现次数教程 在开发过程中&#xff0c;很多时候我们有统计单个字符或者 字符串 在另一个字符串中出现次数的需求&#xff0c;在 Python 中&#xff0c;统计字符串出现次数我们使用 count() 函数。 Python count()函数详解 语法 S.count…

Python实战项目1——自动获取小说工具

&#x1f935;‍♂️ 个人主页老虎也淘气 个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f44d;&#x1f3fb; 收藏…

【C++】二叉树进阶OJ题

​&#x1f320; 作者&#xff1a;阿亮joy. &#x1f386;专栏&#xff1a;《吃透西嘎嘎》 &#x1f387; 座右铭&#xff1a;每个优秀的人都有一段沉默的时光&#xff0c;那段时光是付出了很多努力却得不到结果的日子&#xff0c;我们把它叫做扎根 目录&#x1f449;根据二叉…

前端开发:Webpack的使用总结

前言 在前端开发过程中&#xff0c;尤其是现在前端框架的频繁使用的当下&#xff0c;作为前端开发者想必对于Webpack并不陌生&#xff0c;尤其是在使用Vue框架做前端开发的时候&#xff0c;打包时候必用Webpack。还有就是在前端求职面试的时候&#xff0c;Webpack相关的知识点…