什么是设计模式?请简述其作用。

设计模式其实是在软件开发过程中经过经验积累和验证总结得出的一套通用代码设计方案,它们帮助开发者避免重复发明轮子,确保设计的一致性和可维护性,提高代码的可读性和可扩展性。

如果熟悉了设计模式,当遇到类似的场景,我们可以快速地参考设计模式实现代码。不仅可以加速我们的编码速度,也提升了代码的可扩展性、可重用性与可维护性!

作用

  1. 帮助我们快速解决常见问题:设计模式提供了解决特定软件设计问题的通用方法,拿来套上即用,例如单例模式、代理模式、责任链模式等等。
  2. 提升代码可扩展性:设计模式通常考虑了软件的扩展性,将不同的功能和功能变化分离开来实现,使得未来添加新功能更加容易。
  3. 提高代码可重用性:设计模式本身就是经验的总结,按照设计模式的思路,很多代码封装的很好,便于复用,减少重复工作。
  4. 提升代码可维护性:通过使用设计模式,使得代码结构更加清晰,易于理解和维护。
  5. 简化沟通成本:如果大家都熟悉设计模式,其实设计模式就是一种通用语言,通过设计就能明白其实现含义,有助于开发者之间更有效地沟通设计意图。
  6. 提供最佳实践:它们是经验的总结,可以指导开发者避免常见陷阱,采用最佳实践。

请解释什么是单例模式,并给出一个使用场景

单例模式(Singleton Pattern)在很多人眼里是最简单的一种设计模式,一个类只能有一个实例,这个类就是单例类,这就是单例模式。

它主要用于资源管理(避免资源冲突)、保证全局唯一的场景。

具体使用场景

1)配置管理:

基本上应用都会有一个全局配置,这个配置从理论上来说需要保证唯一性,确保读取到的配置是同一份,是一致的,所以天然适合单例实现。

2)连接池、线程池:

池化资源需要保证唯一性,不然就没有池化的意义了,总不能每次访问池化资源都新建一个吧?需要保持单例,控制具体池化资源的数量,便于管理和监控。

还有日志、缓存等需要全局唯一避免资源冲突的场景。

单例模式有哪几种实现?如何保证线程安全?

懒汉式

单例有很多种实现方式,最简单的可能就是懒汉式单例

