面向可复用性和可维护性的设计模式-软件构造学习总结08


前言

这篇文章是我对软件构造课程的面向可复用性和可维护性的设计模式章节的学习总结,以供未来使用。文章中的图片均来自课程教师的讲义。

主要内容包括:

  • 设计模式总述
  • 创建型模式
  • 结构型模式
  • 行为型模式
  • 各种模式之间的共性和联系

一、设计模式总述

设计模式(Design Patterns):一种通用的、可重用的解决方案,用于解决软件设计中给定情景中常见的问题。
一种设计应该具有可复用性、可维护性和可拓展性。接下来介绍的设计模式可以保证在软件构造过程中保持这些良好的特性。
在面向对象的设计中,除了类本身,设计模式更强调多个类/对象之间的关系和交互过程——比接口/类复用的粒度更大

设计模式可以分为三种:

  • 创建型模式(Creational patterns):关注对象创建过程

  • 结构型模式(Structural patterns):处理类或对象的组合

  • 行为型模式(Behavioral patterns):描述类或对象交互和分配责任的方式


二、创建型模式

工厂方法模式

工厂方法(Factory Method)也被称为虚拟构造器(Virtual Constructor),用于创建对象而无需暴露创建逻辑的细节。也就是说,client并不知道要创建哪个具体类的实例,或者不想在client代码中体现具体创建的实例时,可以使用工厂方法。
在工厂方法模式中,我们定义一个接口或抽象类作为工厂,并在其中声明一个方法用于创建对象。具体的对象创建逻辑由具体的工厂类来实现,每个具体工厂类负责创建特定类型的对象。
下面给出代码示例:

// 抽象产品类
interface Product {
    void operation();
}

// 具体产品类A
class ConcreteProductA implements Product {
    @Override
    public void operation() {
        System.out.println("具体产品A的操作。");
    }
}

// 具体产品类B
class ConcreteProductB implements Product {
    @Override
    public void operation() {
        System.out.println("具体产品B的操作。");
    }
}

// 抽象工厂类
interface Factory {
    Product createProduct();
}

// 具体工厂类A
class ConcreteFactoryA implements Factory {
    @Override
    public Product createProduct() {
        return new ConcreteProductA();
    }
}

// 具体工厂类B
class ConcreteFactoryB implements Factory {
    @Override
    public Product createProduct() {
        return new ConcreteProductB();
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        Factory factoryA = new ConcreteFactoryA();
        Product productA = factoryA.createProduct();
        productA.operation();
        
        Factory factoryB = new ConcreteFactoryB();
        Product productB = factoryB.createProduct();
        productB.operation();
    }
}

在这段代码中,抽象工厂类Factory是一个接口,其中定义方法createProduct()用于生成具体产品类。
该接口由两个具体工厂类实现,内部重写createProduct()方法,通过调用具体产品类的构造方法实现。
这样client在创建具体产品类时,只需要创建具体工厂类,再调用createProduct()方法即可实现。
这时创建的实例,其类型与接口类型一致,就可以实现对具体实现类的隐藏。

工厂模式下,各类间的关系如图所示:
在这里插入图片描述


当然,工厂模式也可以通过静态工厂方法来实现。
在静态工厂方法中,我们不再需要创建工厂类的实例,而是直接在工厂类中定义静态方法来创建对象。这些静态方法通常被称为工厂方法,它们负责创建和返回所需的对象实例。
在这里插入图片描述


工厂模式符合OCP原则,可以轻松地添加新的产品类型,而无需修改现有的代码。通过添加新的具体工厂类和产品类,而不是修改现有的代码,可以实现对修改关闭、对扩展开放。但在这一过程中,工厂模式引入和额外的工厂类,这可能导致类的数量增多,系统变得更加复杂。


三、结构型模式

适配器模式

适配器模式(Adapter Pattern)是一种结构型设计模式,用于将一个类的接口转换成客户端所期望的另一个接口,从而使得原本不兼容的接口能够协同工作。

适配器模式有三大角色:

  • 目标接口Target): 客户端所期待的接口,客户端通过目标接口与适配器交互。
  • 适配器Adapter): 适配器是一个类,它实现了目标接口并包装了一个已有的类的对象。适配器通过调用已有类的方法来实现目标接口的方法,从而使得客户端可以通过目标接口与已有类交互。
  • 被适配者Adaptee): 被适配者是已经存在的类,其接口与目标接口不兼容。

如下图所示,存在目标接口,接口中有目标方法。该目标接口由适配器实现,而适配器对目标方法的实现则是通过委派给被适配者来完成的。此时适配器对被适配者进行包装,比如将被适配者的某个实例作为成员变量。
在这里插入图片描述


