图解设计模式

本贴最后更新于 2076 天前,其中的信息可能已经时移世易

暨上文介绍了PlantUML之后,谨以此文记录学习《图解设计模式》过程中的心得体会,欢迎留言一起交流想法~

Todo List

  • Adapt to Design Pattern——适应设计模式

    • Iterator 模式——一个一个遍历
    • Adaptor 模式——加个“适配器”以便复用
  • Left to subclass——交给子类

    • Template Method——将具体处理交给子类
    • Factory Method——将实例的生成交给子类
  • Generating Instance——生成实例

    • Singleton 模式——只有一个实例
    • Builder 模式——组装复杂的实例
    • Abstract Factory——将关联零件组装成产品
  • Consider Individualy——分开考虑

    • Bridge 模式——将类的功能层次结构与实现层次结构分离
    • Strategy 模式——整体地替换算法
  • Consistency——一致性

    • Composite 模式——容器与内容的一致性
    • Decorator 模式——装饰边框与被装饰物的一致性
  • Access Data Structure——访问数据结构

    • Visitor 模式——访问数据结构并处理数据
    • Chain of Responsibility 模式——推卸责任
  • Simplify——简单化

    • Facade 模式——简单窗口
    • Mediator 模式——只有一个仲裁者
  • Manage Status——管理状态

    • Observer 模式——发送状态变化通知
    • Memento 模式——保存对象状态
    • State 模式——用类表示状态
  • Avoid wasting

    • Flyweight 模式——共享对象,避免浪费
    • Proxy 模式——只在必要时生成对象
  • Represent with Class——用类来表现

    • Command 模式——命令也是类
    • Interpreter 模式——语法规则也是类

约定

由于每个人的行文风格不同,因此表情达意的方式也不同,在本文中有着如下约定:

  • 每个设计模式的示例代码中,Client 类都表示业务类,即脱离于设计模式之外的,将设计模式应用于业务代码的测试类。
  • 本文每个类图都是使用 IDEA 插件 PlantUML 所画,可参考《遇见 PantUML~》一文
  • 本文中的类通常指广义上的类,包含抽象类和接口
  • 本书中的所有实例代码完整版已托管到码云:https://gitee.com/zhenganwen/code-demo

Principle

通常,优良的代码设计需要遵循以下原则

Single Responsibility

每个类的存在应该都只是为了满足一个特定的需求,例如 Collection 类中的方法应该都是为了维护内部元素的结构组织而存在,而应该将如何遍历 Collection 中元素的职责交给 Iterator

单一职责保证专业的事交给专业的人来做,这样每个类发生修改的原因只会有一个(因为每个类的责任只有一个),这样就保证了后续若有需求变更只会导致负责解决该需求的类发生改变,而其他的类均不会受到影响。改变越少,系统发生 BUG 的几率就会越小。

Open/Closed Principle

系统要对修改关闭,对扩展开放。代码设计要尽量避免对现有代码的修改,因为一旦修改一处就可能导致依赖该类的其他类发生改变,一旦改变,就有可能引入新的潜在的 BUG。如果需求变更,代码设计应该通过新增类(多为实现类)的方式来满足新的需求,而客户端代码(依赖该类的其他类)应该无需修改或只需少量修改。

Liskov Substitution Principle

里氏代换原则依托于 OOP 的多态性。在运行时,客户端依赖的对象可被其他“同源”的(有相同的父类或接口)对象替换而客户端的调用逻辑不受任何影响,这要求我们在声明对象的外观类型(声明类型)时尽量选择高层次一些的类(类的层次结构)。

Interface Segregation Principle

接口隔离原则要求我们将接口方法按照接口功能分开定义在不同的接口中,例如 createItertor() 应该定义在 Iterable 中,fly() 应该定义在 Flyable 之中,这样能够减轻实现类的负担,避免实现类被捆绑着要求实现不必要的接口方法。

同时,Java8 之后,接口方法如果有通用实现应该定义为 default

Dependency Inversion Principle

依赖倒置原则要求我们尽量消除点对点的“强”依赖,而应该使两者依赖抽象,例如 Controller 依赖 AbstractService 中的抽象方法进行声明式编程,XxxServiceImpl 则对 AbstractService 的抽象方法进行实现,这样就实现了控制器和具体业务处理类之间的解耦,一旦后续业务变更,我们只需要新增一个 XxxServiceImpl2 并借助多态就能够轻松实现业务处理的切换。

Adapt to Design Pattern

本章将以 Iterator 模式和 Adaptor 模式两个较为简单的作为设计模式的入门,切身体会设计模式存在的价值和软件开发中应遵循的一些原则。

Iterator 模式

Before

Iterator 中译“迭代器”,起逐个访问集合中的元素的作用。在数据结构中组合元素的方式有很多,如数组、连表、哈希表、二叉树等,根据集合的不同的组织形式,我们遍历访问集合元素的方式也是不一样的。这时我们的业务代码中的访问逻辑(即遍历到当前元素时需要干什么)和遍历逻辑(需要知道集合内部结构)是耦合在一起的,一旦集合换一种组织形式,那么我们的业务代码也需要跟着改变。

例如,如下书架 BookShelf 通过数组的形式组织了一些书 Book

@Data @AllArgsConstructor @NoArgsConstructor public class Book { private String name; } public class BookShelfWithArr { private final Book[] books; private final int size; private int index; public BookShelfWithArr(int size) { this.size = size; this.books = new Book[size]; this.index = 0; } public void put(Book book) { if (book == null) { throw new IllegalArgumentException("book can't be null"); } if (index == size) { throw new RuntimeException("the bookshelf is full"); } books[index++] = book; } public Book get(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("index should be equal or big than 0 but less than " + size); } return books[index]; } public int size() { return this.size; } }

如果不使用 Iterator 模式,那么你的业务代码可能如下所示:

