Skip to content

面试煎熬成蛋_设计模式

问法:框架中的设计模式、项目中的设计模式

有空整合一下 编程_设计模式

工厂设计模式

讲一下工厂设计模式,你在项目中是如何使用的

工厂设计模式主要用于创建对象,尤其是在创建对象的逻辑比较复杂时,可以将对象的创建和使用分离,提高系统的灵活性和可维护性。

工厂模式主要有三种变体:

  • 简单工厂模式
  • 工厂方法模式
  • 抽象工厂模式

有空可以系统化学习一下设计模式相关的内容,包括符号表示和创建型 xxx 等概念。(软件工厂体系内容)

image.png

简单工厂模式

简单工厂模式又叫静态工厂方法模式,就是建立一个工厂类,对实现了同一接口的一些类进行 实例的创建

接口 → 不同实现类 → 创建交给工厂类

  1. 简单工厂模式(Simple Factory)

简单工厂模式并不是GoF(四人帮)定义的设计模式之一,但它是工厂模式的一种简化,主要用于创建同一类对象。

  • 结构:一个工厂类根据传入的参数,决定创建出哪一种产品类的实例。
  • 优点:将对象的创建和使用分离。
  • 缺点:工厂类的职责相对过重,增加新的产品时需要修改工厂类的逻辑,违反了开闭原则。

示例:这里给出一个简单工厂模式的Java代码示例,模拟一个日常项目中可能遇到的场景:根据不同的日志类型(如文件日志、数据库日志)创建不同的日志记录器。

首先,定义一个日志记录器接口:

public interface Logger {
    void log(String message);
}

然后,实现具体的日志记录器类,如文件日志记录器和数据库日志记录器:

public class FileLogger implements Logger {
    @Override
    public void log(String message) {
        System.out.println("Logging to a file: " + message);
    }
}

public class DatabaseLogger implements Logger {
    @Override
    public void log(String message) {
        System.out.println("Logging to a database: " + message);
    }
}

接下来,创建简单工厂类,用于根据日志类型创建具体的日志记录器对象:

public class LoggerFactory {
    public static Logger getLogger(String type) {
        if (type == null) {
            return null;
        }
        if (type.equalsIgnoreCase("FILE")) {
            return new FileLogger();
        } else if (type.equalsIgnoreCase("DATABASE")) {
            return new DatabaseLogger();
        }
        return null;
    }
}

最后,演示如何使用这个简单工厂来创建并使用不同类型的日志记录器:

public class SimpleFactoryDemo {
    public static void main(String[] args) {
        Logger fileLogger = LoggerFactory.getLogger("FILE");
        fileLogger.log("This is a message to log in a file.");

        Logger databaseLogger = LoggerFactory.getLogger("DATABASE");
        databaseLogger.log("This is a message to log in a database.");
    }
}

上面示例代码的核心:将对象的创建与使用分离

在这个示例中,LoggerFactory类根据传入的日志类型参数("FILE"或"DATABASE")决定创建并返回哪种类型的Logger实例。 这样,当需要增加新的日志类型时,只需添加新的Logger实现类并修改LoggerFactory类即可,而使用日志记录器的客户端代码不需要任何改动。

工厂方法模式
  1. 工厂方法模式(Factory Method)

工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类的实例化推迟到子类中进行。

  • 结构:定义一个创建对象的接口,但由实现这个接口的类来决定实例化哪个类。工厂方法让类的实例化延迟到其子类。
  • 优点:在添加新产品时不需要修改已有的工厂类,符合开闭原则。
  • 缺点:每增加一个产品,就需要增加一个具体类和对象实现工厂,增加了系统的复杂度。

示例使用:

假设我们有一个应用,需要根据不同的文件类型(如文本文件、图像文件)来处理文件,我们可以为每种文件类型定义一个处理器,并使用工厂方法模式来创建相应的文件处理器。

