设计模式

设计模式

构造器模式和原型模式

ES6之前

  1. 构造器模式

    function User(name,age) {
    this.name = name;
    this.age = age;

    this.say = function() {
    console.log(`我叫${this.name}, 今年${this.age}岁`);
    }
    }

    //实例化之后使用
    const zs = new User("张三", 18);
    const ls = new User("李四", 18);

    zs.say === ls.say; //false
  2. 原型模式

    function User(name,age) {
    this.name = name;
    this.age = age;

    tser.proptotype.say = function() {
    console.log(`我叫${this.name}, 今年${this.age}岁`);
    }
    }

    const zs = new User("张三", 18);
    const ls = new User("李四", 18);

    zs.say === ls.say; //true
  3. 区别

    • 原型模式将方法定义在原型上可以节省内存,因为所有实例都指向同一个方法实例。而将方法定义在构造函数内部则是为了能够让每个实例拥有自己的版本。

ES6

  1. 使用class,构造器+原型,自动将属性挂载到实例上,将方法挂载在原型上

    class User {

    name: string
    age: number

    //放之前的构造器代码
    constructor(name: string, age: number) {
    this.name = name
    this.age = age
    }
    //放之前的原型代码
    say() {
    console.log(`我叫${this.name}, 今年${this.age}岁`)
    }
    }

简单工厂模式

  1. 工厂模式:设计一个工厂函数,接收不同的参数,工厂根据参数不同进行实例化所需要的对象,无需知道创建的具体细节。但是当判断较多修改代码较为麻烦。

    class User {
    constructor(name, skill) {
    this.name = name;
    this.skill = skill;
    }
    static UserFactory(name) {
    switch(name) {
    case "zs":
    return new User("zs", ['js','java','python']);
    break;
    case 'ls':
    return new User("ls", ['c++','c#']);
    break;
    case "ww":
    return new User('ww', ['eat']);
    break;
    }
    }

    }


    //使用
    const user = User.UserFactory("zs");

抽象工厂模式

  1. 抽象工厂:不直接生成实例,只是告诉你该实例化哪个类

    class User {
    constructor (name, skill) {
    this.name = name;
    this.skill = skill;
    }
    sayHello() {
    console.log("welcome")
    }
    skilltree() {

    }

    }

    class HighUser extends User {
    constructor(name) {
    super(name , ['js','java','python'])
    }
    //一系列方法
    }

    class MidUser extends User {
    constructor(name) {
    super(name ,['c++','c#'])
    }
    ....
    }

    class LowUser extends User {
    constructor(name) {
    super(name ,['eat'])
    }
    ....
    }

    function AbstractUserFactory(name) {
    switch(name) {
    case "zs":
    return HighUser
    break;
    case 'ls':
    return MidUser;
    break;
    case "ww":
    return LowUser;
    break;
    }
    }

  2. 抽象工厂模式返回你需要的类,需要自己进行实例化,相比于简单工厂模式,工厂代码中的耦合性有所降低。

建造者模式

  1. 建造者模式:创建复杂对象的设计模式,将复杂对象的构建与它的表示分离,使同样的构建过程可以创建不同的表示
  2. 主要包括四部分:产品,抽象类,具体建造者,指挥者四部分

普通建造者模式示例

主要四部分
// 产品
public class Computer {
private String cpu;
private String gpu;
private String ram;
private String storage;

public void setCpu(String cpu) {
this.cpu = cpu;
}

public void setGpu(String gpu) {
this.gpu = gpu;
}

public void setRam(String ram) {
this.ram = ram;
}

public void setStorage(String storage) {
this.storage = storage;
}
}

// 抽象建造者
public abstract class ComputerBuilder {
protected Computer computer = new Computer();

public abstract void buildCpu();
public abstract void buildGpu();
public abstract void buildRam();
public abstract void buildStorage();

public Computer getComputer() {
return computer;
}
}