public class Client { public static void main(String[] args) { BookShelfWithArr bookShelf = new BookShelfWithArr(4); bookShelf.put(new Book("Java")); bookShelf.put(new Book("C")); bookShelf.put(new Book("php")); bookShelf.put(new Book("python")); for (int i = 0; i < bookShelf.size(); i++) { System.out.println(bookShelf.get(i)); } } }

这是我们初学 Java 集合章节时司空见惯的代码。现在我们来考虑一个问题,假设这个书架被打造成可伸缩的,即可以根据我们所放书籍数量而变大变小,此时我们该怎么办?

BookShelf 好说,根据可扩容的特性,我们可以应用 ArrayList 来代替数组

@Data public class BookShelf { private ArrayList<Book> bookList; public BookShelf() { this.bookList = new ArrayList<>(); } public BookShelf(ArrayList<Book> bookList) { this.bookList = bookList; } }

如此的话,我们的遍历访问逻辑就要做出相应调整

BookShelf bookShelf2 = new BookShelf(); bookShelf2.getBookList().add(new Book("Java")); bookShelf2.getBookList().add(new Book("C")); bookShelf2.getBookList().add(new Book("php")); bookShelf2.getBookList().add(new Book("python")); for (int i = 0; i < bookShelf2.getBookList().size(); i++) { System.out.println(bookShelf2.getBookList().get(i)); }

这里一旦集合改变组织元素的方式,任何其他存在遍历该集合对象的代码(相对于该集合来说,这些代码称为客户端代码)都需要跟着改变。意味着,客户端代码和集合是紧耦合的,客户端代码不应该关心集合内部是如何组织元素的,而只应该关心遍历该集合拿到元素之后应该做什么。

于是我们用 Iterator 模式来改造一下

After

首先无论集合如何组织元素,它都应该是可遍历的,因此需要抽象出两个方法:

  • boolean hasNext——集合中是否还有未遍历的元素
  • E next()——取出下一个未遍历的元素
public interface Iterator<E> { boolean hasNext(); E next(); }

集合应该只关注如何组织元素,因此应该将上述遍历逻辑交由他人 Iterator 来做

public interface Iterable<E> { Iterator<E> iterator(); }

通过调用 BookShelfiterator 方法我们可以获取 BookShelf 的迭代器,通过使用该迭代器的方法可以实现对 BookShelf 中元素的遍历访问:

public class BookShelfIterator implements Iterator<Book> { private int index; private BookShelf bookShelf; public BookShelfIterator(BookShelf bookShelf) { this.index = 0; this.bookShelf = bookShelf; } @Override public boolean hasNext() { return index < bookShelf.getBookList().size(); } @Override public Book next() { if (!hasNext()) { throw new RuntimeException("you have arrived the end") } return bookShelf.getBookList().get(index++); } } @Data public class BookShelf implements Iterable<Book> { private ArrayList<Book> bookList; public BookShelf() { this.bookList = new ArrayList<>(); } public BookShelf(ArrayList<Book> bookList) { this.bookList = bookList; } @Override public Iterator<Book> iterator() { return new BookShelfIterator(this); } }

客户端代码:

public class IteratorClient { public static void main(String[] args) { BookShelf bookShelf = new BookShelf(); bookShelf.getBookList().add(new Book("Java")); bookShelf.getBookList().add(new Book("C")); bookShelf.getBookList().add(new Book("php")); bookShelf.getBookList().add(new Book("python")); Iterator<Book> iterator = bookShelf.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); } } }

如此,如果 BookShelf 换用 Map 来组织元素,我们只需新增一个 BookShelfMapIterator 即可,而客户端代码无需任何改动:

@Data public class MapBookShelf implements Iterable{ /** * book's name -> book */ private Map<String, Book> bookMap = new HashMap<>(); @Override public Iterator iterator() { return new MapBookShelfIterator(this); } }
public class MapBookShelfIterator implements Iterator { private final MapBookShelf mapBookShelf; private final Object[] keys; private int index; public MapBookShelfIterator(MapBookShelf mapBookShelf) { this.mapBookShelf = mapBookShelf; this.keys = mapBookShelf.getBookMap().keySet().toArray(); this.index = 0; } @Override public boolean hasNext() { return index < keys.length; } @Override public Object next() { if (!hasNext()) { throw new RuntimeException("you have arrived the end"); } return mapBookShelf.getBookMap().get(keys[index++]); } }
MapBookShelf bookShelf = new MapBookShelf(); bookShelf.getBookMap().put("Java", new Book("Java")); bookShelf.getBookMap().put("C",new Book("C")); bookShelf.getBookMap().put("PHP",new Book("php")); bookShelf.getBookMap().put("Python", new Book("python")); Iterator iterator = bookShelf.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); }

虽然上述客户端代码也发生了改变,如 getBookMapput(这些改变可以通过抽象出一个 AbstractBookShelf 来避免),但是遍历访问逻辑没变,即首先取得集合的 Iterator 实例,然后调用接口方法 hasNextnext 进行遍历,hasNext 是对遍历界限的一个控制,其本身不做任何事,仅判断当前位置是否有元素;而 next 则做两件事:返回当前位置上的元素,将遍历指针后移。

UML & Summarize

完整的 Iterator 模式可表述如下

image.png

其中 IterableIterator 对应,ConcreteIterableConcreteIterator 对应,客户端仅知道 IteratorIterable 中的 3 个方法,可用此实现集合的遍历而不管集合内部组织形式,不同的集合实例则将其对应的 ConcreteIterator 实现隐藏在了 createIterator 方法中

Roles

  • Iterator,迭代器,专门负责迭代集合,符合单一职责
    • boolean hasNext()
    • E next()
  • Iterable,集合需要实现该接口,将遍历责任委托给具体的迭代器实例
    • Iterator createIterator()
  • Collection,集合
  • ConcreteIterator,具体的迭代器,和具体的集合实例之间是相互依赖的关系

Adapter 模式