首先,定义一个FileProcessor接口和几个实现这个接口的类:

// FileProcessor接口
public interface FileProcessor {
    void process(String fileName);
}

// 文本文件处理器
public class TextFileProcessor implements FileProcessor {
    @Override
    public void process(String fileName) {
        System.out.println("Processing text file: " + fileName);
    }
}

// 图像文件处理器
public class ImageFileProcessor implements FileProcessor {
    @Override
    public void process(String fileName) {
        System.out.println("Processing image file: " + fileName);
    }
}

接着,定义一个抽象的工厂类,以及具体的工厂类来实现这个接口:

// 抽象工厂类
public abstract class FileProcessorFactory {
    // 工厂方法
    public abstract FileProcessor createProcessor();
}

// 文本文件处理器工厂
public class TextFileProcessorFactory extends FileProcessorFactory {
    @Override
    public FileProcessor createProcessor() {
        return new TextFileProcessor();
    }
}

// 图像文件处理器工厂
public class ImageFileProcessorFactory extends FileProcessorFactory {
    @Override
    public FileProcessor createProcessor() {
        return new ImageFileProcessor();
    }
}

最后,使用这些工厂来创建并使用文件处理器:

public class FactoryMethodDemo {
    public static void main(String[] args) {
        FileProcessorFactory textFactory = new TextFileProcessorFactory();
        FileProcessor textProcessor = textFactory.createProcessor();
        textProcessor.process("document.txt");

        FileProcessorFactory imageFactory = new ImageFileProcessorFactory();
        FileProcessor imageProcessor = imageFactory.createProcessor();
        imageProcessor.process("image.png");
    }
}

在这个示例中,FileProcessorFactory是一个抽象的工厂类,它定义了一个抽象的createProcessor方法。TextFileProcessorFactoryImageFileProcessorFactory是具体的工厂类,它们实现了createProcessor方法来创建具体类型的文件处理器。 这样,当我们需要处理不同类型的文件时,只需要使用相应类型的工厂实例来创建处理器,而不需要直接实例化处理器对象。

使用方法和简单工厂模式类似,不过区别是:工厂方法模式每增加一个产品,就需要增加一个具体类和对象实现工厂。


抽象工厂模式

抽象工厂模式是在简单工厂的基础上将未来可能需要修改的代码抽象出来,通过继承的方式让 子类去做决定。

接口 → 不同实现类

抽象工厂(定义统一行为) → 实现类工厂(不同接口方法组合) → 子类决定实例化具体类

抽象工厂模式是一种创建型设计模式,它允许创建一系列相关或相互依赖的对象,而无需指定它们具体的类。这种模式通过定义一个用于创建一组对象的接口(抽象工厂),让子类决定实例化具体类。

  1. 抽象工厂模式(Abstract Factory)

抽象工厂模式提供了一个接口,用于创建相关的对象家族,而不需要明确指定具体类。

  • 结构:多个抽象产品类,每个抽象产品类可以派生出多个具体产品类;一个抽象工厂类,可以派生出多个具体工厂类,每个具体工厂类可以创建多个具体产品类的实例,不同的工厂类互不影响。
  • 优点:能够确保客户端使用的是同一家族的产品。
  • 缺点:当产品家族中需要增加一个新的产品时,所有的工厂类都需要进行修改。

示例:假设我们需要创建跨平台的UI元素,例如按钮和复选框,不同的操作系统(如Windows和Mac)有不同风格的UI元素实现。

首先,定义抽象产品接口及其具体实现:

// 抽象产品 - 按钮
interface Button {
    void paint();
}

// 具体产品 - Windows按钮
class WindowsButton implements Button {
    public void paint() {
        System.out.println("Render a button in a Windows style.");
    }
}

// 具体产品 - Mac按钮
class MacButton implements Button {
    public void paint() {
        System.out.println("Render a button in a Mac style.");
    }
}

// 抽象产品 - 复选框
interface Checkbox {
    void paint();
}