// 具体建造者
public class PCBuilder extends ComputerBuilder {
@Override
public void buildCpu() {
computer.setCpu("Intel Core i7");
}

@Override
public void buildGpu() {
computer.setGpu("NVIDIA GeForce RTX 3080");
}

@Override
public void buildRam() {
computer.setRam("16GB DDR4");
}

@Override
public void buildStorage() {
computer.setStorage("1TB SSD");
}
}

// 指挥者
public class Director {
private ComputerBuilder builder;

public Director(ComputerBuilder builder) {
this.builder = builder;
}

public void construct() {
builder.buildCpu();
builder.buildGpu();
builder.buildRam();
builder.buildStorage();
}
}
使用
public class Client {
public static void main(String[] args) {
ComputerBuilder builder = new PCBuilder();
Director director = new Director(builder);
director.construct();
Computer computer = builder.getComputer();
}
}
构建过程和表示分离
  1. 构建过程:指创建和组装一个复杂对象的步骤。在上面的示例中,构建过程就是 buildCpubuildGpubuildRambuildStorage 这四个方法。这些方法定义了如何创建和组装 Computer 对象。
  2. 表示:这是指复杂对象的最终形态。在上面的示例中,表示就是 Computer 类。Computer 类定义了一个复杂对象的属性和行为。
  3. 释意:我们可以改变对象的构建过程,而不影响对象的表示。例如,我们可以创建一个新的建造者类,比如 LaptopBuilder,它有不同的 buildCpubuildGpubuildRambuildStorage 方法。然后,我们可以使用 Director 类来使用这个新的建造者类,构建一个新的 Computer 对象。尽管构建过程改变了,但是 Computer 对象的表示(即它的属性和行为)并没有改变。

简单构建者模式(支持链式调用)

  1. 大部分简单情况下,对于Director指挥者的部分我们并不需要,所以就有了简单构造者模式
实现
public class Computer {

private String cpu;
private int ram;
private int storage;

private Computer(Builder builder) {
this.cpu = builder.cpu;
this.ram = builder.ram;
this.storage = builder.storage;
}

@Override
public String toString() {
return "Computer{" +
"cpu='" + cpu + '\'' +
", ram='" + ram + '\'' +
", storage=" + storage +
'}';
}
//建造者
public static class Builder {
private String cpu;
private int ram;
private int storage;

public Builder setCpu(String cpu) {
this.cpu = cpu;
return this;
}

public Builder setRam(int ram) {
this.ram = ram;
return this;
}

public Builder setStorage(int storage) {
this.storage = storage;
return this;
}

public Computer build() {
//可以进行逻辑处理
return new Computer(this);
}
}
}
使用
public class Client {
public static void main(String[] args) {
Computer computer = new Computer.Builder()
.setCpu("i7")
.setRam(16)
.setStorage(512)
.build();
System.out.println(computer);
}
}
链式调用优点
  1. 可以灵活设置属性:使用构造方法,实例化时如果设置部分属性的组合,那就需要多个构造方法,不灵活
  2. 可以设置属性后,在build()方法中统一处理逻辑:对于一些逻辑处理,一些属性的判断可能依靠其他属性,必须按照先后顺序规则依次set。
注意点
  1. 目标类的构造方式必须传入builder对象
  2. builder类位于目标类内,且用static描述
  3. builder类中的set方法返回值必须为builder本身,这是可以链式调用的关键
  4. builder中的build方法用来实现目标对象的创建

观察者模式

定义

  1. 包括观察者(Observer),被观察者(Subject)以及行为,通过定义一种订阅机制,当对象状态发生改变时,所有依赖其的对象都会得到通知