Adapter 是为了将已有的实现适应不同的接口而存在。生活中的典型例子是,为了能使两个插销插头查到三个插孔的插座上,通常会在两者之间加上一个插口转换器,这个转换器承担的角色就是本设计模式的用意。

为了使已有的方法适应新的接口(例如已有方法健壮没有毛病,针对相同功能的新接口我们又不像写重复代码),我们通常会编写一个 Adapter,它仅仅起着一个转换器的作用。

Adapter 可以通过继承委托两种方式实现

Extends or Delegates

例如,系统中遗留着他人已写好的字符串打印类 Banner

public class Banner { public void printWithBracket(String s) { System.out.println("(" + s + ")"); } public void printWithStar(String s) { System.out.println("*" + s + "*"); } }

现在你正在对系统迭代,需要为新的接口 Print 编写实现类

public interface Print { void printWeak(String s); void printStrengthen(String s); }

而你的实现逻辑和 Banner 中的两个已有方法不谋而和,于是你可通过继承旧类、实现新接口的方式,既能避免重复代码的编写,又对新接口有所交代

public class PrintBanner extends Banner implements Print { @Override public void printWeak(String s) { this.printWithBracket(s); } @Override public void printStrengthen(String s) { this.printWithStar(s); } }

此种方式的缺点是,若目标类(这里指 Print)不是接口,那么受限于单继承机制就只能采取聚合的方式

你还可以通过聚合的方式,将实现逻辑委托给旧类:

public class CustomPrint implements Print { private Banner banner; public CustomPrint(Banner banner) { this.banner = banner; } @Override public void printWeak(String s) { banner.printWithBracket(s); } @Override public void printStrengthen(String s) { banner.printWithStar(s); } }

UML & Summarize

以下是两种方式的类图

image.png

其中方式 1 受目标类必须是接口的限制。

Roles

  • Adaptee,被适配方,通常为系统中的旧类,且类中存在对某功能 A 的实现
  • Target,目标类,通常为新添加到系统中的类,包含需要实现某功能 A 的抽象方法
  • Adapter,适配类,作为被适配方和目标类之间的桥梁,通过继承或聚合的方式实现代码复用

Left to Subclass

Template Method

声明式编程 & 面对抽象编程

模板方法模式属于特殊的“声明式”编程,即通过抽象定义一个业务处理逻辑中各步骤的先后执行顺序,但对于各步骤的具体实现并不关心,交由运行时实际的子类对象受理。这也充分利用了 OOP 的多态性

例如,现有一个订单业务类 OrderService 如下:

public class OrderService { public void makeOrder() { safeVerification(); reduceStock(); reduceBalance(); noticeDelivery(); } public void safeVerification() { System.out.println("安全校验"); } public void reduceStock() { System.out.println("去MySQL减库存"); } public void reduceBalance() { System.out.println("去MySQL减余额"); } public void noticeDelivery() { System.out.println("通知发货"); } }

客户端代码如下

public class Client { public static void main(String[] args) { OrderService orderService = new OrderService(); orderService.makeOrder(); } }

上述代码逻辑清晰,看起来没什么问题。但假设现在要做秒杀,需要将库存、余额等信息做一个缓存,那你就需要在原有的 OrderService 上做修改了。这违反了“开闭原则”(应对修改关闭而对扩展开放)。

此时我们就需要将业务步骤抽象出来,具体的实现交由特定的子类去做,以满足不同的业务场景:

public abstract class AbstractOrderService { public void makeOrder() { safeVerification(); reduceStock(); reduceBalance(); noticeDelivery(); } public void safeVerification(){ System.out.println("安全校验"); } public abstract void reduceStock() ; public abstract void reduceBalance(); public void noticeDelivery(){ System.out.println("通知发货"); } }

此时若需要缓存支持,只需新增一个实现类即可

public class OrderServiceWithCache extends AbstractOrderService { @Override public void reduceStock() { System.out.println("从缓存中减库存"); } @Override public void reduceBalance() { System.out.println("从缓存中减余额"); } }

客户端代码只需切换具体的实现类:

public class Client { public static void main(String[] args) { AbstractOrderService orderService = new OrderServiceWithCache(); //使用缓存 orderService.makeOrder(); orderService = new OrderService(); // 切换到MySQL orderService.makeOrder(); } }

UML & Summary

image.png

模板方法模式就是在抽象类中进行声明式编程(使用抽象方法的形式强调该业务的完成应该执行哪些步骤以及这些步骤的执行顺序,而不关注每个步骤具体是如何实现的),而将具体业务步骤的实现交由子类(运行时通过多态)完成。

在客户端看来,虽然只是切换了一下子类实例,但好像被切换的实例实现的具体步骤就被注入到整体地业务处理之中一样。

并且对于通用的步骤,如上述的 safeVerificationnoticeDelivery,可能它们的处理逻辑是固定的,这时可以将它们提取到父类中,实现复用。

Roles

  • SuperClass & Template Method,模板方法通常声明在抽象类或接口中,调用本类的抽象方法(当然也可以是包含通用逻辑的非抽象方法)完成特定的业务逻辑。
  • Concrete SubClass & Realization,子类只需实现相应的抽象方法就可以将具体的功能步骤注入到整体业务功能中,无需自己显式调用(模板方法会根据运行时信息动态调用)

Factory Method

Delay the Instance’s Creation

工厂方法模式就是模板方法模式的一个应用,只不过就是抽象父类中的抽象方法特化为一个创建实例的方法,将实例的创建延迟到了子类。

例如 Person 类有一个获取自己交通工具的抽象方法 getVehicle,并且能够在其他地方调用该 Vehicle 暴露的属性、方法,而将 Vehicle 实例的获取延迟到了子类(说是延迟,因为本身是抽象类,是无法被实例化的,因此在实例化 Person 的具体子类时能够确保其 getVehicle 已被重写了)。

如下是示例代码:

public class Vehicle { private String name; public Vehicle(String name) { this.name = name; } public String getName() { return name; } } public class Bicycle extends Vehicle { public Bicycle() { super("自行车"); } } public class JeepCar extends Vehicle { public JeepCar() { super("小汽车"); } } public abstract class Person { public void useVehicle() { System.out.println("使用交通工具"+getVehicle().getName()+"来代步"); } protected abstract Vehicle getVehicle(); } public class Student extends Person { @Override protected Vehicle getVehicle() { return new Bicycle(); } } public class Boss extends Person { @Override protected Vehicle getVehicle() { return new JeepCar(); } } public class Client { public static void main(String[] args) { Person p = new Student(); p.useVehicle(); p = new Boss(); p.useVehicle(); } }

UML & Summary

image.png

Roles