// 具体产品 - Windows复选框
class WindowsCheckbox implements Checkbox {
    public void paint() {
        System.out.println("Render a checkbox in a Windows style.");
    }
}

// 具体产品 - Mac复选框
class MacCheckbox implements Checkbox {
    public void paint() {
        System.out.println("Render a checkbox in a Mac style.");
    }
}

接下来,定义抽象工厂接口及其具体实现:

// 抽象工厂
interface GUIFactory {
    Button createButton();
    Checkbox createCheckbox();
}

// 具体工厂 - Windows工厂
class WindowsFactory implements GUIFactory {
    public Button createButton() {
        return new WindowsButton();
    }
    
    public Checkbox createCheckbox() {
        return new WindowsCheckbox();
    }
}

// 具体工厂 - Mac工厂
class MacFactory implements GUIFactory {
    public Button createButton() {
        return new MacButton();
    }
    
    public Checkbox createCheckbox() {
        return new MacCheckbox();
    }
}

最后,客户端代码根据不同的需求创建不同的UI元素:

public class AbstractFactoryDemo {
    private static GUIFactory factory;

    public static void configure(String osType) {
        if (osType.equalsIgnoreCase("Windows")) {
            factory = new WindowsFactory();
        } else if (osType.equalsIgnoreCase("Mac")) {
            factory = new MacFactory();
        } else {
            throw new IllegalArgumentException("Unknown operating system type.");
        }
    }

    public static void main(String[] args) {
        configure("Windows");

        Button button = factory.createButton();
        Checkbox checkbox = factory.createCheckbox();

        button.paint();
        checkbox.paint();
    }
}

在这个例子中,抽象工厂GUIFactory提供了创建一系列产品(按钮和复选框)的接口,而WindowsFactoryMacFactory具体实现了这些接口,分别创建Windows风格和Mac风格的UI元素。

客户端代码通过抽象工厂接口与具体工厂解耦,使得在不同环境下可以灵活地更换工厂,而不需要修改客户端代码。

策略模式

讲一下策略模式,你在项目中是如何使用的

策略模式定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。

  • 结构:一个策略接口,多个策略实现类;一个上下文(Context)类,用来维护一个策略对象。
  • 优点:策略模式提供了管理相关的算法族的方法。
  • 缺点:客户端必须知道所有的策略类,并自行决定使用哪一个策略类。

策略模式(Strategy Pattern)允许在运行时选择算法的行为。这种模式定义了一系列算法,并将每一个算法封装起来,使它们可以互相替换。策略模式让算法独立于使用它的客户而变化。

以下是策略模式的一个具体示例,演示了如何实现一个简单的支付系统,该系统根据不同的支付策略(如信用卡支付、PayPal支付)来处理支付。

首先,定义支付策略接口和具体策略类:

// 支付策略接口
public interface PaymentStrategy {
    void pay(int amount);
}

// 信用卡支付策略
public class CreditCardStrategy implements PaymentStrategy {
    private String name;
    private String cardNumber;

    public CreditCardStrategy(String name, String cardNumber) {
        this.name = name;
        this.cardNumber = cardNumber;
    }

    @Override
    public void pay(int amount) {
        System.out.println(amount + " paid with credit card.");
    }
}

// PayPal支付策略
public class PaypalStrategy implements PaymentStrategy {
    private String emailId;

    public PaypalStrategy(String email) {
        this.emailId = email;
    }

    @Override
    public void pay(int amount) {
        System.out.println(amount + " paid using PayPal.");
    }
}

接下来,定义一个上下文(Context)类,它将根据客户选择的支付策略来处理支付:

public class ShoppingCart {
    private List<Item> items;
    private PaymentStrategy paymentStrategy;

    public ShoppingCart() {
        this.items = new ArrayList<>();
    }

    public void addItem(Item item) {
        this.items.add(item);
    }

    public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

