重生之我在Java学斩神。

设计模式共有23种,分为创建型、结构型和行为型。此处仅仅介绍常见的八种设计模式:单例模式、工厂模式、抽象工厂模式、建造者模式、策略模式、装饰器模式、适配器模式和观察者模式

设计模式原则

单一职责原则

一个对象只包含单一的职责,并且该职责被完整地封装在一个类中。


1
2
3
4
5
6
7
8
9
10
11
12
13
public class Person {  
public void teach() {
System.out.println("我可以教学生学习");
}

public void takeaway() {
System.out.println("我要去送外卖");
}

public void screw() {
System.out.println("我在大厂打螺丝");
}
}

上述 Person 类中,包含不同的功能太多,根据单一职责原则,我们应该进行更详细的划分,保证一个对象只包含一种功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Teacher {  
public void teach() {
System.out.println("我可以教学生学习");
}
}

class TakeawayClerk {
public void takeaway() {
System.out.println("我要去送外卖");
}
}

class ScrewWorker {
public void screw() {
System.out.println("我在大厂打螺丝");
}
}

开闭原则

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


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface Teacher {  
void teach();
}

class ChineseTeacher implements Teacher {
@Override
public void teach() {
System.out.println("我是语文老师,我可以教学生中文");
}
}
class MathTeacher implements Teacher {
@Override
public void teach() {
System.out.println("我是数学老师,我可以教学生数学");
}
}

class EnglishTeacher implements Teacher {
@Override
public void teach() {
System.out.println("我是英语老师,我可以教学生英语");
}
}

不同的老师要做的工作都是教学生,但具体怎么教由各科老师决定。因此可以将 teach 抽象为一个接口或抽象类,让不同的 Teacher 都实现这个接口,并自由决定自己如何对学生进行教学。这就是对扩展开放。而每个 Teacher 具体干什么都是自己在负责,不会被其他 Teacher 干涉。这就是对修改关闭

里氏代换原则

子类可以扩展父类的功能,但不能改变父类原有的功能。任何父类可以出现的地方,子类一定可以出现。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Coder {  
public void code() {
System.out.println("我会写代码");
}


class JavaCoder extends Coder {
public void game() {
System.out.println("我会玩游戏");
}
@Override
public void code() {
System.out.println("我不会代码");
}
}
}

子类可以实现父类的方法,但不能覆盖父类的方法。 上述子类 JavaCoder 对父类 Coder 的 code 方法进行了重写,不具备父类原来的行为,因此不满足里氏代换原则。
子类可以扩展自己特有的方法。 上述子类扩展了属于自己的 game 方法,如果不对 code 方法进行重写,那么符合里氏代换原则。

依赖倒转原则

顶层模块不依赖底层模块,都依赖抽象。抽象不依赖细节,细节应该依赖于抽象。总结后就是对接口编程,依赖于抽象而不依赖于具体。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class UserMapper {  
// 基础CRUD
...
}

class UserService {
// 实现业务代码
UserMapper userMapper = new UserMapper();
...
}

class UserController {
// 使用业务代码
UserService userService = new UserService();
...
}

现有上述实现业务的模块。我们发现,假设对 User 的相关数据库操作更换为新的实现,我们不仅仅需要更改 UserService,还需要更改 UserController。上面的模块具有很高的关联性,虽然逻辑清晰,但底层模块一旦修改,大量的顶层模块都会因此而改动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface UserMapper {  
// 定义CRUD
}

class UserMapperImpl implements UserMapper {
// 实现CRUD
}


interface UserService {
// 定义业务代码
}

class UserServiceImpl implements UserService {
// 实现业务代码
@Resource
private UserMapper userMapper;
}

class UserController {
// 使用业务代码
@Resource
private UserService userService;
}

通过接口弱化原来的关联,我们只需要知道接口中定义了什么方法然后去使用即可,而具体的操作由接口的实现类来完成。即使底层模块被修改,顶层模块只需要使用底层模块接口中的方法即可,不需要修改具体的代码。

接口隔离原则

使用多个隔离的接口,而不是使用不需要的单个接口。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
interface Device {  
String cpu();
String gpu();
String type();
}

class Computer implements Device {

@Override
public String cpu() {
return "AMD 9950X3D";
}

@Override
public String gpu() {
return "Geforce RTX 5090";
}

@Override
public String type() {
return "计算机";
}
}


class Earphone implements Device {
@Override
public String cpu() {
return null;
}

@Override
public String gpu() {
return null;
}

@Override
public String type() {
return "耳机";
}
}