  • Factory Method,抽象父类中将某类实例的获取延迟到具体子类,但本类中的其他方法可以调用该方法并认为已获取到了该类实例,然后进行属性、方法的访问
  • Concrete Method,真正的获取并返回所需实例的逻辑

Generating Instance

Singleton

Seven Method

  1. Lazy loading

    懒加载模式,instance 会在 getInstance 第一次被调用时被初始化

    public class LazyLoadingSingleton { private LazyLoadingSingleton() { } private static LazyLoadingSingleton instance; public static LazyLoadingSingleton getInstance() { if (instance == null) { instance = new LazyLoadingSingleton(); } return instance; } }
  2. Synchronized Block Singleton

    上述代码在多线程并发执行时会出现 instance 被多次赋值的问题,为此可用内部锁语义 Synchronized 解决

    public class SynchronizerBlockSingleton { private SynchronizerBlockSingleton() { } private static SynchronizerBlockSingleton instance; public static SynchronizerBlockSingleton getInstance() { synchronized (SynchronizerBlockSingleton.class) { if (instance == null) { instance = new SynchronizerBlockSingleton(); } } return instance; } }
  3. Double-Checked Lock

    双重检查锁定,上述代码先锁定,然后检查,会导致 instance 被赋值后 synchronized 对后续并发调用 getInstance 带来上下文切换的开销,为此可以在锁定前先检查一次

    public class DoubleCheckedSingleton { private DoubleCheckedSingleton() { } private static DoubleCheckedSingleton instance; public static DoubleCheckedSingleton getInstance() { if (instance == null) { synchronized (DoubleCheckedSingleton.class) { if (instance == null) { instance = new DoubleCheckedSingleton(); } } } return instance; } }
  4. DCL with volatile

    由于指令重排序可能会导致 new 关键字初始化对象还未完成就返回对象的内存地址,进而导致后续访问 instance 属性时抛空指针异常,需要使用 volatile 保证对象初始化完毕后才返回引用地址

    public class VolatileSingleton { private VolatileSingleton() { } private static volatile VolatileSingleton instance; public static VolatileSingleton getInstance() { if (instance == null) { synchronized (VolatileSingleton.class) { if (instance == null) { instance = new VolatileSingleton(); } } } return instance; } }
  5. Eager Mode

    饿汉模式,如果实例对象的初始化开销较小(占用内存、初始化时间),那么完全可以在类初始化时完成

    public class EagerSingleton { private EagerSingleton() { } private static EagerSingleton instance = new EagerSingleton(); public static EagerSingleton getInstance() { return instance; } }

    instance 会在类初始化时被初始化,类只会在发生主动引用时被初始化一次,由 JVM 来保证

  6. Instance Holder

    如果你仍想使用懒汉模式又想优雅些,则可使用静态内部类的方式

    public class InstanceHolderSingleton { private static class SingletonHolder { private static InstanceHolderSingleton instance = new InstanceHolderSingleton(); } private InstanceHolderSingleton() { } public static InstanceHolderSingleton getInstance() { return SingletonHolder.instance; } }

    初始化 InstanceHolderSingleton 时并不会初始化其静态内部类 SingletonHolder,只有在调用 InstanceHolderSingleton.getInstance() 时,instance 才会随着 SingletonHolder 的初始化而初始化。

    以下种情况会立即导致类的初始化:

    • 使用 new 关键字创建该类实例
    • 访问该类的静态域(包括静态属性和静态方法,但不包括常量)
    • 通过 java.reflect 包下的类反射访问该类,如 Class.forName
    • 初始化子类时会检查其父类是否已被初始化,若父类未被初始化则先初始化其父类
  7. 枚举类的优雅

    JVM 也会保证枚举实例在初始化枚举时被初始化一次