    public int calculateTotal() {
        int sum = 0;
        for (Item item : items) {
            sum += item.getPrice();
        }
        return sum;
    }

    public void pay() {
        int amount = calculateTotal();
        paymentStrategy.pay(amount);
    }
}

最后,演示如何使用不同的支付策略来处理支付:

public class StrategyPatternDemo {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();
        cart.addItem(new Item("1234", 10));
        cart.addItem(new Item("5678", 40));

        // 使用信用卡支付
        cart.setPaymentStrategy(new CreditCardStrategy("John Doe", "1234567890123456"));
        cart.pay();

        // 改用PayPal支付
        cart.setPaymentStrategy(new PaypalStrategy("john.doe@example.com"));
        cart.pay();
    }
}

在这个示例中,PaymentStrategy接口定义了支付方法,CreditCardStrategyPaypalStrategy是具体的支付策略实现。ShoppingCart类允许客户添加商品并设置支付策略。在客户端代码中,可以根据需要轻松更换支付策略,无需修改购物车代码。这样,策略模式使得算法可以独立于使用它的客户端变化,并且可以动态地在运行时改变算法。


上述策略模式的基础使用是通过定义了一个抽象类,然后根据实际的不同行为来进行不同策略行为的实现

策略接口 → 不同策略实现类(对应不同行为实现)

一般常用的操作还可以:

  • 1、定义了一个公共接口 A,里面定义了不同的策略接口(具体是实现会交给抽象类的子类)
  • 2、定义了一个抽象类,这个抽象类实现了公共接口 A,并提供一些公共参数和方法
  • 3、具体实现类继承抽象类,然后实现公共接口 A 中的某种策略。

通过一个中转的抽象类,可用在这个抽象类中加一些公共的内容,可能对于策略模式的使用也是一种比较好的借鉴。

模板模式

模板模式主要是定义一个抽象类,然后这个抽象类中的核心方法中会定义一个既定的行为执行顺序

示例:

假设我们有一个制作饮料的过程,不同的饮料(如咖啡和茶)制作过程中的某些步骤相同(比如煮沸水),而某些步骤不同(如冲泡的原料不同)。我们可以使用模板方法模式来实现:

// 抽象类
abstract class Beverage {
    // 模板方法,制作饮料的步骤
    final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }

    // 抽象方法,由子类实现
    abstract void brew();
    abstract void addCondiments();

    void boilWater() {
        System.out.println("Boiling water");
    }

    void pourInCup() {
        System.out.println("Pouring into cup");
    }
}

// 具体类,咖啡
class Coffee extends Beverage {
    void brew() {
        System.out.println("Dripping Coffee through filter");
    }

    void addCondiments() {
        System.out.println("Adding Sugar and Milk");
    }
}

// 具体类,茶
class Tea extends Beverage {
    void brew() {
        System.out.println("Steeping the tea");
    }

    void addCondiments() {
        System.out.println("Adding Lemon");
    }
}

// 客户端代码
public class TemplateMethodDemo {
    public static void main(String[] args) {
        Beverage tea = new Tea();
        Beverage coffee = new Coffee();

        System.out.println("Making tea...");
        tea.prepareRecipe();

        System.out.println("\nMaking coffee...");
        coffee.prepareRecipe();
    }
}

组合模式

比较适合:构建树状结构的对象组合,通过定义一个组件接口,然后在组合对象中使用到了这个组件接口

示例:

我们将创建一个文件系统的树状结构,其中包含文件和文件夹。

定义组件接口

// 组件接口
interface Component {
    void showInfo();
}

创建叶子对象 File

// 叶子对象 - 文件
class File implements Component {
    private String name;

    public File(String name) {
        this.name = name;
    }

    public void showInfo() {
        System.out.println("File: " + name);
    }
}

创建组合对象 Folder

import java.util.ArrayList;
import java.util.List;

// 组合对象 - 文件夹
class Folder implements Component {
    private String name;
    private List<Component> children = new ArrayList<>();