public class Singleton {
private static Singleton instance;

// 私有构造函数,防止外部实例化
private Singleton() {}

public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

它的优点就是延迟加载,但是线程不安全,多线程并发访问的情况下可能会产生多个实例。

最简单的改进方法就是加个锁:

public class Singleton {
private static Singleton instance;

// 私有构造函数,防止外部实例化
private Singleton() {}

public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

但是这样一来,每次访问单例的时候,都需要有锁的竞争,可能会有性能问题。

因此又改进得到一个双重检查锁定版本的单例实现:

public class Singleton {
private static volatile Singleton instance;

// 私有构造函数,防止外部实例化
private Singleton() {}

public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

这个实现利用 volatile 关键字保证了可见性,利用双重检查机制减少了同步带来的性能损耗。

饿汉式

除了懒汉式的单例实现,还有个饿汉式单例实现:

public class Singleton {
private static final Singleton instance = new Singleton();

// 私有构造函数,防止外部实例化
private Singleton() {}

public static Singleton getInstance() {
return instance;
}
}

这种实现方式是线程安全的,硬要说它的缺点就是不使用的情况下,也因为静态属性的原因,导致类在加载的时候单例就创建了,即没有延迟加载。

如果要改进这个实现,可以采用静态内部类的方式:

public class Singleton {
private Singleton() {}

private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}

public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

这样不仅线程安全,也实现了延迟加载。

以上就是常见的单例实现方式。

工厂模式和抽象工厂模式有什么区别?

工厂模式

工厂方法模式定义了一个创建对象的接口,一个具体的工厂类负责生产一种产品,如果需要添加新的产品,仅需新增对应的具体工厂类而不需要修改原有的代码实现。

工厂方法的目的是使得创建对象和使用对象是分离的,并且客户端总是引用抽象工厂和抽象产品。工厂方法是指定义工厂接口和产品接口,但如何创建实际工厂和实际产品被推迟到子类实现,从而使调用方只和抽象工厂与抽象产品打交道。

我们来看个例子:

public interface PhoneFactory{
public void makePhone();
}

public class IPhone implement PhoneFactory{
public void makePhone(){
System.out.println("Make a best IPhone ");
}
}

public class Samsung implement PhoneFactory{
public void makePhone(){
System.out.println("Make a best Samsung ");
}
}

public class Huawei implement PhoneFactory{
public void makePhone(){
System.out.println("Make a best Huawei ");
}
}

public class Factory{
public static gotoPhone(String phone){
if(phone.equal("Iphone")){
return new IPhone();
}else if(phone.equal("Samsung")){
return new Samsung();
}else{
return new Huawei();
}
}
}

public class Boss{
public static void main(String[] args){
PhoneFactory factory = Factory.gotoPhone("Iphone");
factory.makePhone();
}
}

可以看到简单的工厂模式就是根据if判断语句进行区分的,这样客户端免除了直接创建产品对象的责任,而仅仅负责“消费”产品(正如手机厂老板所为)。加入现在需要加入中兴手机呢,下面我们从开闭原则(对扩展开放;对修改封闭)上来分析下简单工厂模式。如果需要加入中兴手机,那么就需要修改Factory中的gotoPhone(String),加入一个if分支,这样明显就是修改了,违背了开闭原则中的对修改封闭,同时也是因为程序中使用if判断语句进行分类导致的对扩展不够。所以现在我们使用工厂方法模式,直接去掉中间的Factory。

public interface PhoneFactory{
public void makePhone();
}

public class IPhoneFactory implement PhoneFactory{
public void makePhone(){
System.out.println("Make a best IPhone ");
}
}

public class SamsungFactory implement PhoneFactory{
public void makePhone(){
System.out.println("Make a best Samsung ");
}
}

public class HuaweiFactory implement PhoneFactory{
public void makePhone(){
System.out.println("Make a best Huawei ");
}
}
public class Boss{
public static void main(String[] args){
PhoneFactory factory = new IPhoneFactory();
factory.makePhone();
}
}

现在可以看出,还要增加中兴手机就不要修改代码了,直接就是可以实现一个中兴手机的PhoneFactory就可以了。刚好没有修改任何代码,直接就是增加一个实现类。如果哪天中兴手机代工厂关闭了,那么就有直接删掉中兴代工厂的类就好了,也不至于修改其它类。

抽象工厂模式

而抽象工厂提供一个创建一系列相关或相互依赖对象的接口,简单来说不是仅生产一个产品,而是一个系列产品。听起来可能有点抽象,我们还是看一下例子。

假设我们有两个产品 ProductA 和 ProductB,以及它们的具体实现 ConcreteProductA1、ConcreteProductA2、ConcreteProductB1、ConcreteProductB2,我们使用抽象工厂模式来创建这些相关的产品:

// 抽象产品A
public interface ProductA {
void use();
}

// 具体产品A1
public class ConcreteProductA1 implements ProductA {
@Override
public void use() {
System.out.println("Using ConcreteProductA1");
}
}

// 具体产品A2
public class ConcreteProductA2 implements ProductA {
@Override
public void use() {
System.out.println("Using ConcreteProductA2");
}
}

// 抽象产品B
public interface ProductB {
void eat();
}

// 具体产品B1
public class ConcreteProductB1 implements ProductB {
@Override
public void eat() {
System.out.println("Eating ConcreteProductB1");
}
}

// 具体产品B2
public class ConcreteProductB2 implements ProductB {
@Override
public void eat() {
System.out.println("Eating ConcreteProductB2");
}
}

// 抽象工厂
public interface AbstractFactory {
ProductA createProductA();
ProductB createProductB();
}

// 具体工厂1
public class ConcreteFactory1 implements AbstractFactory {
@Override
public ProductA createProductA() {
return new ConcreteProductA1();
}

@Override
public ProductB createProductB() {
return new ConcreteProductB1();
}
}

// 具体工厂2
public class ConcreteFactory2 implements AbstractFactory {
@Override
public ProductA createProductA() {
return new ConcreteProductA2();
}

@Override
public ProductB createProductB() {
return new ConcreteProductB2();
}
}

// 使用抽象工厂创建产品
public class Client {
public static void main(String[] args) {
AbstractFactory factory1 = new ConcreteFactory1();
ProductA productA1 = factory1.createProductA();
ProductB productB1 = factory1.createProductB();
productA1.use();
productB1.eat();

AbstractFactory factory2 = new ConcreteFactory2();
ProductA productA2 = factory2.createProductA();
ProductB productB2 = factory2.createProductB();
productA2.use();
productB2.eat();
}
}

代码虽然很多,但是看下来应该很清晰,抽象工厂就是打包式的创建系列对象,等于帮我们搭配好了,屏蔽关联对象的一些创建细节。

总结一下。

工厂模式:

  • 关注创建单一产品对象。
  • 使用子类来决定创建哪个具体产品。
  • 扩展性较好,新增产品时只需增加新的工厂子类。
  • 包含一个抽象产品接口,多个具体产品实现这个接口。
  • 有一个抽象工厂接口和多个具体工厂实现类,每个具体工厂负责创建一种具体产品。

抽象工厂模式:

  • 关注创建一系列相关或相互依赖的产品对象。
  • 提供一个接口,用于创建多个产品族的对象。
  • 增加新的产品族较为容易,但增加新产品类型较为困难。(比如要加个 createProductC,所有实现具体工厂的代码都得改)
  • 包含多个抽象产品接口,每个接口代表一类相关的产品。
  • 有一个抽象工厂接口,这个接口中包含多个创建不同抽象产品的方法。
  • 多个具体工厂实现类,每个具体工厂实现类负责创建一整套相关的具体产品,即实现抽象工厂接口中的所有创建方法,以提供一个完整的产品系列。

共同点

  1. 两种模式都通过封装对象的创建过程,将客户端代码与具体的实现类分离。

  2. 都使用工厂方法来创建对象,而不是直接使用new关键字。(将对象的创建过程封装起来)

  3. 都遵循”开闭原则”,增加新的产品时,两种模式均可以在不修改客户端代码的情况下,通过增加新的产品类来扩展系统。

  4. 都遵循”单一职责原则”,即工厂类负责对象的创建,客户端负责对象的使用。

不同点

  1. 工厂模式是创建单一的产品对象,即一个工厂负责创建一类产品。
    抽象工厂模式是创建产品族,即一个工厂负责创建一系列相关的产品对象。
  2. 工厂模式更加灵活,可以很容易地增加新的产品类型,但不能轻易地切换产品族。
    抽象工厂模式可以很容易地切换不同的产品族,但不能轻易地增加新的产品类型(因为这需要修改抽象工厂接口。)
  3. 工厂模式的客户端代码依赖于具体的工厂实现类。
    抽象工厂模式的客户端代码依赖于抽象工厂接口,而不依赖于具体的工厂实现类。
  4. 工厂模式通常只有一个抽象工厂接口和多个具体工厂实现类。
    抽象工厂模式通常有一个抽象工厂接口,以及多个具体工厂实现类,每个实现类创建一个产品族。

总结

当你需要创建一个对象,但不关心它的具体类型时,可以使用工厂模式。如果需要产品切换族,适合使用抽象工厂模式。

工厂模式适用于创建单一产品的场景,而抽象工厂模式适用于创建产品族的场景。

什么是模板方法模式?一般用在什么场景?

模板方法模式,它在一个抽象类中定义了一个算法(业务逻辑)的骨架,具体步骤的实现由子类提供,它通过将算法的不变部分放在抽象类中,可变部分放在子类中,达到代码复用和扩展的目的。

复用:所有子类可以直接复用父类提供的模板方法,即上面提到的不变的部分。 扩展:子类可以通过模板定义的一些扩展点就行不同的定制化实现。

abstract class AbstractClass {
// 模板方法
public final void templateMethod() {
primitiveOperation1();
primitiveOperation2();
hook();
}

// 基本操作(抽象方法)
protected abstract void primitiveOperation1();
protected abstract void primitiveOperation2();

// 钩子方法(可选的操作,提供默认实现)
protected void hook() {}
}


class ConcreteClassA extends AbstractClass {
@Override
protected void primitiveOperation1() {
System.out.println("ConcreteClassA: primitiveOperation1");
}

@Override
protected void primitiveOperation2() {
System.out.println("ConcreteClassA: primitiveOperation2");
}
}

class ConcreteClassB extends AbstractClass {
@Override
protected void primitiveOperation1() {
System.out.println("ConcreteClassB: primitiveOperation1");
}

@Override
protected void primitiveOperation2() {
System.out.println("ConcreteClassB: primitiveOperation2");
}

@Override
protected void hook() {
System.out.println("ConcreteClassB: hook");
}
}

上述代码定义了模板方法 templateMethod,内部定义了业务处理的逻辑,按序执行 primitiveOperation1()、primitiveOperation2() 和 hook()。

子类只需要关心几个方法的内部实现,不需要关心模板方法的内部执行顺序,这就是我们所说的将通用的算法步骤放在抽象类中,不同的实现细节放在子类中,在多个子类中共享相同的代码,而不需要在每个子类中重复实现相同的逻辑。

在 Java 中有很多应用场景,例如 JdbcTemplate 就是使用了模板方法来处理数据库的操作。

再比如 HttpServlet 类的 service 方法也用了模板方法,doGet、doPost 等方法都是需要子类实现的。

优点