    public class EnumSingleton { private EnumSingleton() { } private static EnumSingleton instance; private enum InstanceEnum{ INSTANCE; private EnumSingleton instance; InstanceEnum() { instance = new EnumSingleton(); } } public static EnumSingleton getInstance() { return InstanceEnum.INSTANCE.instance; } }

Builder

组装具有复杂结构的实例

建造者模式通常应用于需要通过一系列复杂步骤才能得到最终实例的情况。就像建造房子一样,我们需要经过打地基、搭建框架、添砖加瓦、粉饰美化等一系列步骤才能得到最终能住人的房子。并且,这些步骤可以个性化定制,例如有人喜欢欧美风格的,那么房屋框架顶部就要打造成锥形的;有人喜欢粉色,那就可以铺上粉色的墙纸……

本例中,我们以一个邮件内容 String 实例的生成来演示 Builder 设计模式的应用。

public abstract class EmailBuilder { protected String content = ""; public String getContent() { return content; } public abstract void makeTitle(String title); public abstract void makeBody(String body); public abstract void makeGreeting(String greeting); } public class TextEmailBuilder extends EmailBuilder { @Override public void makeTitle(String title) { StringBuilder stringBuilder = new StringBuilder(content); stringBuilder.append(title).append("\n"); content = stringBuilder.toString(); } @Override public void makeBody(String body) { StringBuilder stringBuilder = new StringBuilder(content); stringBuilder.append(body).append("\n"); content = stringBuilder.toString(); } @Override public void makeGreeting(String greeting) { StringBuilder stringBuilder = new StringBuilder(content); stringBuilder.append(greeting).append("\n"); content = stringBuilder.toString(); } } public class HTMLEmailBuilder extends EmailBuilder { @Override public void makeTitle(String title) { StringBuilder stringBuilder = new StringBuilder(content); stringBuilder.append("<h3>").append(title).append("</h3>").append("\n"); content = stringBuilder.toString(); } @Override public void makeBody(String body) { StringBuilder stringBuilder = new StringBuilder(content); stringBuilder.append("<p style=\"font-family: Microsoft Ya Hei; font-size: 16px\">").append(body).append("</p>").append("\n"); content = stringBuilder.toString(); } @Override public void makeGreeting(String greeting) { StringBuilder stringBuilder = new StringBuilder(content); stringBuilder.append("<i>").append(greeting).append("</i>").append("\n"); content = stringBuilder.toString(); } } public class Director { private EmailBuilder emailBuilder; public Director(EmailBuilder emailBuilder) { this.emailBuilder = emailBuilder; } public void construct() { emailBuilder.makeTitle("About Interview"); emailBuilder.makeBody("We are honor to tell you that you can participate our interview."); emailBuilder.makeGreeting("Good Luck!"); } }

UML & Summary

image.png

其中 EmailBuilder 中声明了实例的初始状态(空串)和构建实例的一系列过程,而 TextEmailBuilderHTMLEmailBuilder 则对这一系列过程进行了个性化实现。最终 Director 是建造实例整个过程的监工,由它确保实例的成型规则地经历了哪些建造过程。

Roles

  • Builder,定义了产品成型需要经过的一系列工艺(接口方法)
  • ConcreteBuilder,针对每道工艺进行个性化处理
  • Director,监工,根据产品成型流程调用接口方法打造产品
  • Client,模式使用者,通知 Director 根据传入的 ConcreteBuilder 打造特定风格的产品

Abstract Factory

将一组关联的零件组装成产品

AbstractFactory 其实就是包含了一系列 Factory Method 的类,只不过这些 Factory Method 生成的实例都是相互关联的,一起组成某个共同体,少了谁都不行。

例如汽车 Car 需要汽车外壳 Facade、轮胎 Wheel、发动机 Engine 等部件,那么我们就可以创建一个 CarFactory 抽象工厂,其中声明了一系列部件的获取(抽象方法,不关心该部件是哪个厂家生产的或是哪个牌子的),并提供了产品的构造过程(调用这一系列抽象方法获取所需部件组装成车)

public class Engine { String name; public Engine(String name) { this.name = name; } }
public class Wheel { String name; public Wheel(String name) { this.name = name; } }
public class Facade { String name; public Facade(String name) { this.name = name; } }
public class Car { Engine engine; Wheel wheel; Facade facade; public Car(Engine engine, Wheel wheel, Facade facade) { this.engine = engine; this.wheel = wheel; this.facade = facade; } }
public abstract class CarFactory { public Car getCar() { return new Car(getEngine(), getWheel(), getFacade()); } public abstract Engine getEngine(); public abstract Wheel getWheel(); public abstract Facade getFacade(); }
public class CustomEngine extends Engine { public CustomEngine() { super("自定义牌发动机"); } }
public class CustomWheel extends Wheel{ public CustomWheel() { super("自定义牌轮胎"); } }
public class CustomFacade extends Facade { public CustomFacade() { super("自定义牌车壳"); } }
public class CustomCarFactory extends CarFactory{ @Override public Engine getEngine() { return new CustomEngine(); } @Override public Wheel getWheel() { return new CustomWheel(); } @Override public Facade getFacade() { return new CustomFacade(); } }
public class Client { public static void main(String[] args) { CarFactory carFactory = new CustomCarFactory(); Car car = carFactory.getCar(); System.out.println("custom car -> " + car.engine.name + "+" + car.wheel.name + "+" + car.facade.name); } }

UML

image.png

Consider Individualy

Bridge

将类的功能层次和类的实现层次分开

  • 类的功能层次结构

    通过继承我们能够继承基类已有的功能,在此之上我们能够:重写基类已有功能(使该功能具备本类特色)、也可以新增功能,重写(这里的重写特指重写基类的已有实现)或新增功能的子类与基类构成类的功能层次结构

    public class Animal { public void eat() { System.out.println("动物会觅食"); } } public class Bird extends Animal { @Override public void eat() { System.out.println("鸟觅食虫子"); } public void fly() { System.out.println("鸟会飞"); } }
  • 类的实现层次结构

    通过继承,我们能够实现基类的抽象方法,通过运用 Template Method,我们可以将子类的逻辑注入到基类模板过程中,这时新增子类仅为了实现基类的抽象方法,子类和基类构成类的实现层次结构