    public Folder(String name) {
        this.name = name;
    }

    public void add(Component component) {
        children.add(component);
    }

    public void remove(Component component) {
        children.remove(component);
    }

    public void showInfo() {
        System.out.println("Folder: " + name);
        for (Component component : children) {
            component.showInfo();
        }
    }
}

客户端代码使用

public class CompositePatternDemo {
    public static void main(String[] args) {
        File file1 = new File("file1.txt");
        File file2 = new File("file2.txt");
        Folder folder1 = new Folder("Folder 1");
        folder1.add(file1);
        folder1.add(file2);

        File file3 = new File("file3.txt");
        Folder folder2 = new Folder("Folder 2");
        folder2.add(file3);

        Folder rootFolder = new Folder("Root");
        rootFolder.add(folder1);
        rootFolder.add(folder2);

        rootFolder.showInfo();
    }
}

在这个示例中,我们创建了文件和文件夹的树状结构,其中包含了叶子对象和组合对象。客户端可以递归地访问整个文件系统,而不需要关心对象是文件还是文件夹,体现了组合模式的统一处理特性。

责任链模式

讲一下责任链模式,你在项目中是如何使用的

责任链模式为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。

  • 结构:抽象处理者角色(Handler)定义了一个处理请求的接口,一系列具体处理者(ConcreteHandler)尝试处理请求,如果一个对象不能处理该请求,它会把相同的请求传给下一个接收者,依此类推。
  • 优点:降低耦合度。它将请求的发送者和接收者解耦。
  • 缺点:在找到正确的处理对象之前,所有的条件判断都要被执行一遍,当责任链过长时,可能会影响性能,尤其是在递归调用的时候。

关于责任链和策略模式的具体使用,需要再看一下相关视频: https://www.bilibili.com/list/watchlater

image.png

优缺点

image.png

常用场景

image.png


责任链模式

责任链:通过客户端定义执行链条,并设置下一链条执行流程,

责任链模式:为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。

典型使用:SpringMVC 中的拦截器

示例:

考虑一个简单的日志系统,它可以根据消息的严重性将消息输出到不同的地方(例如:控制台、文件、邮件等)。

abstract class Logger {
    public static int INFO = 1;
    public static int DEBUG = 2;
    public static int ERROR = 3;

    protected int level;

    //责任链中的下一个元素
    protected Logger nextLogger;

    public void setNextLogger(Logger nextLogger) {
        this.nextLogger = nextLogger;
    }

    public void logMessage(int level, String message) {
        if (this.level <= level) {
            write(message);
        }
        if (nextLogger != null) {
            nextLogger.logMessage(level, message);
        }
    }

    abstract protected void write(String message);
}

class ConsoleLogger extends Logger {

    public ConsoleLogger(int level) {
        this.level = level;
    }

    @Override
    protected void write(String message) {        
        System.out.println("Standard Console::Logger: " + message);
    }
}

class ErrorLogger extends Logger {

    public ErrorLogger(int level) {
        this.level = level;
    }

    @Override
    protected void write(String message) {        
        System.out.println("Error Console::Logger: " + message);
    }
}

class FileLogger extends Logger {

    public FileLogger(int level) {
        this.level = level;
    }

    @Override
    protected void write(String message) {
        System.out.println("File::Logger: " + message);
    }
}

public class ChainPatternDemo {
    
    private static Logger getChainOfLoggers(){

        Logger errorLogger = new ErrorLogger(Logger.ERROR);
        Logger fileLogger = new FileLogger(Logger.DEBUG);
        Logger consoleLogger = new ConsoleLogger(Logger.INFO);

        errorLogger.setNextLogger(fileLogger);
        fileLogger.setNextLogger(consoleLogger);

        return errorLogger;  
    }

