软件构造 | Design Patterns for Reuse and Maintainability

Design Patterns for Reuse and Maintainability

(面向可复用性和可维护性的设计模式)

Open-Closed Principle (OCP) ------对扩展的开放,对修改已有代码的封

Why reusable design patterns

A design...

  1. ...enables flexibility to change (reusability)
  2. ...minimizes the introduction of new problems when fixing old ones (maintainability)
  3. ...allows the delivery of more functionality after an initial delivery (extensibility).

除了类本身,设计模式更强调多 个类/对象之间的关系和交互过程---比接口/类复用的粒度更大.

1 🍉Creational patterns

Factory Method pattern(工厂方法)

Also known as "Virtual Constructor"(虚拟构造器)

  1. 当client不知道要创建哪个具体类的实例,或者不想在client代码中指明要具体 创建的实例时,用工厂方法。
  2. 定义一个用于创建对象的接口,让其子类来决定实例化哪一个类,从而使一个 类的实例化延迟到其子类。
java 复制代码
// Abstract product
public interface Trace{
    // turn om and off debugging
    public void setDebug(boolean debug);
    // write out aa debug message
    public void debug(String message);
    // write out an error message
    public void error(String message);
}

// Concrete product 1
public class FileTrace implements Trace{...}

// Concrete product 1
public class  SystemTrace implements Trace{...}

工厂方法来创建实例

java 复制代码
interface TraceFactory {
    public Trace getTrace();
    public Trace getTrace(String type);
    void otherOperation(){};
}

public class Factory1 implements TraceFactory {
    public Trace getTrace() {
    	return new SystemTrace();
    }
}