  1. 提高复用性
  2. 提高扩展性
  3. 符合开闭原则

缺点

  1. 类数目增加
  2. 增加了系统实现的复杂度
  3. 继承关系自身的缺点,如果父类添加新的抽象方法,所有子类都要改一遍。

什么是策略模式?一般用在什么场景?

策略模式是一种行为型设计模式,它定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,让算法独立于使用它的客户端(调用方)而变化。

很多情况下,我们代码里有大量的 if else、switch 等,可以通过使用策略模式,避免大量条件语句的使用,实现算法的分离和独立变化。

它的主要目的是为了解耦多个策略,并方便调用方在针对不同场景灵活切换不同的策略。

// 策略的定义
public interface Strategy {
void execute(User user);
}

//具体策略
public class AStrategy implements Strategy{
void execute(User user) {
System.out.println("Executing Strategy A " + user.getName());
}
}
public class BStrategy implements Strategy{
void execute(User user) {
System.out.println("Executing Strategy B " + user.getName());
}
}


// 策略的创建
public class StrategyFactory {
private static final Map<String, Strategy> strategies = new HashMap<>();

static {
strategies.put("A", new AStrategy());
strategies.put("B", new BStrategy());
}

public static Strategy getStrategy(OrderType type) {
return strategies.get(type);
}
}

// 策略的使用
public class xxService {
public void execute(User user) {
String type = user.getType();
Strategy strategy = StrategyFactory.getStrategy(type);
return strategy.execute(user);
}
}

可以看到,上述的代码通过使用策略模式,可以将不同的算法策略封装起来,并根据 user 的类型在运行时动态选择具体的算法策略,从而提高系统的灵活性和可扩展性。

可以用在例如多种支付方式的切换、不同排序算法的切换等多策略实现的场景。

什么是责任链模式?一般用在什么场景?

责任链模式允许将多个对象连接成一条链,并且沿着这条链传递请求,让多个对象都有机会处理这个请求,请求会顺着链传递,直到某个对象处理它为止。

它主要避免了请求发送者和接受者之间的耦合,增强了系统的灵活性和可扩展性。

在很多场景都能看到责任链模式,比如日志的处理,不同级别不同输出。在Spring框架中,责任链设计模式通常被用于过滤器链、拦截器链等场景。

下面是一个简单的日志处理示例代码。日志处理器有三种类型:控制台日志处理器(ConsoleLogger)、文件日志处理器(FileLogger)和错误日志处理器(ErrorLogger)。日志处理器按照优先级进行处理,如果当前处理器不能处理,则将请求传递给下一个处理器。

// 责任链模式的抽象处理器类
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);
}
}

protected abstract 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 FileLogger extends Logger {
public FileLogger(int level) {
this.level = level;
}

@Override
protected void write(String message) {
System.out.println("File::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);
}
}

// 客户端代码
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, "info");
loggerChain.logMessage(Logger.DEBUG, "debug");
loggerChain.logMessage(Logger.ERROR, "error");
}
}

什么是观察者模式?一般用在什么场景?

观察者模式其实也称为发布订阅模式,它定义了对象之间的一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。当主题对象状态发生变化时,它会通知所有观察者对象。

它的目的就是将观察者和被观察者代码解耦,使得一个对象或者说事件的变更,让不同观察者可以有不同的处理,非常灵活,扩展性很强,是事件驱动编程的基础。

观察者模式的组成部分

  • Subject(主题/被观察者):状态发生变化时,通知所有注册的观察者。
  • Observer(观察者):接收来自主题的更新通知,并进行相应的操作。
  • ConcreteSubject(具体主题):实现具体的主题对象,保存需要被观察的状态。
  • ConcreteObserver(具体观察者):实现具体的观察者对象,更新自己以与主题的状态同步。

