重构获得模式 Refactoring to Patterns
面向对象设计模式是“好的面向对象设计”,所谓“好的面向对象设计”指的是那些可以满足“应对变化,提高复用”的设计。
现代软件设计的特征是“需求的频繁变化”。设计模式的要点是“寻找变化点,然后在变化点处应用设计模式,从而更好地应对需求的变化”。“什么时候、什么地点应用设计模式”比“理解设计模式结构本身”更为重要。
设计模式的应用不宜先入为主,一上来就使用设计模式是对设计模式的最大误用。没有一步到位的设计模式。敏捷软件开发倡导的“Refactoring to Patterns”是目前普遍公认的最好的获得设计模式的方法。
软件设计的复杂性
设计模式的原则
封装变化角度对设计模式的分类
组件协作:
Template Method
Strategy
Observer / Event
单一职责:
Decorator
Bridge
对象创建:
Factory Method
Abstract Factory
Prototype
Builder
对象性能:
Singleton
Flyweight
接口隔离:
Façade
Proxy
Mediator
Adapter
状态变化:
Memento
State
数据结构:
Composite
Iterator
Chain of Responsibility
行为变化:
Command
Visitor
领域对象:
Interpreter
重构的关键技法
>静态 → 动态
>早绑定 → 晚绑定
>继承 → 组合
>编译时依赖 → 运行时依赖
>紧耦合 → 松耦合
学完设计模式,其实你会发现这八个点其实只是在不同的侧面来说明同一个问题。
“组件协作”模式:
现代软件专业分工之后的第一个结果是“框架与应用程序的划分”,“组件协作”模式通过晚期绑定,来实现框架与应用间的松耦合,是二者之间协作时常用的模式。>典型模式Template,MethodStrategy,Observer/Event
Template Method
动机(Motivation)
- 在软件构建过程中,对于某一项任务,它常常有稳定的结构,但各个子步骤却有很多改变的需求,或者由于固有原因(比如框架与应用之间的关系)而无法和任务的整体结构同时实现
- 如何在确定稳定操作结构的前提下,来灵活应对各个子步骤化或者晚期实现需求 ?
要点总结
- Template Method模式是一种非常基础性的设计模式象系统中有着大量的应用。用最简洁的机制(接口方法)为很多应用程序框架提供了灵活的扩展点,是代码复用实现的基本实现结构。
- 除了可以灵活应对子步骤的变化外“不要调用我,让我来调用你”的反向控制结构是Template Method的典型应用。
- 在具体实现方面,被Template Method调用的方法可以实现,也可以没有任何实现(抽象方法),但它们设置为protected方法。
Strategy
动机(Motivation)
- 在软件构建过程中,某些对象使用的算法可能多种多样,经常改,如果将这些算法都编码到对象中,将会使对象变得异常复杂变而且有时候支持不使用的算法也是一个性能负担。
- 如何在运行时根据需要透明地更改对象的算法?将算法与对象本身解耦,从而避免上述问题 ?
1. 定义策略接口
首先,我们定义一个策略接口 PaymentStrategy
,这是所有支付策略的共同接口。
// PaymentStrategy.java
public interface PaymentStrategy {
void pay(int amount);
}
2. 实现具体策略
接着,我们创建几个实现了 PaymentStrategy
接口的具体策略类:
// CreditCardPayment.java
public class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
private String cardHolderName;
public CreditCardPayment(String cardNumber, String cardHolderName) {
this.cardNumber = cardNumber;
this.cardHolderName = cardHolderName;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid using Credit Card.");
}
}
// PayPalPayment.java
public class PayPalPayment implements PaymentStrategy {
private String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid using PayPal.");
}
}
3. 创建上下文类
上下文类 ShoppingCart
使用一个策略来进行支付。策略对象在运行时可以替换,因此 ShoppingCart
可以动态地改变它的支付行为。
// ShoppingCart.java
public class ShoppingCart {
private PaymentStrategy paymentStrategy;
public ShoppingCart(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
4. 使用策略模式
最后,我们来看一下如何使用策略模式:
public class StrategyPatternDemo {
public static void main(String[] args) {
// 使用信用卡支付
PaymentStrategy creditCardPayment = new CreditCardPayment("1234 5678 9012 3456", "John Doe");
ShoppingCart cart = new ShoppingCart(creditCardPayment);
cart.checkout(100); // 输出: "100 paid using Credit Card."
// 切换到使用PayPal支付
PaymentStrategy payPalPayment = new PayPalPayment("john@example.com");
cart.setPaymentStrategy(payPalPayment);
cart.checkout(200); // 输出: "200 paid using PayPal."
}
}
5. 运行输出
100 paid using Credit Card. 200 paid using PayPal.
解释
- 策略接口(
PaymentStrategy
):定义了一个支付策略的接口。 - 具体策略(
CreditCardPayment
和PayPalPayment
):实现了策略接口,提供了不同的支付算法。 - 上下文类(
ShoppingCart
):使用策略接口,客户端可以动态地更改上下文类的策略。
这种设计模式的优点是可以轻松地扩展新的策略而不改变现有代码,这非常符合“开闭原则”(Open/Closed Principle)。
Observer
动机(Motivation )
- 在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系"---------一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密将使软件不能很好地抵御变化。
- 使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。
Observer 模式由两种对象构成:
- Subject(主题或被观察者):主题维护了一个观察者列表,当主题的状态发生变化时,负责通知所有注册的观察者。
- Observer(观察者):观察者订阅主题,并在主题状态变化时接收通知。
1. 定义 Observer
接口
包含一个更新方法,当主题状态变化时,主题调用这个方法来通知观察者。
// 观察者接口,定义了接收到更新通知的方法
interface Observer {
void update(float temperature);
}
2. 定义 Subject
接口
用于注册、移除观察者,以及在状态变化时通知观察者。
// 主题接口,定义了注册、移除观察者,以及通知观察者的方法
interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}
3. 实现具体的 Subject
类
管理观察者列表,并在状态变化时调用通知方法。
import java.util.ArrayList;
import java.util.List;
// 具体的主题类实现 Subject 接口
class WeatherStation implements Subject {
private List<Observer> observers;
private float temperature;
public WeatherStation() {
observers = new ArrayList<>(); // 初始化观察者列表
}
@Override
public void registerObserver(Observer observer) {
observers.add(observer); // 添加新的观察者
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer); // 移除观察者
}
@Override
public void notifyObservers() {
for (Observer observer : observers) { // 通知所有观察者
observer.update(temperature);
}
}
// 更新温度,并通知所有观察者
public void setTemperature(float temperature) {
this.temperature = temperature;
notifyObservers(); // 通知所有观察者温度已更新
}
}
4. 实现具体的 Observer
类
实现 Observer 接口,并定义在接收到主题通知时的行为。
// 具体的观察者类实现 Observer 接口
class TemperatureDisplay implements Observer {
private float temperature;
@Override
public void update(float temperature) {
this.temperature = temperature;
display(); // 更新后显示温度
}
public void display() {
System.out.println("当前温度: " + temperature + "°C");
}
}
5. 使用策略模式
public class Main {
public static void main(String[] args) {
WeatherStation weatherStation = new WeatherStation(); // 创建一个天气站
// 创建两个温度显示器(观察者)
TemperatureDisplay display1 = new TemperatureDisplay();
TemperatureDisplay display2 = new TemperatureDisplay();
// 将两个观察者注册到天气站
weatherStation.registerObserver(display1);
weatherStation.registerObserver(display2);
// 更新温度,通知所有观察者
weatherStation.setTemperature(25.3f); // 输出两次温度变化通知
weatherStation.setTemperature(30.2f);
// 移除一个观察者
weatherStation.removeObserver(display1);
// 再次更新温度,通知剩余的观察者
weatherStation.setTemperature(28.4f); // 只输出一次温度变化通知
}
}
通过这种实现,WeatherStation
的状态变化会自动通知所有注册的TemperatureDisplay
,展示了Observer模式的典型用法。这个模式解耦了主题和观察者,使它们可以独立地改变而不会相互影响。
“单一职责”模式:
在软件组件的设计中,如果责任划分的不清晰,使用继承得到的结果往往是随着需求的变化,子类急剧膨胀,同时充斥着重复代码,这时候的关键是划清责任。
典型模式
- Decorator
- Bridge
装饰模式
动机(Motivation)
- 在某些情况下我们可能会“过度地使用继承来扩展对象的功能”,由于继承为类型引入的静态特质,使得这种扩展方式缺乏灵活性;并且随着子类的增多(扩展功能的增多),各种子类的组合(扩展功能的组合)会导致更多子类的膨胀。
- 如何使“对象功能的扩展”能够根据需要来动态地实现?同时避免“扩展功能的增多”带来的子类膨胀问题?从而使得任何“功能扩展变化”所导致的影响将为最低?
对于文件的读取,可能有不同类型的流,如果按照继承的方式,子类会爆炸
按照这个典型的子类,其中read()方法在各个子类都有。出现大量的重复代码
class CryptoBufferedFileStream : public FileStream {
public:
virtual char Read(int number) {
// 额外的加密操作...
// 额外的缓冲操作...
FileStream::Read(number); // 读文件流
}
virtual void Seek(int position) {
// 额外的加密操作...
// 额外的缓冲操作...
FileStream::Seek(position); // 定位文件流
}
virtual void Write(byte data) {
// 额外的加密操作...
// 额外的缓冲操作...
FileStream::Write(data); // 写文件流
}
};
按照设计原则,“优先使用组合,不是继承”,进行优化
发现57行和79行,当变量都是某个类型的子类的时候,就可以将其声明成某个类型,此处就是Stream。依次类推,这里改成了,编译时一样,运行时不一样(用多态来应对变化)
class CryptoNetworkStream {
Stream* stream; // = new NetworkStream();
public:
virtual char Read(int number) {
// 额外的加密操作...
stream->Read(number); // 读网络流
}
};
进一步优化,将统一的类型,提取到一个公共类,变成装饰类
// 扩展操作
class DecoratorStream : public Stream {
protected:
Stream* stream; // 指向被装饰的流
};
class CryptoStream : public DecoratorStream {
public:
CryptoStream(Stream* stm) : DecoratorStream(stm) {
// 构造函数实现
}
};
模式定义
动态(组合)地给一个对象增加一些额外的职责。就增加功能而言,Decorator模式比生成子类(继承)更为灵活(消除重复代码 &减少子类个数)。
《设计模式》
Bridge
动机(Motivation)
- 由于某些类型的固有的实现逻辑,使得它们具有两个变化的维度乃至多个纬度的变化。
- 如何应对这种“多维度的变化”?如何利用面向对象技术来使得类型可以轻松地沿着两个乃至多个方向变化,而不引入额外的复杂度?
对于平台实现和业务抽象,两个不同方向,分别都有不同的实现,就会出现多个子类,分别去override playground...以及Login,结构相似,出现大量的重复代码,按照设计原则,“优先使用组合,不是继承”,进行优化
然后,两个类里面,当变量都是某个类型的子类的时候,就可以将其声明成某个类型,此处就是messager。依次类推,这里改成了,编译时一样,运行时不一样(用多态来应对变化)
改到这里,你会发现这两个类已经是一样的了(编译时一样),消除同样的子类
接着,我们发现对于messager里面的方法,并不是每一个子类都会去override所有的方法。所以,我们根据业务,将messager的方法进行拆分。
然后子类根据需要进行,将继承转组合
更进一步,我们发现,messagerImpl是在每一个被使用的地方都有,这个也是重复代码,如此,将其提取到上一级。
模式定义
将抽象部分(业务功能)与实现部分(平台实现)分离,使它们都可以独立地变化。
《设计模式》
“对象创建”模式
通过“对象创建”模式绕开new,来避免对象创建(new)过程中所导致的紧耦合(依赖具体类)从而支持对象创建的稳定。它是接口抽象之后的第一步工作。
典型模式
- Factory Method
- Abstract Factory
- Prototype
- Builder
Factory Method
动机(Motivation)
- 在软件系统中,经常面临着创建对象的工作;由于需求的变化,需要创建的对象的具体类型经常变化。
- 如何应对这种变化?如何绕过常规的对象创建方法(new),提供一种“封装机制”来避免客户程序和这种“具体对象创建工作”的紧耦合
现在,我们来对一个拥有多个Splitter的场景进行设计。
首先第一步,在14行将Splitter的声明改成接口,而不是具体的类
接着发现15行,还是依赖了一个具体的类,思考,如何绕开这个new.
如何不直接new,而是通过方法返回一个
在调用的地方,是可以实现不依赖具体的类了,但是在方法里面依然是具体的,
继续优化,只有create方法是抽象方法,而不是具体的方法时,才真正的解决了具体依赖的问题
然后让具体的工厂类去继承Factory,将new方法延迟在子类实现
模式定义
定义一个用于创建对象的接口,让子类决定实例化哪一个类Factory Method使得一个类的实例化延迟(目的:解耦,手段:抽象方法)到子类。
要点总结
- Factory Method模式用于隔离类对象的使用者和具体类型之间的耦合关系。面对一个经常变化的具体类型,紧耦合关系(new)会导致软件的脆弱。
- Factory Method模式通过面向对象的手法,将所要创建的具体对象工作延迟到子类,从而实现一种扩展(而非更改)的策略,较好地解决了这种紧耦合关系。
- Factory Method模式解决“单个对象”的需求变化。缺点在于要求创建方法/参数相同。
抽象工厂模式
动机(Motivation)
- 在软件系统中,经常面临着“一系列相互依赖的对象”的创建工作;同时,由于需求的变化,往往存在更多系列对象的创建工作。
- 如何应对这种变化?如何绕过常规的对象创建方法(new),提供一种“封装机制"来避免客户程序和这种"多系列具体对象创建工作”的紧耦合?
模式定义
提供一个接口,让该接口负责创建一系列“相关或者相互依赖的对象”,无需指定它们具体的类。
abstract class IDBFactory {
abstract IConnection createConnection();
abstract IDBReader createDBReader();
}
对于数据库来说,connection和DBReader等都是相互依赖的,不能设置connect是mysql而reader是oracle。这里的方法是一个整体的
要点总结
- 如果没有应对“多系列对象构建”的需求变化,则没有必要使用Abstract Factory模式,这时候使用简单的工厂完全可以。
- “系列对象”指的是在某一特定系列下的对象之间有相互依赖、或作用的关系。不同系列的对象之间不能相互依赖。
- Abstract Factory模式主要在于应对“新系列”的需求变动。其缺点在于难以应对“新对象”的需求变动。
原型模式
动机(Motivation)
- 避免昂贵的对象创建:如果创建一个对象的成本很高(如复杂初始化或资源消耗大),可以通过原型模式复制已有对象来提高效率。
- 克隆对象:Java 提供了
Cloneable
接口和clone()
方法来实现对象的浅拷贝。
1. 定义 Prototype
接口
通常,Prototype
是一个类,它实现了 Cloneable
接口并重写了 clone()
方法。
class Prototype implements Cloneable {
private String name;
public Prototype(String name) {
this.name = name;
}
// 重写 clone() 方法
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
2. 使用具体原型类
public class PrototypePatternDemo {
public static void main(String[] args) {
// 创建一个原型对象
Prototype original = new Prototype("Original");
try {
// 克隆对象
Prototype cloned = (Prototype) original.clone();
System.out.println("Original object name: " + original.getName());
System.out.println("Cloned object name: " + cloned.getName());
// 修改克隆对象的属性
cloned.setName("Cloned");
System.out.println("After modifying the cloned object:");
System.out.println("Original object name: " + original.getName());
System.out.println("Cloned object name: " + cloned.getName());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
原型模式的应用场景(实际上很少用):
- 需要重复创建相同对象,而且创建成本较高时,可以通过原型模式克隆现有对象来提高性能。
- 需要创建对象的不同状态,可以创建原型对象并克隆它们来生成不同状态(对这些副本进行不同的修改)的对象。
- 系统希望通过减少子类数量来实现灵活扩展,原型模式可以代替传统的子类继承。
缺点:
- 浅拷贝的局限性:浅拷贝无法处理复杂对象的深层数据,需要开发者手动处理深拷贝。
- 对
Cloneable
接口的依赖:Java 的Cloneable
机制比较基础,需要开发者自己实现复杂的克隆逻辑。
模式定义
使用原型实例指定创建对象的种类,然后通过拷贝这些原型来创建新的对象。
Builder模式
它允许我们一步一步创建复杂对象。与工厂模式不同,Builder模式更加关注构造对象的步骤顺序,尤其适合需要很多配置选项或需要复杂初始化的对象。
1. Builder模式的核心思想
Builder模式将对象的构建过程和表示分离,使得相同的构建过程可以创建不同的表示。通过将复杂对象的构建逻辑封装在一个Builder类中,客户端只需要指定对象的属性,而不用关心对象的创建细节。
class Computer {
private String CPU;
private String RAM;
private String storage;
private String GPU;
// 私有构造函数,避免直接使用构造函数创建对象
private Computer(ComputerBuilder builder) {
this.CPU = builder.CPU;
this.RAM = builder.RAM;
this.storage = builder.storage;
this.GPU = builder.GPU;
}
@Override
public String toString() {
return "Computer [CPU=" + CPU + ", RAM=" + RAM + ", Storage=" + storage + ", GPU=" + GPU + "]";
}
// 静态的 Builder 内部类
public static class ComputerBuilder {
private String CPU;
private String RAM;
private String storage;
private String GPU;
public ComputerBuilder(String CPU, String RAM) {
this.CPU = CPU;
this.RAM = RAM;
}
public ComputerBuilder setStorage(String storage) {
this.storage = storage;
return this;
}
public ComputerBuilder setGPU(String GPU) {
this.GPU = GPU;
return this;
}
// 构建 Computer 对象
public Computer build() {
return new Computer(this);
}
}
}
2. 适用场景
- 当创建对象时参数过多,或参数有很多可选项时,使用Builder模式可以避免构造函数过于复杂。
- 需要一步一步创建一个复杂对象,并且可以灵活控制各个步骤的顺序时。
对象性能模式
面向对象很好地解决了“抽象”的问题,但是必不可免地要付出一定的代价。对于通常情况来讲,面向对象的成本大都可以忽略不计。但是某些情况,面向对象所带来的成本必须谨慎处理。
典型模式
- Singleton
- Flyweight
1.单例模式
动机(Motivation)
- 在软件系统中,经常有这样一些特殊的类,必须保证它们在系统中只存在一个实例,才能确保它们的逻辑正确性、以及良好的效率。
- 如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例?
- 这应该是类设计者的责任,而不是使用者的责任。
实现单例模式的方式有懒汉模式,饿汉模式,在此不做介绍。这里推荐一个性能更高的实现机制
public class Singleton {
// 1. 私有化构造函数,防止外部创建实例
private Singleton() {}
// 2. 静态内部类,持有 Singleton 的唯一实例
private static class SingletonHolder {
// 静态变量,单例实例,只有在调用 getInstance() 方法时才会初始化
private static final Singleton INSTANCE = new Singleton();
}
// 3. 提供公共访问方法,返回内部类中的唯一实例
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
私有构造函数:
private Singleton()
防止外部通过new
操作符创建该类的实例。静态内部类(
SingletonHolder
):SingletonHolder
是一个私有静态内部类,它只在第一次调用getInstance()
时才会被加载和初始化。- 由于静态变量
INSTANCE
是在类加载时由 JVM 保证的,因此它具有线程安全性。
这种方式的关键在于静态内部类只在其被调用时才会加载,JVM 保证了类加载的线程安全性。
2.享元模式
动机(Motivation)
- 在软件系统采用纯粹对象方案的问题在于大量细粒度的对象会很快充斥在系统中,从而带来很高的运行时代价--主要指内存需求方面的代价。
- 如何在避免大量细粒度对象问题的同时,让外部客户程序仍然能够透明地使用面向对象的方式来进行操作?
模式定义
运用共享技术有效地支持大量细粒度的对象。
import java.util.HashMap;
import java.util.Map;
public class TreeFactory {
private static final Map<String, Tree> treeMap = new HashMap<>();
public static Tree getTree(String color) {
Tree tree = treeMap.get(color);
if (tree == null) {
tree = new ConcreteTree(color);
treeMap.put(color, tree);
}
return tree;
}
}
享元工厂:TreeFactory
类管理享元对象的创建和共享。它保证了相同颜色的树实例只创建一次,从而节省内存。
要点总结
- 面向对象很好地解决了抽象性的问题,但是作为一个运行在机器中的程序实体,我们需要考虑对象的代价问题。Flyweight主要解决面向对象的代价问题,一般不触及面向对象的抽象性问题。
- Flyweight采用对象共享的做法来降低系统中对象的个数,从而降低细粒度对象给系统带来的内存压力。在具体实现方面,要注意对象状态的处理。
- 对象的数量太大从而导致对象内存开销加大--什么样的数量才算大?这需要我们仔细的根据具体应用情况进行评估,而不能凭空臆断。
“接口隔离”模式
在组件构建过程中,某些接口之间直接的依赖常常会带来很多问题、甚至根本无法实现。采用添加一层间接(稳定)接口,来隔离本来互相紧密关联的接口是一种常见的解决方案。
典型模式
- Facade
- Proxy
- Adapter
- Mediator
Facade模式
动机(Motivation)
- 上述A方案的问题在于组件的客户和组件中各种复杂的子系统有了过多的耦合,随着外部客户程序和各子系统的演化,这种过多的耦合面临很多变化的挑战。
- 如何简化外部客户程序和系统间的交互接口?如何将外部客户程序的演化和内部子系统的变化之间的依赖相互解耦?
模式定义
子系统中的一组接口提供一个一致(稳定)的界面Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用(复用)。
要点总结
- 从客户程序的角度来看,Facade模式简化了整个组件系统的接口对于组件内部与外部客户程序来说,达到了一种“解耦”的效果-内部子系统的任何变化不会影响到Facade接口的变化。
- Facade设计模式更注重从架构的层次去看整个系统,而不是单个类的层次。Facade很多时候更是一种架构设计模式。
- Facade设计模式并非一个集装箱,可以任意地放进任何多个对象Façade模式中组件的内部应该是“相互耦合关系比较大的一系列组件”,而不是一个简单的功能集合。