上述我们可以发现,虽然 Computer 和 Earphone 都属于 Device,但是 Device 中有很多 Earphone不需要实现的方法,因此我们应该对上述接口再进行细分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
interface BaseDevice {  
String type();
}

interface SmartDevice {
String cpu();
String gpu();
}

class Computer implements SmartDevice, BaseDevice {

@Override
public String type() {
return "计算机";
}

@Override
public String cpu() {
return "AMD Threadripper 5995WX";
}

@Override
public String gpu() {
return "NVIDIA A200";
}
}

class Earphone implements BaseDevice {
@Override
public String type() {
return "耳机";
}
}

通过设计多个接口,让类根据需要去实现需要的接口,不仅能够降低耦合度,还能更好的进行维护。

合成复用原则

优先使用合成,而不是继承。


1
2
3
4
5
6
7
8
9
10
11
12
class Mother {  
public void eat() {
System.out.println("喝粥");
}
}

class Baby extends Mother {
public void needEat() {
System.out.println("看见妈妈吃我也想吃");
eat();
}
}

如果未来 eat 不再由 Mother 负责,而是由 Father 负责,那么我们就需要在 Baby 中对所有复用 Mother 方法的地方进行修改。并且由于是继承,会将父类中的实现细节暴露给子类,这是不安全的。
我们可以采用如下方式:

1
2
3
4
5
6
7
8
9
10
11
12
class Mother {  
public void eat() {
System.out.println("吃饭");
}
}

class Baby {
public void needEat(Mother mother) {
System.out.println("宝宝需要吃饭");
mother.eat();
}
}

在 Baby 需要的时候去使用 Mother 中的方法,可以很大的降低类和类之间的耦合度。如果使用接口的话,将会更加灵活。

迪米特法则

一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。


类与类或者模块与模块之间,交互越少越好。(还是降低耦合度)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Goods {  
private double price;

public Goods(double price) {
this.price = price;
}

public double getPrice() {
return price;
}
}

class Customer {
public void purchase(Goods goods) {
System.out.println("Purchasing " + goods.getPrice());
}
}

public class Person {
public static void main(String[] args) {
Customer customer = new Customer();
customer.purchase(new Goods(12.99));
}
}

上述设计没有问题,但是违背了迪米特法则。因为 purchase() 方法仅仅只需要 price,而不是整个 Goods 类,我们只需要将 price 传过去即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...

class Customer {
public void purchase(double price) {
System.out.println("Purchasing " + price);
}
}

public class Person {
public static void main(String[] args) {
Customer customer = new Customer();
customer.purchase(new Goods(12.99).getPrice());
}
}

八大设计模式

单例模式(Singleton)