什么是装饰器模式?一般用在什么场景?

装饰器模式是一种结构型设计模式,用于在不改变现有类的情况下动态地为其增加新功能。通过将对象嵌套在装饰器中,可以实现功能扩展,同时保留原始对象的行为。

主要作用是给原始类增强功能,一般使用组合形式对原始类进行一定的扩展,并且可以将多个装饰器组合在一起,实现多个功能的叠加(多个装饰器组合需要装饰类需要和原始类实现同样接口或继承同样的抽象类)。

装饰器模式的特点

  1. 动态扩展:无需修改原始类的代码即可添加新功能。
  2. 遵循开闭原则:对扩展开放,对修改关闭。
  3. 灵活组合:装饰器可以嵌套组合,形成复杂的功能。

一般用在什么场景?

  1. 需要动态扩展功能: 对现有对象添加功能,但不希望通过继承方式。
  2. 不同组合的功能需求:功能可以按需组合,而不需要创建大量子类。
  3. 透明扩展:客户端无需了解对象是否被装饰过。

典型场景

  • 日志系统:在日志记录中动态添加时间戳、日志级别等信息。
  • 网络编程:为网络流添加缓存、压缩、加密功能。

例子

最典型的装饰器实现就是 Java 中的 I/O 类库,示例代码如下:

import java.io.*;

public class IOExample {
public static void main(String[] args) throws IOException {
File file = new File("test.txt");
FileInputStream fis = new FileInputStream(file);
BufferedInputStream bis = new BufferedInputStream(fis);
DataInputStream dis = new DataInputStream(bis);

while (dis.available() > 0) {
System.out.println(dis.readLine());
}

dis.close();
}
}

FileInputStream 用来读取文件流,然后被 BufferedInputStream 装饰,提供了缓存的功能

然后再被 DataInputStream 装饰,提供了按数据类型读取的功能

可以看到,多个装饰器叠加在一起,实现了多个功能的增强,且不会增加类的实现复杂度,每个装饰器仅需关注自己的加强功能即可,提高代码的灵活性和可维护性。

什么是享元模式?一般用在什么场景?

享元模式是一种结构型设计模式,用于减少程序中对象的数量,节约内存资源。通过共享具有相同状态的对象,来减少内存占用,提高内存效率

例如一个系统中可能会存在大量的重复对象,并且这些对象实际是不可变的,在设计上我们就可以仅在内存中保留一份实例,然后多处引用即可,这样就能节省内存的开销。

享元模式的特点

1)对象共享:通过共享对象来减少内存消耗。

2)状态分离

  • 内部状态:可以共享的状态,不随环境变化。
  • 外部状态:不能共享的状态,由客户端负责传递。

3)高效性:适合大量细粒度对象的场景,通过对象复用提升性能。

一般用在什么场景?

  1. 大量重复对象:系统中需要创建大量相似对象,而这些对象的状态大部分是可共享的。
  2. 内存优化:对象数量过多会导致内存开销过大,享元模式通过共享对象减少内存占用。
  3. 状态稳定:共享的对象需要具有较为稳定的内部状态。

典型场景

  • 文本编辑器中字符对象的复用。
  • 图形应用中重复绘制的形状或图元(如树、建筑)。
  • 数字常量缓冲池。

例子

在 Integer 中就采用了享元模式,即 Integer 中缓冲池。

很多人都遇到过一个面试题,即 Integer -128 到 127 之内的相等,而超过这个范围用 == 就不对了,因为这个范围内采用了享元模式,本质就是同一个对象,所以用 == 判断当然一致

原理就是 Integer 内部有个 IntegerCache ,在静态块中会初始化好缓存值:

在相关 Integer 操作的时候,会判断它的值是否在 IntegerCache 内,如果是则直接范围 IntegerCache 内已经初始化好的值,比如 valueOf 接口:

jdk 之所以要这样实现,是因为根据实践发现大部分的数据操作都集中在值比较小的范围,所以采用了享元模式,给 Integer 搞了个缓存池,默认范围是 -128 到 127。延伸下,可根据设置 JVM-XX:AutoBoxCacheMax=<size> 来修改缓存的最大值,最小值改不了。

参考链接

人人都能看懂的工厂方法模式和抽象工厂模式的区别-CSDN博客