示例

  1. Observer接口定义了一个通用的update方法,所有具体的观察者类都需要实现这个方法来响应被观察者状态的变化。
  2. ConcreteObserverAConcreteObserverB是具体的观察者类,它们实现了Observer接口并提供了update方法的具体实现。
  3. Subject类是抽象被观察者,它维护了一个观察者列表,并提供了addremovenotifyObservers方法。其中,notifyObservers方法负责在被观察者状态变化时遍历观察者列表并调用每个观察者的update方法。
  4. ConcreteSubject是具体被观察者类,它继承自Subject并添加了一个state属性及对应的getStatesetState方法。当setState方法被调用且状态实际发生变化时,会触发notifyObservers方法,通知所有注册的观察者。
  5. 运行主程序Main,您将看到当ConcreteSubject对象的状态发生变化时,两个具体的观察者ConcreteObserverAConcreteObserverB都会收到通知并打印出接收到的消息。
//观察者
public interface Observer {
void update(String message);
}

/**
* 具体观察者
*/
public class ConcreteObserverA implements Observer {
@Override
public void update(String message) {
System.out.println("接收到消息,并进行处理" + message);
}
}

public class ConcreteObserverB implements Observer{
@Override
public void update(String message) {
System.out.println("ConcreteObserverB接收到消息,并进行处理" + message);
}
}

/**
* 被观察者类
*/
public abstract class Subject {

private List<Observer> observers = new ArrayList<>();

public void addObserver(Observer observer) {
observers.add(observer);
}

public void removeObserver(Observer observer) {
observers.remove(observer);
}

public void notifyObserver(String message) {
for (Observer observer : observers) {
observer.update(message);
}
}
}

/**
* 具体被观察者
*/
public class ConcreteSubject extends Subject{

private String state;

public String getState() {
return state;
}

public void setState(String state) {
this.state = state;
notifyObserver(state);
}
}

public class Main {
public static void main(String[] args) {
ConcreteSubject subject = new ConcreteSubject();

Observer observerA = new ConcreteObserverA();
Observer observerB = new ConcreteObserverB();

subject.addObserver(observerA);
subject.addObserver(observerB);

subject.setState("Hello");
subject.setState("World");
}
}

设计模式六大原则

开闭原则(Open-Closed Principle,OCP)