举个具体的例子。客户端期望目标接口Shape提供一个display方法,传入矩形的左下和右上两点来绘制该矩形。而当前已经存在一个类实现了该功能,但它的参数却是矩形的左下点、长和宽。
在这里插入图片描述
为了对已存在的类LegacyRectangle(被适配者)的复用,创建一个新的适配器类Rectangle,实现目标接口Shape。其内部对目标方法display的实现,则是先对参数进行预处理,然后再委派给被适配者LegacyRectangle完成该功能。
客户端使用该功能时,调用适配器的方法即可完成。


装饰器模式

装饰器模式(Decorator Pattern)是一种结构型设计模式,它允许你动态地给一个对象添加额外的功能,而不需要通过继承来实现。装饰器模式通过将对象包装在装饰器类中,然后用一个或多个装饰器类来包装原始对象,从而实现功能的逐层叠加。
它可以避免只使用继承造成的组合爆炸和大量代码重复,主要用于需要多个特性任意组合的情景。

装饰器模式包括以下几个核心角色:

  • 组件接口Component): 定义了被装饰的对象的接口。可以是一个抽象类或接口,也可以是具体类。
  • 具体组件Concrete Component): 实现了组件接口,并定义了被装饰的对象。
  • 装饰器Decorator): 继承了组件接口,并持有一个指向组件对象的引用。装饰器类通常是抽象类,用于定义装饰器的公共接口和属性。
  • 具体装饰器Concrete Decorator): 继承自装饰器类,实现了具体的装饰逻辑。

组件接口就是目标接口。具体组件就是最基本的“核心”组件,完成最基本的功能。装饰器继承了组件接口,并可以具有类型为目标接口的成员变量,其由多个具体装饰器实现。
首先,可以创建目标接口的一个实例——利用具体组件来完成。然后便可以像“穿衣服”那样将该组件交给具体装饰器,具体装饰器会根据该组件构造出新的对象,该对象的特性被具体装饰器的内部逻辑所修饰。通过一层层的多次“装饰”,最终就可以构造出具有多种特性组合的对象。
整体结构如下图所示。
在这里插入图片描述
显然,装饰器模式用到了继承,而由于装饰器包装了对象,其某些方法的实现交给被包装的对象来完成,所以也用到了委派


下面给出一个代码实现的例子

// 组件接口
interface Coffee {
    String getDescription();
    double getCost();
}

// 具体组件
class SimpleCoffee implements Coffee {
    @Override
    public String getDescription() {
        return "Simple coffee";
    }

    @Override
    public double getCost() {
        return 1.0;
    }
}

// 装饰器
abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee;

    public CoffeeDecorator(Coffee decoratedCoffee) {
        this.decoratedCoffee = decoratedCoffee;
    }

    public String getDescription() {
        return decoratedCoffee.getDescription();
    }

    public double getCost() {
        return decoratedCoffee.getCost();
    }
}

// 具体装饰器
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }

    public String getDescription() {
        return super.getDescription() + ", with milk";
    }

    public double getCost() {
        return super.getCost() + 0.5;
    }
}

class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }

    public String getDescription() {
        return super.getDescription() + ", with sugar";
    }

    public double getCost() {
        return super.getCost() + 0.2;
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        // 创建简单咖啡
        Coffee simpleCoffee = new SimpleCoffee();
        System.out.println("Cost: " + simpleCoffee.getCost() + "; Description: " + simpleCoffee.getDescription());
        
        // 加入牛奶的咖啡
        Coffee milkCoffee = new MilkDecorator(simpleCoffee);
        System.out.println("Cost: " + milkCoffee.getCost() + "; Description: " + milkCoffee.getDescription());
        
        // 加入糖的咖啡
        Coffee sugarCoffee = new SugarDecorator(simpleCoffee);
        System.out.println("Cost: " + sugarCoffee.getCost() + "; Description: " + sugarCoffee.getDescription());
        
        // 加入牛奶和糖的咖啡
        Coffee milkAndSugarCoffee = new SugarDecorator(new MilkDecorator(simpleCoffee));
        System.out.println("Cost: " + milkAndSugarCoffee.getCost() + "; Description: " + milkAndSugarCoffee.getDescription());
    }
}