单例模式是Java中最常见的设计模式。单例模式仅有一个实例化对象,并提供一个全局访问点。单例模式的核心是将类的构造方法私有化,以防止外部直接通过构造函数创建实例。同时,类内部需要提供一个静态方法或变量来获取该类的唯一实例。
单例模式分为饿汉式和懒汉式。饿汉式在一开始就创建好了实例化对象,懒汉式在需要获取实例化对象时才进行创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 饿汉式
class Singleton {
private static Singleton instance = new Singleton();

private Singleton() { }
public static Singleton getInstance() {
return instance;
}
}
// 懒汉式
class Singleton {
private static Singleton instance = null;

private Singleton() { }

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

由于懒汉式在方法内部进行实例化对象,因此其在多线程中并不安全,使用synchronized关键字又会导致在高并发下效率低。我们可以利用静态内部类的特性实现懒汉式。

1
2
3
4
5
6
7
8
9
10
11
class Singleton {  
private Singleton() { }

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

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

如果我们仅仅使用 Singleton 类而没有使用 SingletonHolder 类,那么 SingletonHolder 类便不会被初始化。只有在真正使用它时,例如上述的 getInstance,才会进行类初始化。

工厂模式(Factory)

简单工厂模式

我们一般使用new去创建对象。当我们的程序中大量使用这种方案创建对象时,如果该对象的构造方法或者类名发生了修改,那我们每一处使用了该对象的地方都需要修改。因此我们可以将频繁使用的对象封装为一个工厂类,当我们需要对象时,直接调用工厂类中的方法来为我们生成对象。如果某个类出现了变动,我们也只需要修改工厂中的代码,而不是大面积地进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 产品抽象类
abstract class Product {
private String name;

public Product(String name) {
this.name = name;
}
}
// 产品A
class ProductA extends Product {
public ProductA() {
super("ProductA");
}
}
// 产品B
class ProductB extends Product {
public ProductB() {
super("ProductB");
}
}

// 产品工厂
public class Factory {
public static Product createProduct(String name) {
switch (name){
case "ProductA":
return new ProductA();
case "ProductB":
return new ProductB();
default:
return null;
}
}}

上面定义了一个产品工厂,现在我们可以直接向 Factory 索取对象:

1
Product product = Factory.createProduct("ProductA");

普通工厂模式

但是我们能够发现,如果此时新增一个 ProductC,就需要去修改 Factory 中的 createFactory 方法,很显然不符合开闭原则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class Factory<T extends Product> {  
abstract T getProduct();
}

public class FactoryA extends Factory<ProductA> {

@Override
ProductA getProduct() {
return new ProductA();
}
}

public class FactoryB extends Factory<ProductB> {
@Override
ProductB getProduct() {
return new ProductB();
}
}

我们可以利用泛型,利用子类确定产品类型。这样如果我们需要新增一个 ProductC,直接新建一个工厂 FactoryC 即可,不需要修改工厂类,符合开闭原则。工厂还屏蔽了对象的创建细节,我们只需要关注如何去使用对象即可。

抽象工厂模式(Abstract Factory)

如果产品类型有很多,例如 Food,按照上述普通工厂模式,就要新建大量的工厂。因此我们可以按照产品类型划分,将相同的产品类型划分到同一个工厂中。

1
2
3
4
5
6
7
8
9
public class FoodA { ... }
public class FoodB { ... }
public class FoodC { ... }

public abstract class AbstractFactory {
abstract FoodA getFoodA();
abstract FoodB getFoodB();
abstract FoodC getFoodC();
}

但是,如果某个产品类别新增了产品,就需要对产品工厂新增生产方法。因此,抽象工厂模式违背了开闭原则。 但一个工厂可以生产同一个产品类的所有产品,按类别进行分类,显然比之前的普通厂模式更好。

建造者模式(Builder)

建造者模式也是非常常见的设计模式。例如,Java中的 StringBuilder就是建造者模式。在使用过 StringBuilder 之后,我们可以发现,建造者模式就是不断配置参数或内容,直到配置完成后,再进行对象的构建。

1
2
3
4
5
6
7
8
9
10
11
class Employee {  
int id;
String name;
int managerId;
Date hiredate;
double salary;
int departmentId;
String departmentName;

public Employee(int id, String name, int managerId, Date hiredate, double salary, int departmentId, String departmentName) { }
}

如果我们的类属性非常多,直接通过 new 的方式去填充参数就会非常麻烦。我们可以用建造者模式去修改这个类,变成逐步填充参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Employee {  
// 私有构造函数
private Employee(int id, String name, int managerId, Date hiredate, double salary, int departmentId, String departmentName) { }

public static EmployeeBuilder builder() {
return new EmployeeBuilder();
}

public static class EmployeeBuilder {
private int id;
private String name;
private int managerId;
private Date hiredate;
private double salary;
private int departmentId;
private String departmentName;
// 对属性赋值,并返回自身,形成链式调用
public EmployeeBuilder id(int id) {
this.id = id;
return this;
}
public EmployeeBuilder name(String name) {
this.name = name;
return this;
}

...

// 最后调用建造者提供的 build 方法完成对象的构建
public Employee build() {
return new Employee(id, name, managerId, hiredate, salary, departmentId, departmentName);
}
}
}

现在我们就可以使用建造者模式构建这个对象了:

1
2
3
4
5
Employee employee = Employee.builder()  
.id(1)
.name("John")
. ...
.build();

策略模式(Strategy)

策略模式是我们手动设定一种策略,这样对象之后的行为将由我们所指定的策略而决定。
策略模式主要由三部分组成:策略接口、策略实现类和策略上下文类。策略接口定义了某种方法的统一接口,策略实现类负责实现策略接口中定义的方法,而策略上下文类持有一个策略接口的引用,负责调用具体策略类中的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 策略接口
interface Strategy {
void execute();
}
// 策略实现
class ConcreteStrategyA implements Strategy {
@Override
public void execute() {
System.out.println("Executing Strategy A");
}
}
// 策略实现
class ConcreteStrategyB implements Strategy {
@Override
public void execute() {
System.out.println("Executing Strategy B");
}
}
// 策略上下文
class Context {
private Strategy strategy;

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

定义好上述策略模式后,我们就可以手动设置策略,确定要执行哪一个类的 execute 方法了。

1
2
3
4
5
Context context = new Context(new ConcreteStrategyA());  
context.executeStrategy();
// ----------
Context context = new Context(new ConcreteStrategyB());
context.executeStrategy();

装饰器模式(Decorator)

装饰器模式用于向一个对象添加额外的行为而不需要修改原来的类,并且通过组合的形式完成,而不是传统的继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
interface Service {  
// 业务方法
void test();
}

class ServiceImpl implements Service {
// 业务方法的实现
@Override
public void test() { }
}

class Decorator implements Service {
private Service service;

public Decorator(Service service) {
this.service = service;
}

@Override
public void test() {
// 调用被装饰对象的业务方法
service.test();
}
}

class DecoratorImpl extends Decorator {
public DecoratorImpl(Service service) {
super(service);
}

@Override
public void test() {
System.out.println("装饰方法,在原有功能之前");
super.test();
System.out.println("装饰方法,在原有功能之后");
}
}

定义好装饰器类后,我们就可以对原有的业务方法进行装饰:

1
2
3
4
Service service = new ServiceImpl();  
Decorator decorator = new DecoratorImpl(service);
// service.test();
decorator.test();

适配器模式(Adapter)

现实中常见的适配器有:设备接口不适配时使用接口扩展坞来转化为适配的接口,电源适配器将高电压交流电转换成设备需要的低电压直流电。在程序中,适配器模式用于将一个已有类的接口转换成需要的接口形式,适配器让一个类的接口与另一个类的接口相适应。
适配器分为类适配器和对象适配器。

类适配器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface Fruit {  
void eat();
}

class Apple {
public void eat() {
System.out.println("吃苹果");
}
}

public class Test {
public static void main(String[] args) {
Apple apple = new Apple();
test(???); // 不适用于 Apple
}

public static void test(Fruit fruit) {
fruit.eat();
}
}

现有一个 test 方法需要 Fruit 类型的对象,但我们的 Apple 显然不适用,因此就需要一个类适配器,让 Apple 能够适用于 test 方法,并且能够成功执行 Apple 中的 eat 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Adapter extends Apple implements Fruit {  
@Override
public void eat() {
super.eat();
}
}

public class Test {
public static void main(String[] args) {
Fruit fruit = new Adapter();
test(fruit);
}

public static void test(Fruit fruit) {
fruit.eat();
}
}

对象适配器

类适配器中占用了继承的位置,如果 Fruit 不是接口而是抽象类的话,由于Java不支持多继承,那么上述方法将不再适用,根据合成复用原则,我们应该优先使用合成的方式,而不是继承。因此我们可以使用对象适配器:

1
2
3
4
5
6
7
8
9
10
11
12
class Adapter implements Fruit {  
private Apple apple;

public Adapter(Apple apple) {
this.apple = apple;
}

@Override
public void eat() {
apple.eat();
}
}

我们将对象以组合的形式存放在 Adapter 中,这样就能通过存放的对象完成具体实现。

观察者模式(Observer)

在观察者模式中,当一个对象状态改变时,所有依赖它的对象都能收到通知并自动更新。
观察者模式有两种对象:观察者和被观察者(称之为主题)。观察者在主题中被注册,在主题的状态发生改变时能够接收通知。主题会维护一组观察者,并在自身状态改变时给观察者发送通知。主题通过调用观察者的统一接口来通知它们状态的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 观察者接口  
interface Observer {
void update();
}

// 具体观察者A
class ConcreteObserverA implements Observer {
@Override
public void update() {

}
}

// 具体观察者B
class ConcreteObserverB implements Observer {
@Override
public void update() {

}
}

// 主题接口
interface Subject {
// 注册观察者
void registerObserver(Observer observer);
// 移除观察者
void removeObserver(Observer observer);
// 通知观察者
void notifyObserver();
}

// 具体主题
class ConcreteSubject implements Subject {
Set<Observer> observerSet = new HashSet<>();
@Override
public void registerObserver(Observer observer) {
observerSet.add(observer);
}

@Override
public void removeObserver(Observer observer) {
observerSet.remove(observer);
}

@Override
public void notifyObserver() {
for (Observer observer : observerSet) {
observer.update();
}
}
}

这样我们就实现了一个简单的观察者模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test {  
public static void main(String[] args) {
// 创建主题
ConcreteSubject subject = new ConcreteSubject();
// 创建具体观察者
ConcreteObserverA observerA = new ConcreteObserverA();
ConcreteObserverB observerB = new ConcreteObserverB();
// 注册观察者
subject.registerObserver(observerA);
subject.registerObserver(observerB);
// 状态发生改变时通知观察者
subject.notifyObserver();
}
}