定义

  1. 对扩展开放,对修改关闭。

  2. 实现开闭原则的主要策略是使用抽象构建框架,使用实现扩展细节。以下是一些常用的策略:

    1. 使用接口或抽象类:接口和抽象类都是定义规范和抽象层,子类或实现类提供具体的实现。当需求变化时,只需要增加新的实现类或子类,而无需修改原有代码。
    2. 使用设计模式:很多设计模式都是为了满足开闭原则而生的。例如,策略模式允许我们在运行时选择算法,模板方法模式允许我们改变算法的某些步骤,装饰器模式允许我们动态地添加或修改行为等。
    3. 使用依赖注入:依赖注入是一种编程技术,它允许我们在运行时改变程序的依赖关系。这样,我们可以在不修改代码的情况下改变程序的行为。
  3. 依赖注入示例:NotificationService依赖于MessageService,但是它并不知道具体使用的是哪个实现类。MessageService的具体实现是通过构造函数注入的,这就是依赖注入。当我们需要改变消息发送的方式时,我们只需要更改MessageService的实现类,而不需要修改NotificationService的代码。例如,如果我们想要使用 SMS 来发送消息,我们只需要在 Spring 的配置文件中更改MessageService的实现类为SMSService`,而不需要修改 NotificationService 的代码。这就是开闭原则的体现。

    public interface MessageService {
    void sendMessage(String message);
    }

    public class EmailService implements MessageService {
    @Override
    public void sendMessage(String message) {
    System.out.println("Email Message Sent: " + message);
    }
    }

    public class SMSService implements MessageService {
    @Override
    public void sendMessage(String message) {
    System.out.println("SMS Message Sent: " + message);
    }
    }
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;

    @Component
    public class NotificationService {
    private MessageService messageService;

    @Autowired
    public NotificationService(MessageService messageService) {
    this.messageService = messageService;
    }

    public void notify(String message) {
    messageService.sendMessage(message);
    }
    }

里氏替换原则

定义

  • 子类可以扩展父类的功能,但不能改变父类原有的功能。当将基类替换成它的子类对象,程序不会产生异常。

关注点

行为一致性
  1. 子类对象在替换其基类对象时,应确保在程序中表现出与基类对象相同的行为。这意味着在任何使用基类的地方,都可以无缝地使用其子类对象,而不会影响程序的正确性。这要求子类对基类接口的实现必须遵循基类定义的约定,包括方法的输入/输出、异常处理、状态变化等。
前置条件
  1. 子类在重写基类方法时,不得放宽基类方法的前置条件(即方法的输入要求)。例如,如果基类方法要求输入参数是非空的,那么子类方法也必须保持这一要求,不得接受空参数。

  2. 反例:基类 PaymentProcessor,其中有一个方法 processPayment(amount: number),要求输入的 amount 必须大于0。子类 DiscountedPaymentProcessor 覆盖了该方法,但在实现中允许处理金额为0的支付请求。

    class PaymentProcessor {
    processPayment(amount: number) {
    if (amount <= 0) throw new Error('Amount must be greater than zero.');
    // ...正常的支付处理逻辑
    }
    }

    //折扣处理
    class DiscountedPaymentProcessor extends PaymentProcessor {
    processPayment(amount: number) {
    if (amount === 0) console.log('Processing free payment.');
    else super.processPayment(amount);
    }
    }
  3. 解决方法: 子类应保持与基类相同的前置条件,不允许处理金额为0的支付请求。如果需要特殊处理这种情况,应在其他方法或逻辑中处理,而不是直接放宽基类方法的前置条件。

    class DiscountedPaymentProcessor extends PaymentProcessor {
    processFreePayment() {
    console.log('Processing free payment.');
    }

    processPayment(amount: number) {
    if (amount <= 0) this.processFreePayment();
    else super.processPayment(amount);
    }
    }
后置条件
  1. 子类方法应至少提供与基类方法相同的后置条件(即方法的输出保证)。理想情况下,子类方法还应努力提供更强的后置条件或保证,以增强方法的行为。比如,如果基类方法承诺在成功执行后将对象置于某种有效状态,子类方法也应确保同样的有效状态。

  2. 反例: 基类 List 有一个方法 add(item: T): boolean,承诺在成功添加元素时返回 true,否则返回 false。子类 FixedSizeList 覆盖了该方法,当列表已满时直接忽略添加请求,返回 undefined

    class List<T> {
    add(item: T): boolean {
    // ...正常的添加逻辑,返回是否成功添加
    }
    }

    class FixedSizeList<T> extends List<T> {
    add(item: T) {
    if (this.isFull()) return; // 返回 undefined,违反后置条件
    super.add(item);
    }
    }
  3. 解决方法: 子类应遵循基类方法的后置条件,即使在列表满时也应该返回一个布尔值,表明添加操作的结果。可以返回 false 表示添加失败。

    class FixedSizeList<T> extends List<T> {
    add(item: T) {
    if (this.isFull()) return false; // 返回 false,表示添加失败
    super.add(item);
    }
    }
异常行为
  1. 子类方法抛出的异常类型应与基类方法保持一致,或者为基类方法抛出异常类型的子类型。这样,客户端代码无需因使用子类对象而改变对异常的处理方式。如果基类方法声明抛出某个异常,子类方法可以抛出该异常的同类型或子类型,但不应抛出基类方法未声明的异常。

  2. 反例: 基类 FileManager 有一个方法 deleteFile(path: string),在文件不存在时抛出 FileNotFoundException。子类 CloudFileManager 覆盖了该方法,当云文件不存在时抛出 CloudStorageException

    class FileManager {
    deleteFile(path: string) {
    if (!fileExists(path)) throw new FileNotFoundException();
    // ...正常的文件删除逻辑
    }
    }

    class CloudFileManager extends FileManager {
    deleteFile(path: string) {
    if (!cloudFileExists(path)) throw new CloudStorageException(); // 抛出不同的异常类型
    super.deleteFile(path);
    }
    }
  3. 解决方法: 子类应保持与基类方法相同的异常层次结构。在本例中,可以继续抛出 FileNotFoundException 或其子类,以保持对客户端代码的兼容性。

    class CloudStorageException extends FileNotFoundException {
    // ...
    }

    class CloudFileManager extends FileManager {
    deleteFile(path: string) {
    if (!cloudFileExists(path)) throw new CloudStorageException(); // 抛出基类异常的子类
    super.deleteFile(path);
    }
    }

依赖倒置原则(Dependency Inversion Principle,DIP)

定义

  1. 高层模块不依赖于低层模块
  2. 都应依赖于抽象而非具体实现
  3. 细节依赖抽象而非抽象依赖细节
  4. 面向接口编程

实现

  1. 其实以上四点的核心就是抽象不应该依赖于细节,细节应该依赖于抽象,

  2. 在传统的程序设计中,高层模块(复杂的、业务逻辑多的模块)依赖于低层模块(简单的、基础的模块),而低层模块是基于具体的实现细节来设计的。这样一来,高层模块就会依赖于这些实现细节,这就导致了代码的耦合度高,不利于维护和扩展。

  3. 不管是高层模块还是低层模块,都应该基于抽象(如接口或抽象类)来设计。这样,高层模块就不会依赖于低层模块的实现细节,而是依赖于抽象;低层模块的实现细节也是基于抽象,也就是说,细节依赖于抽象。

  4. 这样做的好处是,当我们需要改变实现细节(如使用不同的数据源)时,只需要提供一个新的抽象实现,而不需要修改依赖于这些细节的高层模块的代码。这就降低了代码的耦合度,提高了代码的可维护性和可扩展性。

  5. 示例:在这个例子中,NotificationService(高层模块)不直接依赖于 EmailSender 或 SMSSender(低层模块),而是依赖于 MessageSender 抽象。这就是面向接口编程。


    // 抽象
    interface MessageSender {
    void send(String message);
    }

    // 细节
    class EmailSender implements MessageSender {
    @Override
    public void send(String message) {
    // Send email
    }
    }

    class SMSSender implements MessageSender {
    @Override
    public void send(String message) {
    // Send SMS
    }
    }

    // 高层模块
    class NotificationService {
    private MessageSender messageSender;

    public NotificationService(MessageSender messageSender) {
    this.messageSender = messageSender;
    }

    public void notify(String message) {
    messageSender.send(message);
    }
    }

    // 使用
    public class Client {
    public static void main(String[] args) {
    NotificationService service = new NotificationService(new EmailSender());
    service.notify("Hello, world!");
    }
    }
  6. 反例: NotificationService 直接依赖于 EmailSenderSMSSender,如果我们想要修改消息发送的实现(从邮件切换到短信发送),就需要修改NotificationService代码,这就违反了开闭原则(实体对扩展开放,对修改关闭)

    // 细节
    class EmailSender {
    public void send(String message) {
    // Send email
    }
    }

    class SMSSender {
    public void send(String message) {
    // Send SMS
    }
    }

    // 高层模块
    class NotificationService {
    private EmailSender emailSender;

    public NotificationService() {
    this.emailSender = new EmailSender(); // 直接依赖于具体的实现
    }

    public void notify(String message) {
    emailSender.send(message);
    }
    }

    // 使用
    public class Client {
    public static void main(String[] args) {
    NotificationService service = new NotificationService();
    service.notify("Hello, world!");
    }
    }
Author: Yang Wa
Link: https://blog.wxywxy.cn/2024/04/07/设计模式/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.