    public static void main(String[] args) {
        Logger loggerChain = getChainOfLoggers();

        loggerChain.logMessage(Logger.INFO, "This is an information.");
        loggerChain.logMessage(Logger.DEBUG, "This is a debug level information.");
        loggerChain.logMessage(Logger.ERROR, "This is an error information.");
    }
}

在这个例子中,我们创建了一个日志处理的链。每个处理器(ConsoleLoggerFileLoggerErrorLogger)负责处理特定类型的日志消息。logMessage方法会根据消息的级别决定是否由当前处理器处理,如果不是,则传递给链中的下一个处理器。


责任链模式:客户端定义一个处理链条,一般情况下是:如果链路中它中间有一环能够处理,则这个链路进行处理,不行再到下一个链路

image.png

单例模式

你平常有了解过单例模式吗,一般怎么实现单例模式

单例模式是一种常用的软件设计模式,在应用这个模式时,单例对象的类必须保证只有一个实 例存在,整个系统只能使用一个对象实例

单例模式有很多种写法,懒汉模式, 饿汉模式,双重检查模式等。懒汉模式就是用的时候再去创建对象,饿汉模式就是提前就已经加载 好的静态static对象,双重检查模式就是两次检查避免多线程造成创建了多个对象。

单例模式有很多种的写法

  • (1)饿汉式单例模式的写法:线程安全
  • (2)懒汉式单例模式的写法:非线程安全
  • (3)双检锁单例模式的写法:线程安全

饿汉式

饿汉式单例模式在类加载时就完成了实例的初始化。它通过确保构造函数为private,避免了类在外部被实例化,在自己内部定义了自己的一个实例。

public class SingletonEager {
    // 在类加载时就完成实例化
    private static final SingletonEager instance = new SingletonEager();

    // 私有构造函数,防止被实例化
    private SingletonEager() {}

    // 获取唯一可用的对象
    public static SingletonEager getInstance() {
        return instance;
    }
}

懒汉式

懒汉式单例模式在第一次被引用时,才会将自己实例化。它同样保证了构造函数为private,确保类不能在外部被实例化。

public class SingletonLazy {
    private static SingletonLazy instance;

    private SingletonLazy() {}

    // 使用synchronized关键字保证线程安全
    public static synchronized SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
}

双检锁单例模式

双检锁单例模式结合了懒汉式单例模式的懒加载特性和饿汉式单例模式的线程安全特性。它在getInstance()方法中两次检查实例是否已经创建,并使用同步块确保只有一个实例被创建。这种方式既能保证线程安全,也能够减少同步带来的性能影响。

public class SingletonDoubleCheckedLocking {
    // 使用volatile关键字确保多线程环境下的可见性和禁止指令重排
    private static volatile SingletonDoubleCheckedLocking instance;

    private SingletonDoubleCheckedLocking() {}

    public static SingletonDoubleCheckedLocking getInstance() {
        // 第一次检查,避免不必要的同步
        if (instance == null) {
            synchronized (SingletonDoubleCheckedLocking.class) {
                // 第二次检查,确保只有一个实例被创建
                if (instance == null) {
                    instance = new SingletonDoubleCheckedLocking();
                }
            }
        }
        return instance;
    }
}

每种单例模式实现方式都有其适用场景:

  • 饿汉式适合于单例对象较少时使用。
  • 懒汉式适合于单例对象较多或单例对象创建开销大时使用,但需注意线程安全问题。
  • 双检锁则是一种既保证了懒加载也保证了线程安全的优化方案,但使用复杂且需要注意volatile关键字的使用。
你在项目中有使用过单例模式吗

代理模式

代理模式是给某一个对象提供一个代理,并由代理对象控制对原对象的引用。

代理模式是一种结构型设计模式,它为其他对象提供一个代理以控制对这个对象的访问。代理对象在客户端和目标对象之间起到中介的作用,可以在不改变目标对象接口的前提下,增强或控制对目标对象的访问。

对目标对象进行一定额外的操作

image.png