    public abstract class Animal { public void hunt() { lockTarget(); quickAttack(); swallow(); } public abstract void lockTarget(); public abstract void quickAttack(); public abstract void swallow(); } public class Snake extends Animal { @Override public void lockTarget() { System.out.println("锁定猎物"); } @Override public void quickAttack() { System.out.println("迅速咬住猎物喉部"); } @Override public void swallow() { System.out.println("一口吞掉整个猎物"); } }

如果我们将两个例子的 Animal 整合在一起:

public abstract class Animal { public void eat() { System.out.println("动物会觅食"); } public void hunt() { lockTarget(); quickAttack(); swallow(); } public abstract void lockTarget(); public abstract void quickAttack(); public abstract void swallow(); }

你会发现,Bird 无法编译,作为具体子类它必须实现抽象方法 lockTargetquickAttackswallow,但是我们新增 Bird 的初衷只是为了继承 Animaleat 方法,并新增一个自己会 fly 的功能。

这时就需要我们将类的功能层次和实现层次分开了

public abstract class Animal { private Hunt hunt; public Animal(Hunt hunt) { this.hunt = hunt; } public void eat() { System.out.println("动物会觅食"); } public void hunt() { hunt.hunt(); } } public abstract class Hunt { public void hunt() { lockTarget(); quickAttack(); swallow(); } public abstract void lockTarget(); public abstract void quickAttack(); public abstract void swallow(); } public class Bird extends Animal { public Bird(Hunt hunt) { super(hunt); } @Override public void eat() { System.out.println("鸟觅食虫子"); } public void fly() { System.out.println("鸟会飞"); } } public class DefaultHunt extends Hunt { @Override public void lockTarget() { System.out.println("用眼睛锁定猎物"); } @Override public void quickAttack() { System.out.println("快速咬死猎物"); } @Override public void swallow() { System.out.println("一口一口吃掉猎物"); } } public class Snake extends Animal { public Snake(Hunt hunt) { super(hunt); } } public class SnakeHunt extends Hunt { @Override public void lockTarget() { System.out.println("红外线感知锁定猎物"); } @Override public void quickAttack() { System.out.println("使用尖牙和毒液快速致死猎物"); } @Override public void swallow() { System.out.println("一口吞掉整个猎物"); } } public class Client { public static void main(String[] args) { Hunt defaultHunt = new DefaultHunt(); Hunt snakeHunt = new SnakeHunt(); Animal snake = new Snake(snakeHunt); System.out.println("蛇开始狩猎=========="); snake.hunt(); Animal bird = new Bird(defaultHunt); System.out.println("鸟开始狩猎==========="); bird.hunt(); System.out.println("鸟有不同于一般动物的功能"); ((Bird) bird).fly(); } }

UML & Summary

image.png

如上,AnimalBirdSnake 组成功能层次结构、HuntDefaultHuntSnake 则组成了实现层次结构。这样,以后如果我们想扩展功能(重写或新增),那么就可以找对应功能层次结构中的类继承;如果想针对狩猎方式进行个性化实现,则继承实现层次结构中的类即可。

桥接模式避免了将实现和扩展捆绑在一起,减少底层类扩展基类的压力

Roles

  • 功能层次结构
  • 实现层次结构
  • 功能层次结构的基类持有实现层次结构基类的引用,并将实现层面的逻辑委托给该引用

Strategy

封装一个特定的算法

假如你是一个农场主,需要按照顾客的需求从已采摘的苹果中挑选出符合顾客标准的苹果

@Data @AllArgsConstructor @NoArgsConstructor public class Apple { private AppleColorEnum color; private double weight; public enum AppleColorEnum { RED,YELLOW,GREEN } }

于是你编写了如下挑选苹果的业务处理类

public class AppleService { List<Apple> findAppleByColor(List<Apple> apples, Apple.AppleColorEnum color) { List<Apple> res = new ArrayList<>(); for (Apple apple : apples) { if (Objects.equals(apple.getColor(),color)){ res.add(apple); } } return res; } }

但是如果你遇到了一个刁钻的顾客,他不仅要求颜色为红色,而且还要求重量在 500g 以上呢?你可以再添加一个 findRedAndWeightGreatThan500,但是每个顾客可能对颜色和重量的标准都是不一样的,并且你无法估量有哪些顾客,对应有哪些挑选标准。

这时就需要把挑选标准(实际上就是一个算法)单独抽离出来,分析输入和输出:

  • 输入:Apple,给你一个苹果
  • 输出:boolean,该苹果是否符合标准
public interface AppleFilterStrategy { /** * 如果该苹果符合挑选标准,那么就返回true * @param apple * @return */ boolean filterApple(Apple apple); }

这时,挑选苹果业务类就无需预知和预置众多挑选苹果的方法了,因为挑选策略交给了 AppleFilterStrategy

public class AppleService { List<Apple> findApple(List<Apple> apples, AppleFilterStrategy strategy) { List<Apple> res = new ArrayList<>(); for (Apple apple : apples) { if (strategy.filterApple(apple)) { res.add(apple); } } return res; } }

客户端可以根据客户提出的挑选需求,通过匿名类的方式随意地注入挑选策略

public class Client { public static void main(String[] args) { // 农场主采摘的苹果 List<Apple> apples = Arrays.asList( new Apple(Apple.AppleColorEnum.RED, 200), new Apple(Apple.AppleColorEnum.RED, 400), new Apple(Apple.AppleColorEnum.RED, 600), new Apple(Apple.AppleColorEnum.YELLOW, 100), new Apple(Apple.AppleColorEnum.YELLOW, 500), new Apple(Apple.AppleColorEnum.YELLOW, 900), new Apple(Apple.AppleColorEnum.GREEN, 400), new Apple(Apple.AppleColorEnum.GREEN, 500), new Apple(Apple.AppleColorEnum.GREEN, 600) ); AppleService appleService = new AppleService(); // A顾客需要红色的重量大于500g的苹果 List<Apple> res1 = appleService.findApple(apples, new AppleFilterStrategy() { @Override public boolean filterApple(Apple apple) { return Objects.equals(apple.getColor(), Apple.AppleColorEnum.RED) && apple.getWeight() >= 500; } }); System.out.println(res1); System.out.println("======================"); // B顾客需要青色的种类小于400的 List<Apple> res2 = appleService.findApple(apples, new AppleFilterStrategy() { @Override public boolean filterApple(Apple apple) { return Objects.equals(apple.getColor(), Apple.AppleColorEnum.GREEN) && apple.getWeight() <= 400; } }); System.out.println(res2); } }

函数式编程

在 Java8 之后,像 AppleFilterStrategy 这种只包含一个接口方法的接口可以被标注为 @FunctionalInterface,并使用 Lambda 表达式替代冗余的匿名类

@FunctionalInterface public interface AppleFilterStrategy { boolean filterApple(Apple apple); }
List<Apple> res3 = appleService.findApple(apples, apple -> apple.getColor() != Apple.AppleColorEnum.GREEN && apple.getWeight() >= 300); System.out.println(res3);

UML & Summary

image.png

策略模式的核心思想就是将复杂多变的算法逻辑抽取出来,交给客户端实现,而业务层只负责应用客户端传递的算法实现

Role