// 根据类型决定创建哪个具体产品
public class Factory2 implements TraceFactory {
    public getTrace(String type) {    // public static getTrace(String type);
   		if(type.equals("file")
    		return new FileTrace();
    	else if (type.equals("system")
    		return new SystemTrace();
     }
}
                 


Trace log1 = new Factory1().getTrace(); //  TraceFactory2.getTrace("system")
log1.setDebug(true);
log1.debug( "entering log" );
Trace log2 = new Factory2().getTrace("system");
log2.setDebug(false);
log2.debug("...");

2 🍈Structural patterns

2.1 Adapter适配器模式

Convert the interface of a class into another interface that clients expect to get.

将某个类/接口转换为client期望的其他形式

A LegacyRectangle 组件的 display() 方法希望接收 接收 "x、y、w、h "参数。但客户希望传递 "左上方 x 和 y "以及 "右下方 "x 和 y"。这种不协调可以通过添加额外的间接层(即适配器对象)来解决。

没有适配器

有适配器

2.2 Decorator(装饰器模式)

对每一个特性构造子类,通过委派机制增加到对象上

继承与装饰器的区别

  1. 目的:继承主要用于实现代码重用和扩展功能,而装饰器主要用于在不修改原有类代码的情况下动态地添加新功能或修改其行为。
  2. 耦合性:继承可能导致较高的耦合性,因为子类与父类紧密相关;而装饰器与原始类之间的耦合度较低,因为它们之间是通过接口或抽象类进行交互的。
  3. 灵活性:装饰器比继承更灵活,因为它们可以在运行时动态地添加或删除功能,而不需要修改原始类的代码。
  4. 扩展性:虽然继承也支持扩展功能,但装饰器提供了一种更灵活、更可维护的方式来扩展类的功能。
  5. 使用场景:当需要在多个类之间共享一些公共功能时,可以使用继承;当需要在不修改原始类代码的情况下动态地添加新功能或修改其行为时,可以使用装饰器。

An example

以下是一个使用装饰器设计模式的具体例子,这个例子展示如何为一个简单的咖啡类添加不同的"调料"或"装饰",比如加奶或加糖。

首先,我们定义一个Coffee接口,它有一个getCost()方法和一个getDescription()方法:

java 复制代码
public interface Coffee {  
    double getCost();  
    String getDescription();  
}

然后,我们创建一个实现了Coffee接口的简单咖啡类SimpleCoffee

java 复制代码
public class SimpleCoffee implements Coffee {  
    @Override  
    public double getCost() {  
        return 2.0;  
    }  
  
    @Override  
    public String getDescription() {  
        return "Simple Coffee";  
    }  
}

接下来,我们定义一个装饰器基类CoffeeDecorator,它也实现了Coffee接口,并持有一个Coffee对象的引用:

java 复制代码
public abstract class CoffeeDecorator implements Coffee {  
    protected Coffee coffee;  
  
    public CoffeeDecorator(Coffee coffee) {  
        this.coffee = coffee;  
    }  
  
    @Override  
    public double getCost() {  
        return coffee.getCost();  
    }  
  
    @Override  
    public String getDescription() {  
        return coffee.getDescription();  
    }  
}

现在,我们可以创建具体的装饰器类,比如MilkDecorator(加奶的装饰器)和SugarDecorator(加糖的装饰器):

java 复制代码
public class MilkDecorator extends CoffeeDecorator {  
    public MilkDecorator(Coffee coffee) {  
        super(coffee);  
    }  
  
    @Override  
    public double getCost() {  
        return super.getCost() + 0.5; // 假设加奶增加0.5元  
    }  
  
    @Override  
    public String getDescription() {  
        return super.getDescription() + ", Milk";  
    }  
}  
  
public class SugarDecorator extends CoffeeDecorator {  
    public SugarDecorator(Coffee coffee) {  
        super(coffee);  
    }  
  
    @Override  
    public double getCost() {  
        return super.getCost() + 0.3; // 假设加糖增加0.3元  
    }  
  
    @Override  
    public String getDescription() {  
        return super.getDescription() + ", Sugar";  
    }  
}

最后,我们创建一个客户端类来测试这些装饰器:

java 复制代码
public class CoffeeShop {  
    public static void main(String[] args) {  
        Coffee coffee = new SimpleCoffee();  
        System.out.println("Cost: " + coffee.getCost() + ", Description: " + coffee.getDescription());  
  
        coffee = new MilkDecorator(coffee);  
        System.out.println("Cost: " + coffee.getCost() + ", Description: " + coffee.getDescription());  
  
        coffee = new SugarDecorator(coffee);  
        System.out.println("Cost: " + coffee.getCost() + ", Description: " + coffee.getDescription());  
  
        // 也可以先加糖再加奶  
        Coffee anotherCoffee = new SimpleCoffee();  
        anotherCoffee = new SugarDecorator(anotherCoffee);  
        anotherCoffee = new MilkDecorator(anotherCoffee);  
        System.out.println("Cost: " + anotherCoffee.getCost() + ", Description: " + anotherCoffee.getDescription());  
    }  
}

运行CoffeeShopmain方法,你将看到不同装饰器对咖啡成本和描述的影响。这就是装饰器设计模式的一个具体实现,它允许我们在运行时动态地给对象添加新的责任(比如加奶或加糖),同时保持代码的灵活性和可扩展性。

3 🍌Behavioral patterns

3.1 Strategy

有多种不同的算法来实现同一个任务,但需要client根据需要 动态切换算法,而不是写死在代码里

java 复制代码
public interface PaymentStrategy {
	public void pay(int amount);
}

public class CreditCardStrategy implements PaymentStrategy {
    private String name;
    private String cardNumber;
    private String cvv;
    private String dateOfExpiry;
    public CreditCardStrategy(String nm, String ccNum, 
        String cvv, String expiryDate){
        this.name=nm;
        this.cardNumber=ccNum;
        this.cvv=cvv;
        this.dateOfExpiry=expiryDate;
    }
    @Override
    public void pay(int amount) {
    	System.out.println(amount +" paid with credit card");
    }
}

public class PaypalStrategy implements PaymentStrategy {
    private String emailId;
    private String password;
    public PaypalStrategy(String email, String pwd){
        this.emailId=email;
        this.password=pwd;
    }
    @Override
    public void pay(int amount) {
    	System.out.println(amount + " paid using Paypal.");
    }
}

策略设计模式定义了一系列的算法,并将每一个算法封装起来,使它们可以互相替换。策略模式使得算法可以独立于使用它的客户端变化。

在给出的代码中,我们可以看到以下组成部分:

  1. 策略接口 (PaymentStrategy):
    定义了一个支付策略的公共接口,即pay(int amount)方法。客户端代码将使用此接口与不同的支付策略交互,而无需知道它们的具体实现。
  2. 具体策略 (CreditCardStrategyPaypalStrategy):
    这两个类分别实现了PaymentStrategy接口,提供了不同的支付算法实现。它们分别对应于使用信用卡和PayPal进行支付的逻辑。
    • CreditCardStrategy类保存了信用卡的相关信息(如持卡人姓名、卡号、CVV和到期日期),但在pay方法中并没有使用这些信息,只是简单地打印了一条消息。在实际应用中,这些信息可能会被用来与信用卡处理系统交互以完成支付。
    • PaypalStrategy类保存了PayPal账户的相关信息(如电子邮件和密码),但同样在pay方法中并没有使用这些信息,只是打印了一条消息。在实际应用中,这些信息可能会被用来与PayPal系统交互以完成支付。
  3. 客户端代码
    客户端代码通常会包含一个PaymentStrategy类型的变量,并使用它来执行支付操作。客户端代码可以通过在运行时动态地更改此变量的类型来选择不同的支付策略。

下面是一个简单的客户端代码示例,展示了如何使用这些策略:

java 复制代码
public class PaymentClient {  
    private PaymentStrategy paymentStrategy;  
  
    public PaymentClient(PaymentStrategy strategy) {  
        this.paymentStrategy = strategy;  
    }  
  
    public void makePayment(int amount) {  
        paymentStrategy.pay(amount);  
    }  
  
    public static void main(String[] args) {  
        PaymentClient client = new PaymentClient(new CreditCardStrategy("John Doe", "1234567890123456", "123", "12/25"));  
        client.makePayment(100); // 100 paid with credit card  
  
        client = new PaymentClient(new PaypalStrategy("user@example.com", "password"));  
        client.makePayment(50);  // 50 paid using Paypal.  
    }  
}

在这个示例中,PaymentClient类封装了对支付策略的引用,并提供了makePayment方法来执行支付操作。通过向PaymentClient构造函数传入不同的PaymentStrategy对象,我们可以动态地更改其使用的支付策略。

3.2 Template Method

做事情的步骤一样,但具体方法不同;共性的步骤在抽象类内公共实现,差异化的步骤在各个子类中实现

Template Method设计模式是一种行为设计模式,它在一个方法中定义了一个算法的骨架,并允许子类为一个或多个步骤提供实现。模板方法使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤。

java 复制代码
public abstract class OrderProcessTemplate {
    public boolean isGift;
    public abstract void doSelect();
    public abstract void doPayment();
    public void giftWrap() {
    	System.out.println("Gift wrap done.");
}
    public abstract void doDelivery();
    public final void processOrder() {
        doSelect();
        doPayment();
        if (isGift)
            giftWrap();
        doDelivery();
    }
}

public class NetOrder extends OrderProcessTemplate {
    @Override
    public void doSelect() { ... }
    @Override
    public void doPayment() { ... }
    @Override
    public void doDelivery() { ... }
}

3.3 Iterator

客户端希望遍历被放入 容器/集合类的一组ADT对象,无需关心容器的具体类型

也就是说,不管对象被放进哪里,都应该提供同样的遍历方式

Iterable接口:实现该接口的集合对象是可迭代遍历的

java 复制代码
public interface Iterable<T> {
    ...
    Iterator<T> iterator();
}

Iterator接口:迭代器

java 复制代码
public interface Iterator<E> {
    boolean hasNext();
    E next();
    void remove();
}

Iterator pattern :让自己的集合类实现Iterable接口,并实现自己的独特Iterator迭代器(hasNext, next, remove),允许客户端利用这个 迭代器进行显式或隐式的迭代遍历:

迭代器设计模式包含以下几个主要角色:

  1. 迭代器(Iterator):
    • 定义一个访问聚合对象元素但不暴露其内部表示的接口。
    • 提供遍历聚合对象的方法,如hasNext()(检查是否还有下一个元素)和next()(获取下一个元素)。
  2. 具体迭代器(ConcreteIterator):
    • 实现迭代器接口,并跟踪遍历中的当前位置。
    • 使用聚合对象的内部表示法来遍历元素。
  3. 聚合(Aggregate):
    • 定义创建迭代器对象的接口。
    • 通常是包含一组元素的类,如列表、集合等。
  4. 具体聚合(ConcreteAggregate):
    • 实现聚合接口,返回具体迭代器的实例。
    • 在具体聚合中,通常包含存储元素的私有成员变量。

下面是一个简单的Java示例,展示了迭代器设计模式的基本用法:

java 复制代码
// 聚合接口  
interface Aggregate {  
    Iterator createIterator();  
}  
  
// 迭代器接口  
interface Iterator {  
    boolean hasNext();  
    Object next();  
}  
  
// 具体聚合(例如一个列表)  
class MyList implements Aggregate {  
    private List<Object> elements = new ArrayList<>();  
  
    // 添加元素的方法  
    public void add(Object element) {  
        elements.add(element);  
    }  
  
    // 创建迭代器的方法  
    @Override  
    public Iterator createIterator() {  
        return new MyListIterator(this);  
    }  
  
    // 内部类:具体迭代器  
    private class MyListIterator implements Iterator {  
        private int currentIndex = 0;  
        private MyList list;  
  
        public MyListIterator(MyList list) {  
            this.list = list;  
        }  
  
        @Override  
        public boolean hasNext() {  
            return currentIndex < list.elements.size();  
        }  
  
        @Override  
        public Object next() {  
            if (!hasNext()) {  
                throw new NoSuchElementException();  
            }  
            return list.elements.get(currentIndex++);  
        }  
    }  
}  
  
// 客户端代码  
public class Client {  
    public static void main(String[] args) {  
        MyList list = new MyList();  
        list.add("Element 1");  
        list.add("Element 2");  
        list.add("Element 3");  
  
        Iterator iterator = list.createIterator();  
        while (iterator.hasNext()) {  
            System.out.println(iterator.next());  
        }  
    }  
}

显示的迭代

在这个示例中,MyList类实现了Aggregate接口,并提供了创建迭代器的方法。MyListIteratorMyList的内部类,实现了Iterator接口,并用于遍历MyList中的元素。客户端代码通过调用createIterator()方法获取迭代器,并使用迭代器遍历聚合对象中的元素。

An example of Iterator pattern

java 复制代码
public class Pair<E> implements Iterable<E> {
    private final E first, second;
    public Pair(E f, E s) { first = f; second = s; }
    public Iterator<E> iterator() {
   		return new PairIterator();
    }
    private class PairIterator implements Iterator<E> {
        private boolean seenFirst = false, seenSecond = false;
        public boolean hasNext() { return !seenSecond; }
        public E next() {
            if (!seenFirst) { seenFirst = true; return first; }
            if (!seenSecond) { seenSecond = true; return second; }
                throw new NoSuchElementException();
        }
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }
}

尽管for-each循环在语法上看起来像是隐式迭代,但实际上它仍然依赖于Iterable接口和Iterator的实现来工作。在运行时,Java会调用iterable.iterator()来获取迭代器,并使用迭代器的hasNext()next()方法来遍历元素。因此,虽然从语法上看似隐式,但从实现上看,迭代仍然是显式的。(for-each循环是Java提供的一种语法糖,用于简化对实现了Iterable<E>接口集合的遍历。)

3.4 Visitor

java 复制代码
/* Abstract element interface (visitable) */
public interface ItemElement {
	public int accept(ShoppingCartVisitor visitor);
}

/* Concrete element */
public class Book implements ItemElement{
    private double price;
    ...
    int accept(ShoppingCartVisitor visitor) {
    	visitor.visit(this);
    }
}

public class Fruit implements ItemElement{
    private double weight;
    ...
    int accept(ShoppingCartVisitor visitor) {
    	visitor.visit(this);
    }
}

代码展示了Visitor设计模式中的一部分,包括一个抽象元素接口(ItemElement)和两个具体的元素类(BookFruit)。这里我会逐一分析并解释代码。

抽象元素接口ItemElement

java 复制代码
/* Abstract element interface (visitable) */  
public interface ItemElement {  
    public int accept(ShoppingCartVisitor visitor);  
}
  • 接口定义ItemElement 是一个接口,它定义了一个名为 accept 的方法,该方法接受一个类型为 ShoppingCartVisitor 的参数,并返回一个整数。这个接口表示可以被访问(或称为"可访问的")的元素。
  • 方法 accept :这个方法用于允许一个 ShoppingCartVisitor 对象"访问"当前的元素对象。accept 方法中通常不会包含任何具体的业务逻辑,它只是调用传入的 visitor 对象的 visit 方法,并将当前对象(this)作为参数传递。

具体元素类

Book

java 复制代码
/* Concrete element */  
public class Book implements ItemElement{  
    private double price;  
    // ... 其他成员变量和方法 ...  
  
    int accept(ShoppingCartVisitor visitor) {  
        visitor.visit(this);  
    }  
}
  • 类定义Book 类实现了 ItemElement 接口,这意味着它必须提供 accept 方法的实现。
  • 方法 acceptBook 类的 accept 方法按照 ItemElement 接口的定义实现了对 ShoppingCartVisitor 的访问。当 accept 方法被调用时,它会将 this(即当前的 Book 对象)传递给 visitor 对象的 visit 方法。
  • 注意accept 方法应该是 public 的,因为接口中的方法默认是 public 的。但是在您提供的 Book 类的定义中,accept 方法是默认的包级私有访问权限(即没有显式地声明为 public)。这会导致编译错误,因为子类必须实现接口中的所有方法,并且这些方法的访问级别必须与接口中定义的访问级别相匹配。

Fruit

java 复制代码
public class Fruit implements ItemElement{  
    private double weight;  
    // ... 其他成员变量和方法 ...  
  
    int accept(ShoppingCartVisitor visitor) {  
        visitor.visit(this);  
    }  
}
  • 类定义Fruit 类同样实现了 ItemElement 接口,并且也需要提供一个 accept 方法的实现。
  • 方法 accept :与 Book 类类似,Fruit 类的 accept 方法也是将当前对象(this)传递给 visitor 对象的 visit 方法。
  • 同样的问题accept 方法也应该是 public 的,否则会导致编译错误。

总结

代码展示了Visitor设计模式的一部分,但是有几个地方需要注意:

  1. accept 方法的访问级别 :在 BookFruit 类中,accept 方法应该是 public 的,以便它们可以正确地实现 ItemElement 接口。
  2. ShoppingCartVisitor 接口 :虽然您没有提供 ShoppingCartVisitor 接口的定义,但根据代码中的使用,我们可以推测它应该定义了一个或多个 visit 方法,这些方法接受不同类型的 ItemElement(例如 BookFruit)作为参数。
  3. 返回类型accept 方法返回了一个整数。在Visitor模式中,这通常用于表示操作的结果或状态,但在某些情况下,该方法也可能返回 void
  4. 方法的实际业务逻辑 :通常,在 accept 方法内部不直接执行业务逻辑,而是在 visitorvisit 方法中执行。这样,您就可以在不修改元素类的情况下添加新的操作。
java 复制代码
/* Abstract visitor interface */
public interface ShoppingCartVisitor { 
    int visit(Book book); 
    int visit(Fruit fruit); 
} 

public class ShoppingCartVisitorImpl implements ShoppingCartVisitor {
    public int visit(Book book) {
        int cost=0;
        if(book.getPrice() > 50){
        	cost = book.getPrice()-5;
        }else 
        	cost = book.getPrice();
        System.out.println("Book ISBN::"+book.getIsbnNumber() + " cost ="+cost);
        return cost;
    }
    public int visit(Fruit fruit) {
        int cost = fruit.getPricePerKg()*fruit.getWeight();
        System.out.println(fruit.getName() + " cost = "+cost);
        return cost;
    }
}

4 🫐Commonality and Difference of Design Patterns

4.1 共性样式1

只使用"继承",不使用"delegation" 核心思路:OCP/DIP 依赖反转,客户端只依赖"抽象",不能 依赖于"具体" 发生变化时最好是"扩展"而不是"修改"

4.2 共性样式2

两棵"继承树",两个层次的"delegation"

相关推荐
FightingLod3 分钟前
C++中list容器使用详解
开发语言·c++·list
yogima7 分钟前
在Spring Data JPA中使用@Query注解
java·数据库·spring
赫萝的红苹果12 分钟前
基于Redisson实现分布式锁
java·spring boot·分布式
Zucker n13 分钟前
学会python——用python制作一个登录和注册窗口(python实例十八)
开发语言·python
mana飞侠13 分钟前
代码随想录算法训练营第59天:动态[1]
开发语言·数据结构·算法·动态规划
艾恩小灰灰15 分钟前
为何Web前端开发仍坚守 HTML 和 CSS,而不全然拥抱纯 JavaScript?
开发语言·前端·javascript·css·html·纯js
wang_book20 分钟前
redis学习(003 数据结构和通用命令)
java·数据库·redis·学习
英雄汉孑23 分钟前
图片压缩代码和实际操作页面
java
瑶风25 分钟前
go语言并发编程2-runtime
开发语言·golang·xcode
Eiceblue27 分钟前
Python 插入、替换、提取、或删除Excel中的图片
开发语言·vscode·python·pycharm·excel