在这段代码中,Coffee作为组件接口定义了基本的方法,并交给具体组件 SimpleCoffee实现。这是“核心”。
装饰器CoffeeDecorator继承了组件接口,并添加了类型为Coffee的成员变量,并对应的设计了构造方法。对于接口中定义的基本方法,均委派给该成员变量对象实现。
具体装饰器则继承了CoffeeDecorator,在修饰基本方法时,通过调用父类CoffeeDecorator的方法(实际上就是调用成员变量的方法),以及添加新的代码逻辑来实现。
客户端在使用时,想要使用多个特性,只需要先创建一个“核心”即SimpleCoffee的实例,然后再通过需要的装饰器对其装饰——逐层调用构造方法,即可完成。

由以上例子可以明显的体会到,装饰器模式的“装饰”过程是在运行时进行的,也就是说装饰为具有哪些特性组合的目标实例,是由客户端动态的完成的。区别于继承,继承所创建的各种子类均在编译时已经确定,客户端在运行时可以直接调用。


三、行为型模式

策略模式

策略模式(Strategy Pattern)是一种行为型设计模式,它定义了一系列算法,并将每个算法封装到独立的类中,使得它们可以互相替换。策略模式使得算法的变化可以独立于使用它们的客户端代码而变化。

策略模式包含以下几个核心角色:

  • 策略接口Strategy Interface): 定义了所有支持的算法的通用接口。通常是一个接口或抽象类,它声明了一个或多个方法来执行特定的算法。
  • 具体策略Concrete Strategies): 实现了策略接口,并提供了算法的具体实现。每个具体策略类实现了一种特定的算法。
  • 上下文(Context): 上下文是使用策略的类,它持有一个策略对象,并在运行时可以切换不同的策略。上下文将客户端请求委派给具体的策略对象来执行。

各个类之间的关系如图所示:
在这里插入图片描述
下面给出代码示例:

// 策略接口
interface Strategy {
    int doOperation(int num1, int num2);
}

// 具体策略类:加法
class AddStrategy implements Strategy {
    @Override
    public int doOperation(int num1, int num2) {
        return num1 + num2;
    }
}

// 具体策略类:减法
class SubtractStrategy implements Strategy {
    @Override
    public int doOperation(int num1, int num2) {
        return num1 - num2;
    }
}

// 上下文
class Context {
    private Strategy strategy;

    public Context(Strategy strategy) {
        this.strategy = strategy;
    }

    public int executeStrategy(int num1, int num2) {
        return strategy.doOperation(num1, num2);
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        Context context = new Context(new AddStrategy());
        System.out.println("10 + 5 = " + context.executeStrategy(10, 5));

        context = new Context(new SubtractStrategy());
        System.out.println("10 - 5 = " + context.executeStrategy(10, 5));
    }
}

在这段代码中,策略接口提供了方法doOperation(),其由两个具体策略类分别以加法和减法的方法实现。
上下文类Context则包装了一个类型为策略接口的对象,当它调用executeStrategy()方法时,会委派给具体策略类实现。
到底委派给哪一个由用户指定,这里采用的是构造方法,当然也可以设计为将策略接口作为参数传入。

模板模式

模板模式(Template Pattern)是一种行为型设计模式,它定义了一个算法的骨架,并允许子类重写其中的某些步骤,而不改变算法的结构。模板模式使得可以在不改变算法整体结构的情况下,通过重写特定步骤来实现算法的定制化。

模板模式包含以下几个核心角色:

  • 抽象模板Abstract Template): 定义了算法的骨架,包含了算法中的各个步骤,并将其中的一部分步骤标记为抽象方法,以便由子类来实现。
  • 具体模板Concrete Template): 继承自抽象模板,并实现了其中的抽象方法,以完成算法的具体实现。

各类之间的关系如图所示:
在这里插入图片描述
目标的抽象类由client直接使用,其中的模板方法确定了该方法的整体结构——分三步进行。这三步中的某些步骤可以由抽象类来完成,这是共性部分,而剩余的步骤则需要子类完成。在子类中,根据各自的需要对继承来的步骤方法进行重写即可。总之,模板模式主要利用了继承重写
在这一模式中,父类和子类的关系是透明的,所以模板模式是一种白盒框架


代码示例如下:

// 抽象模板
abstract class AbstractClass {
    // 模板方法,定义了算法的骨架
    public void templateMethod() {
        operation1();
        operation2();
        operation3();
    }

    // 抽象方法,子类需要实现
    protected abstract void operation1();

    // 具体方法,子类可以选择是否重写
    protected void operation2() {
        System.out.println("AbstractClass: Performing operation 2");
    }

    // 具体方法,子类可以选择是否重写
    protected void operation3() {
        System.out.println("AbstractClass: Performing operation 3");
    }
}