  • Strategy Interface,策略接口,定义了某个特定的算法
  • Service,应用策略接口的算法,面对抽象编程
  • Client,调用 Service 时注入具体的算法实现
  • Concrete Strategy,具体的算法实现,通常以匿名类的形式存在,Java8 之后可用 Lambda 代替

Consistency

Composite

让容器和容器中的内容有着一致的外观

混合模式的典型应用就是文件系统,一个目录 Directory 中可以存放若干条目 Entry,每个条目既可以是目录又可以是文件 File。混合模式的目的就是让容器(如目录)和容器中的内容(如目录或文件)有着一致性的外观(如 Entry

public abstract class Entry { private String name; private int size; private List<Entry> items; public Entry(String name, int size) { this.name = name; this.size = size; items = new ArrayList<>(); } public void addEntry(Entry entry) { this.items.add(entry); } public abstract void print(int... layer); } public class Directory extends Entry { public Directory(String name, int size) { super(name, size); } @Override public void print(int... layer) { int n; if (layer == null || layer.length == 0) { n = 0; } else { n = layer[0]; } for (int i = 0; i < n; i++) { System.out.print("\t"); } System.out.println(getName()); getItems().forEach(entry -> entry.print(n + 1)); } } public class File extends Entry { public File(String name, int size) { super(name, size); } @Override public void print(int... layer) { int n; if (layer == null || layer.length == 0) { n = 0; } else { n = layer[0]; } for (int i = 0; i < n; i++) { System.out.print("\t"); } System.out.println(getName() + " size=" + getSize()+"kb"); getItems().forEach(entry -> entry.print(n + 1)); } } public class Client { public static void main(String[] args) { Entry root = new Directory("/root", 2); Entry bin = new Directory("/bin", 0); root.addEntry(bin); Entry usr = new Directory("/usr", 1); root.addEntry(usr); Entry etc = new Directory("/etc", 0); root.addEntry(etc); Entry local = new Directory("/local", 3); usr.addEntry(local); Entry java = new File("java.sh", 128); local.addEntry(java); Entry mysql = new File("mysql.sh", 64); local.addEntry(mysql); Entry hadoop = new File("hadoop.sh", 1024); local.addEntry(hadoop); root.print(); } } /root /bin /usr /local java.sh size=128kb mysql.sh size=64kb hadoop.sh size=1024kb /etc

UML

image.png

Roles

  • 组件

    如本例的 Entry,是容器和容器内容的同一外观。系统不直接操作容器和容器中的内容,而是操作组件

  • 容器

    其中可包含若干容器和条目

  • 条目

    系统中的基本单元

Decorator

装饰者模式是一种结合继承和组合(委托)设计模式,通过继承能够实现统一外观(装饰类和目标类有共同的父类),通过委托能够在目标功能的基础之上进行增强。并且装饰类不管目标类是源目标类还是被装饰过的目标类,它对目标类是否已被保存过和被哪种包装器(装饰类)包装过以及被包装了几层都不关心,它只负责当前的装饰逻辑。

不改变目标类的行为,只是在其之上做一些包装

本例中,有一个糕点师 Baker,他有一个 bake 烘焙面包的抽象方法,BreadBaker 则是他的一个实现。现在我们需要根据顾客的不同口味对原味的面包进行包装,例如应该加哪些佐料 Ingredient,以下是示例代码

public abstract class Baker { public abstract void bake(); } public class BreadBaker extends Baker { @Override public void bake() { System.out.println("面包被烘焙"); } }
public class IngredientDecorator extends Baker { private Baker baker; private String ingredient; public IngredientDecorator(Baker baker,String ingredient) { this.baker = baker; this.ingredient = ingredient; } @Override public void bake() { System.out.print("添加了" + ingredient + "的"); baker.bake(); } }
public class Client { public static void main(String[] args) { Baker baker = new BreadBaker(); baker.bake(); Baker pepper = new IngredientDecorator(baker, "胡椒粉"); pepper.bake(); Baker mustard = new IngredientDecorator(baker, "芥末"); mustard.bake(); Baker oliveOil = new IngredientDecorator(baker, "橄榄油"); oliveOil.bake(); Baker pepperAndOlive = new IngredientDecorator(new IngredientDecorator(baker, "橄榄油"), "胡椒粉"); pepperAndOlive.bake(); Baker mustardAndOliveAndPepper = new IngredientDecorator( new IngredientDecorator( new IngredientDecorator(baker, "胡椒粉"), "橄榄油"), "芥末"); mustardAndOliveAndPepper.bake(); } } 面包被烘焙 添加了胡椒粉的面包被烘焙 添加了芥末的面包被烘焙 添加了橄榄油的面包被烘焙 添加了胡椒粉的添加了橄榄油的面包被烘焙 添加了芥末的添加了橄榄油的添加了胡椒粉的面包被烘焙

当然本例只是单纯演示装饰者模式的思想,你完全可以将 Ingredient 具体化为 PepperIngredientMustardIngredident 等代替字符串魔法值

UML & Summary

image.png

应用装饰者模式有一个口诀:是你(IS-A)还有你(HAS-A),一切拜托你(所有的重写方法委托给目标类对象,在此之上自己可以添加当前这一层包装的逻辑)。

Roles

  • Parent,装饰类和目标类有一个共同的父类,通过在父类声明抽象方法使得两者有共同的外观
  • Target,目标类,需要被包装的类
  • Decorator,装饰类,既可以直接装饰目标对象,又可以装饰装饰过目标对象的装饰对象,因为两者有共同的外观

Access Data Structure

Visitor

将数据结构的管理和访问处理分开

visitor 模式目的是将数据结构的管理(对元素的增删改查)和对数据结构的访问处理逻辑分离开,通常和迭代器模式结合使用。我们将对数据访问处理的逻辑单独定义一个 Visitor 接口以及声明相应的 visit(E element) 方法,而数据接口则对应提供一个受理 Visitoraccept(Visitor visitor) 方法(其实就是简单的调用 visitor.visit()visit(E element) 相当于对访问到的元素进行消费。

以下是数据结构为多叉树时的示例代码(其中迭代器用的是前文实现的而非 JDK 自带的)

@Data public abstract class Node<E> implements Iterable<Node<E>> { private E element; private List<Node<E>> children; public Node(E element) { this.element = element; } public abstract void accept(NodeVisitor<E> visitor); @Override public Iterator<Node<E>> iterator() { return new NodeIterator(this); } } public class Leaf<E> extends Node<E> { public Leaf(E element) { super(element); this.setChildren(Collections.emptyList()); } @Override public void accept(NodeVisitor<E> visitor) { visitor.visitLeaf(this); } } public class Branch<E> extends Node<E> { public Branch(E elemnt) { super(elemnt); this.setChildren(new ArrayList<>()); } @Override public void accept(NodeVisitor<E> visitor) { visitor.visitBranch(this); } }
public class NodeIterator<E> implements Iterator<Node<E>> { private Node<E> root; private Stack<Node<E>> stack; public NodeIterator(Node<E> root) { this.root = root; this.stack = new Stack<>(); stack.push(root); } @Override public boolean hasNext() { return stack.size() > 0; } @Override public Node<E> next() { if (!hasNext()) { throw new RuntimeException("no more elements"); } Node<E> node = stack.pop(); List<Node<E>> children = node.getChildren(); for (int i = children.size() - 1; i >= 0; i--) { stack.push(children.get(i)); } return node; } }
public interface NodeVisitor<E> { void visitLeaf(Leaf<E> leaf); void visitBranch(Branch<E> branch); } public class PrintNodeVisitor<E> implements NodeVisitor<E> { @Override public void visitLeaf(Leaf<E> leaf) { System.out.print(leaf.getElement()+" "); } @Override public void visitBranch(Branch<E> branch) { System.out.print(branch.getElement()+" "); } } public class PlusOneNodeVisitor implements NodeVisitor<Integer> { /** * 访问到叶子节点则将节点值+1 * @param leaf */ @Override public void visitLeaf(Leaf<Integer> leaf) { leaf.setElement(leaf.getElement() + 1); } /** * 访问到分叉节点,则将节点值+其孩子节点数 * @param branch */ @Override public void visitBranch(Branch<Integer> branch) { branch.setElement(branch.getElement() + branch.getChildren().size()); } }

UML & Summary

image.png

visitor 模式将对数据结构的访问逻辑通过 accept 委托给 Visitor 接口和迭代器模式将遍历访问逻辑通过 createIterator 委托给 Iterator 接口有异曲同工之妙。将专业的事交给专业的人做,满足 Single ResponsibilityInterface Segregation Principle;需要修改访问处理逻辑我们只需要新增一个 NodeVisitor 的实现,满足 Open/Closed Principle;逻辑实现和客户端代码都面向接口 NodeVisitor 编程,满足 Dependency Inversion

Roles

  • Visitor,拜访者,声明相关的 visit 方法,用于对数据结构进行访问处理
  • DataStructure,数据结构,提供元素的增删改查接口,通过 accept(visitor) 将访问处理逻辑交给 Visitor
  • ConcreteVisitor,根据业务所需,实现具体的访问处理逻辑

Chain Of Responsibility

责任链模式通过维护若干请求受理者形成一条链来处理客户端发起的请求,该模式最大的优点在于它弱化了客户端和请求受理者之间的关系,客户端只需要将请求发送到责任链,在责任链中通过连环委托的机制就能够做到无法受理请求的人直接忽略请求,而能够受理请求的人截断请求并受理。

客户端不用关心请求具体会被谁受理,这样就提高了客户端的独立性。

弱化请求方和受理方之间的关系

本例中,存在一条公司组织架构责任链(Employee->Leader->Manager->Boss),他们都能够受理报销费用的请求 handle(int amount)Client 无需关心多少面值的报销金额应该由谁来受理

public abstract class Handler { private Handler handler = null; public abstract void handle(int amount); public Handler setNext(Handler handler) { this.handler = handler; return handler; } public Handler getNext() { return handler; } } public class Employee extends Handler { @Override public void handle(int amount) { if (amount <= 100) { System.out.println("$" + amount + " is handled by employee"); return; } this.getNext().handle(amount); } } public class Leader extends Handler{ @Override public void handle(int amount) { if (amount <= 1000) { System.out.println("$" + amount + " is handled by leader"); return; } this.getNext().handle(amount); } } public class Manager extends Handler{ @Override public void handle(int amount) { if (amount <= 5000) { System.out.println("$" + amount + " is handled by manager"); return; } this.getNext().handle(amount); } } public class Boss extends Handler { @Override public void handle(int amount) { System.out.println("$" + amount + " is handled by boss"); } }
public class Client { public static void main(String[] args) { Handler employee = new Employee(); Handler leader = new Leader(); Handler manager = new Manager(); Handler boss = new Boss(); employee.setNext(leader).setNext(manager).setNext(boss); for (int i = 0; i < 6; i++) { int amount = (int) (Math.random() * 10000); employee.handle(amount); } } } $6643 is handled by boss $4964 is handled by manager $684 is handled by leader $9176 is handled by boss $8054 is handled by boss $909 is handled by leader

参考资料

《图解设计模式》

  • 设计模式

    设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

    200 引用 • 120 回帖 • 3 关注
  • PlantUML
    4 引用 • 6 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • someone

    大佬,您这个 UML 图是用上个博客的插件画的吗?

  • someone

    是的是的,我看到前面的内容了

  • someone27889 via macOS

    感觉你不像是正经 hadoop 呢 ~

  • PeterChu via Redmi Note 7

    哇,插个眼。

    好长的贴啊,看的压力好大,分贴做个系列。