重生之我在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 { ... } 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 { } class UserMapperImpl implements UserMapper { } 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; } } class ProductA extends Product { public ProductA () { super ("ProductA" ); } } 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 ; } ... 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); 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(???); } 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 () ; } class ConcreteObserverA implements Observer { @Override public void update () { } } 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(); } }