// 具体模板A
class ConcreteClassA extends AbstractClass {
    @Override
    protected void operation1() {
        System.out.println("ConcreteClassA: Performing operation 1");
    }

    @Override
    protected void operation3() {
        System.out.println("ConcreteClassA: Performing customized operation 3");
    }
}

// 具体模板B
class ConcreteClassB extends AbstractClass {
    @Override
    protected void operation1() {
        System.out.println("ConcreteClassB: Performing operation 1");
    }

    @Override
    protected void operation2() {
        System.out.println("ConcreteClassB: Performing customized operation 2");
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        AbstractClass templateA = new ConcreteClassA();
        templateA.templateMethod();

        System.out.println("--------------------");

        AbstractClass templateB = new ConcreteClassB();
        templateB.templateMethod();
    }
}

在这段代码中,抽象模板是一个抽象类,它定义了模板方法templateMethod(),并为其确定了框架——分三步执行。
且已经对第二步和第三步的方法编写完成,说明这两步是比较共性的部分,当然子类也可以个性化的对其重写。
而第一步定义为抽象方法,则必须由子类实现。
抽象模板被两个具体模板继承,在具体模板中实现了模板方法的某些具体步骤。

迭代器模式

迭代器模式(Iterator Pattern)是一种行为型设计模式,用于提供一种访问聚合对象(例如列表、集合、数组等)中各个元素的方法,而不需要暴露聚合对象的内部表示。迭代器模式将迭代的责任分离出来,使得迭代逻辑可以独立于聚合对象实现,并使得聚合对象的结构可以灵活地改变而不影响迭代器的使用。

迭代器模式包含以下几个核心角色:

  • 迭代器接口Iterator Interface): 定义了访问和遍历聚合对象元素的方法,通常包括 hasNext()、next() 等方法。
  • 具体迭代器Concrete Iterator): 实现了迭代器接口,负责遍历聚合对象并记录当前位置。
  • 聚合对象接口Aggregate Interface): 定义了创建迭代器对象的方法。
  • 具体聚合对象Concrete Aggregate): 实现了聚合对象接口,负责创建对应的具体迭代器对象,并可能实现一些其他的方法。

各类之间的关系如图所示
在这里插入图片描述
在迭代器模式中,迭代器接口定义了迭代器标准的三个方法。该接口由具体迭代器实现,它会根据目标ADT实现这三种方法,所以目标ADT的对象的某些字段会被包装在具体迭代器中,以进行遍历。聚合对象接口定义了创建迭代器对象的方法,而该接口则会由目标ADT实现,这表示目标ADT采用了迭代器模板,提供遍历机制。在目标ADT内部,创建迭代器对象的方法会委派给具体迭代器的构造方法来完成,在这个过程中ADT对象的某些字段会作为参数传给具体迭代器。


具体代码示例如下:

// 迭代器接口
interface Iterator<T> {
    boolean hasNext();
    T next();
}

// 聚合对象接口
interface Aggregate<T> {
    Iterator<T> createIterator();
}

// 具体迭代器
class ConcreteIterator<T> implements Iterator<T> {
    private T[] items;
    private int position = 0;

    public ConcreteIterator(T[] items) {
        this.items = items;
    }

    public boolean hasNext() {
        return position < items.length && items[position] != null;
    }

    public T next() {
        return items[position++];
    }
}

// 具体聚合对象
class ConcreteAggregate<T> implements Aggregate<T> {
    private T[] items;

    public ConcreteAggregate(T[] items) {
        this.items = items;
    }

    public Iterator<T> createIterator() {
        return new ConcreteIterator<>(items);
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        String[] names = {"Alice", "Bob", "Charlie", "David"};

        Aggregate<String> aggregate = new ConcreteAggregate<>(names);
        Iterator<String> iterator = aggregate.createIterator();

        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}


访问者模式

访问者模式(Visitor Pattern)是一种行为型设计模式,它允许你对一组对象的元素进行操作,而不暴露这些对象的内部结构。通过在对象结构中添加一个访问者接口,以及让每个元素都能够接受访问者的访问,访问者模式可以实现在不改变元素类的前提下,定义新的操作。

访问者模式包含以下几个核心角色:

  • 访问者接口Visitor Interface): 定义了访问者的接口,包括了对不同类型元素的访问方法。
  • 具体访问者Concrete Visitor): 实现了访问者接口中定义的方法,对元素进行具体的操作。
  • 元素接口Element Interface): 定义了元素的接口,包括了接受访问者访问的方法。
  • 具体元素Concrete Element): 实现了元素接口,具体的元素类,实现了接受访问者访问的方法。
  • 对象结构Object Structure): 维护了一个元素的集合,并提供了遍历集合的方法。

各类关系如图所示:
在这里插入图片描述
首先对象结构维护了具体元素的集合,具体元素类则实现了元素接口,主要实现accept()方法。该方法用于接受访问者(作为参数),用于对元素按照某种访问者方法进行访问。实际上这一方法会委派给具体访问者类实现。在具体访问者类内部,可以通过重载根据元素类型来对元素进行访问,这个访问方法可以将元素作为参数来接收。
这里的关系是双向的,也就是说由client创建某各访问者实例visitor,然后由元素实例调用accept(visitor)方法,该方法的执行则会委派给visitor完成,即visitor.visit(this),将自身作为参数传递给visitor来完成访问。


具体代码示例如下:

// 访问者接口
interface Visitor {
    void visit(ConcreteElementA element);
    void visit(ConcreteElementB element);
}

// 具体访问者A
class ConcreteVisitorA implements Visitor {
    @Override
    public void visit(ConcreteElementA element) {
        System.out.println("ConcreteVisitorA visiting ConcreteElementA");
    }

    @Override
    public void visit(ConcreteElementB element) {
        System.out.println("ConcreteVisitorA visiting ConcreteElementB");
    }
}

// 具体访问者B
class ConcreteVisitorB implements Visitor {
    @Override
    public void visit(ConcreteElementA element) {
        System.out.println("ConcreteVisitorB visiting ConcreteElementA");
    }

    @Override
    public void visit(ConcreteElementB element) {
        System.out.println("ConcreteVisitorB visiting ConcreteElementB");
    }
}

// 元素接口
interface Element {
    void accept(Visitor visitor);
}

// 具体元素A
class ConcreteElementA implements Element {
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

// 具体元素B
class ConcreteElementB implements Element {
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

// 对象结构
class ObjectStructure {
    private List<Element> elements = new ArrayList<>();

    public void attach(Element element) {
        elements.add(element);
    }

    public void detach(Element element) {
        elements.remove(element);
    }

    public void accept(Visitor visitor) {
        for (Element element : elements) {
            element.accept(visitor);
        }
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        ObjectStructure objectStructure = new ObjectStructure();
        objectStructure.attach(new ConcreteElementA());
        objectStructure.attach(new ConcreteElementB());

        Visitor visitorA = new ConcreteVisitorA();
        Visitor visitorB = new ConcreteVisitorB();

        objectStructure.accept(visitorA);
        objectStructure.accept(visitorB);
    }
}


策略模式和访问者模式都通过委派建立,
但visitor是站在外部client的角度,灵活增加对ADT的各种不同操作(哪怕ADT没实现该操作)。
strategy则是站在内部ADT的角度,灵活变化对其内部功能的不同配置。


四、各种模式之间的共性与区别

这里老师总结的已经非常到位了,所以直接上PPT。
总体来说,设计模式可以根据是否使用委派分为两种共性样式,因为模板模式未使用委派,而是使用继承和重写来实现,其他模板则均使用委派。
在这里插入图片描述
在这里插入图片描述


在这里插入图片描述
对于适配器模式,就是对某个接口进行实现,通过委派关系将要实现的类和已存在的类关联起来,以实现复用。这里被适配的类可能在另一棵继承树中。
在这里插入图片描述
策略模式就是给用户提供了多个算法实现同一功能。只需要用户将策略类的实例动态传给上下文类即可。上下文类对功能的实现会委派给策略类完成。

在这里插入图片描述
迭代器模式中,ADT的实例会通过迭代器创建方法传给迭代器实例,再通过迭代器实例遍历该ADT。迭代器创建方法被定义在Iterable接口中,如果某个类实现了该接口,说明该类是可创建迭代器并通过该迭代器遍历的。
在这里插入图片描述
工厂模式就是讲实现类的创建委派给工厂类实现。这样做是为了隐藏具体实现。
在这里插入图片描述
访问者模式对ADT的某些操作拓展。所以需要双向委派,ADT实现"可访问"接口,便可以接受各种访问者来完成各种操作。访问者作为参数传给accept,而accept内部实现则是ADT实例传给访问者。
在这里插入图片描述
装饰模式的实现其实最根本的就是基于ADT和装饰器共用同一个接口。装饰器对"核心"进行层层装饰后,其类型仍为接口类型,所以又可被外层装饰器接收并装饰。装饰的过程,首先要调用内层的方法,然后再由外层完成修饰,所以这里说是递归的设计。
在这里插入图片描述

  • 35
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值