java基础-设计模式(创建型模式)

这部分内容的脑图:链接

image-20200508190605436

简介

设计模式,即Design Patterns,是指在软件设计中,被反复使用的一种代码设计经验。使用设计模式的目的是为了可重用代码,提高代码的可扩展性和可维护性。

为什么要使用设计模式?根本原因还是软件开发要实现可维护、可扩展,就必须尽量复用代码,并且降低代码的耦合度。设计模式主要是基于OOP编程提炼的,它基于以下几个原则:

开闭原则

由Bertrand Meyer提出的开闭原则(Open Closed Principle)是指,软件应该对扩展开放,而对修改关闭。
这里的意思是在增加新功能的时候,能不改代码就尽量不要改,如果只增加代码就完成了新功能,那是最好的。

里氏替换原则

里氏替换原则是Barbara Liskov提出的,这是一种面向对象的设计原则,即如果我们调用一个父类的方法可以成功,那么替换成子类调用也应该完全可以运行。

23个常用模式分为创建型模式、结构型模式和行为型模式三类

1. 创建型模式

创建型模式关注点是如何创建对象,其核心思想是要把对象的创建和使用相分离,这样使得两者能相对独立地变换。

创建型模式包括:

  • 工厂方法:Factory Method
  • 抽象工厂:Abstract Factory
  • 建造者:Builder
  • 原型:Prototype
  • 单例:Singleton

1.1 工厂方法

Factory Method

基本方式

一般的工厂方法的创建使用方式

编写工厂接口

//编写工厂接口
public interface NumberFactory{
    //创建方法(工厂中的创建方法)
    Number parse(String s);

    //创建静态的接口类,创建真实的子类
    static NumberFactory impl = new NumberFactoryImp();

    //有了工厂接口的实现类,再添加静态方法返回真正的子类
    static NumberFactoryImp getFactory(){
        return (NumberFactoryImp) impl;
    }
}

编写工厂接口的实现类(真正的工厂)

//有了工厂接口后,编写工厂接口的实现类
//这个实现类,才是真正的工厂
//通过这样两层的嵌套,调用方只需要调用接口而可以完全忽略真正的工厂
//同样,可以忽略真正的产品类型 BigDecimal,而只用和抽象产品Number打交道
public static class NumberFactoryImp implements NumberFactory{
    //实现接口声明的方法
    public Number parse(String s){
        return new BigDecimal(s);
    }
}

使用

NumberFactory factory = NumberFactory.getFactory();
Number result = factory.parse("123.456");

好处

  • 只需要和工厂接口 NumberFactory和抽象产品 Number打交道,
    不关心真正的工厂NumberFactoryImpl和实际的产品BigDecimal
  • 允许创建产品的代码独立地变换,而不会影响到调用方

静态工厂方法

实际上大多数情况下我们并不需要抽象工厂(即上面的工厂接口),而是通过静态方法直接返回产品,即:

public class NumberFactory {
    public static Number parse(String s) {
        return new BigDecimal(s);
    }
}

这种简化的使用静态方法创建产品的方式称为静态工厂方法(Static Factory Method)。
静态工厂方法广泛地应用在Java标准库中。例如:

Integer n = Integer.valueOf(100);

Integer既是产品又是静态工厂。它提供了静态方法valueOf()来创建Integer

.valueOf() 对比 new Integer()

观察valueOf()方法:

public final class Integer {
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
    ...
}

valueOf()内部可能会使用new创建一个新的Integer实例,但也可能直接返回一个缓存的Integer实例。对于调用方来说,没必要知道Integer创建的细节。

工厂方法可以隐藏创建产品的细节,且不一定每次都会真正创建产品,完全可以返回缓存的产品,从而提升速度并减少内存消耗。

如果调用方直接使用Integer n = new Integer(100),那么就失去了使用缓存优化的可能性。

小结

  • 工厂方法是指定义工厂接口和产品接口,但如何创建实际工厂和实际产品被推迟到子类实现,从而使调用方只和抽象工厂与抽象产品打交道。
  • 实际更常用的是更简单的静态工厂方法,它允许工厂内部对创建产品进行优化。
  • 调用方尽量持有接口或抽象类,避免持有具体类型的子类,以便工厂方法能随时切换不同的子类返回,却不影响调用方代码。

1.2 抽象工厂

提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

抽象工厂模式(Abstract Factory)是一个比较复杂的创建型模式。

抽象工厂模式和工厂方法不太一样,它要解决的问题比较复杂,不但工厂是抽象的,产品是抽象的,而且有多个产品需要创建,因此,这个抽象工厂会对应到多个实际工厂,每个实际工厂负责创建多个实际产品:

image-20200507230533053

这种模式有点类似于多个供应商负责提供一系列类型的产品。

例子:Markdown转HTML和Word

为用户提供一个Markdown文本转换为HTML和Word的服务,它的接口定义如下:

需求方

抽象工厂接口:

public interface AbstractFactory {
    // 创建Html文档:
    HtmlDocument createHtml(String md);
    // 创建Word文档:
    WordDocument createWord(String md);
}

上述就得到了

抽象工厂(接口)

  • AbstractFactory

抽象产品(接口)

  • HtmlDocument
  • WordDocument

因为HtmlDocumentWordDocument都比较复杂,现在我们并不知道如何实现它们,所以只有接口,但是需要在接口内部描述清楚,接口能实现的方法和功能,并通过:

// Html文档接口:
public interface HtmlDocument {
    String toHtml();
    void save(Path path) throws IOException;
}

// Word文档接口:
public interface WordDocument {
    void save(Path path) throws IOException;
}

供应商:实现产品

市场上有两家供应商可以提供上述抽象产品

FastDoc Soft

  • FastHtmlDocument
  • FastWordDocument

GoodDoc Soft

  • GoodHtmlDocument
  • GoodWordDocument

第一家公司的产品(实现需求方给出的抽象产品,是一个实际的实现类)

public class FastHtmlDocument implements HtmlDocument {
    public String toHtml() {
        ...
    }
    public void save(Path path) throws IOException {
        ...
    }
}

public class FastWordDocument implements WordDocument {
    public void save(Path path) throws IOException {
        ...
    }
}

FastDoc Soft必须提供一个实际的工厂来生产这两种产品,即FastFactory

也就是implement抽象工厂接口,最后可以通过抽象产品接口调用这个类

public class FastFactory implements AbstractFactory {
    public HtmlDocument createHtml(String md) {
        return new FastHtmlDocument(md);
    }
    public WordDocument createWord(String md) {
        return new FastWordDocument(md);
    }
}

第二家公司的产品(实现需求方给出的抽象产品,是一个实际的实现类)

// 实际工厂:
public class GoodFactory implements AbstractFactory {
    public HtmlDocument createHtml(String md) {
        return new GoodHtmlDocument(md);
    }
    public WordDocument createWord(String md) {
        return new GoodWordDocument(md);
    }
}

// 实际产品:
public class GoodHtmlDocument implements HtmlDocument {
    ...
}

public class GoodWordDocument implements HtmlDocument {
    ...
}

使用供应商提供的产品

客户端编写代码如下:(FastDoc公司)

// 创建AbstractFactory,实际类型是FastFactory:
AbstractFactory factory = new FastFactory();
// 生成Html文档:
HtmlDocument html = factory.createHtml("#Hello\nHello, world!");
html.save(Paths.get(".", "fast.html"));
// 生成Word文档:
WordDocument word = fastFactory.createWord("#Hello\nHello, world!");
word.save(Paths.get(".", "fast.doc"));

客户端编写代码如下:(GoodDoc公司)

只需要将第一行改动

AbstractFactory factory = new GoodFactory();

屏蔽实际工厂

将创建工厂的代码,放在抽象工厂接口中(用接口的静态方法实现),就可以屏蔽实际工厂

在创建工厂的时候指定fast或者good即可

public interface AbstractFactory {
    public static AbstractFactory createFactory(String name) {
        if (name.equalsIgnoreCase("fast")) {
            return new FastFactory();
        } else if (name.equalsIgnoreCase("good")) {
            return new GoodFactory();
        } else {
            throw new IllegalArgumentException("Invalid factory name");
        }
    }
}

小结

  • 抽象工厂模式是为了让创建工厂和一组产品与使用相分离,并可以随时切换到另一个工厂以及另一组产品;
  • 抽象工厂模式实现的关键点是定义工厂接口和产品接口,但如何实现工厂与产品本身需要留给具体的子类实现,客户端只和抽象工厂与抽象产品打交道。

1.3 生成器

Builder模式是为了创建一个复杂的对象,需要多个步骤完成创建,或者需要多个零件组装的场景,且创建过程中可以灵活调用不同的步骤或组件。

生成器模式(Builder)是使用多个“小型”工厂来最终创建出一个完整对象。

当我们使用Builder的时候,一般来说,是因为创建这个对象的步骤比较多,每个步骤都需要一个零部件,最终组合成一个完整的对象。

例子:我们把Markdown转HTML看作一行一行的转换,每一行根据语法,使用不同的转换器:

  • 如果以#开头,使用HeadingBuilder转换;
  • 如果以>开头,使用QuoteBuilder转换;
  • 如果以---开头,使用HrBuilder转换;
  • 其余使用ParagraphBuilder转换。

定义Builder

这个HtmlBuilder写出来如下:

public class HtmlBuilder {
    private HeadingBuilder headingBuilder = new HeadingBuilder();
    private HrBuilder hrBuilder = new HrBuilder();
    private ParagraphBuilder paragraphBuilder = new ParagraphBuilder();
    private QuoteBuilder quoteBuilder = new QuoteBuilder();

    public String toHtml(String markdown) {
        StringBuilder buffer = new StringBuilder();
        markdown.lines().forEach(line -> {
            if (line.startsWith("#")) {
                buffer.append(headingBuilder.buildHeading(line)).append('\n');
            } else if (line.startsWith(">")) {
                buffer.append(quoteBuilder.buildQuote(line)).append('\n');
            } else if (line.startsWith("---")) {
                buffer.append(hrBuilder.buildHr(line)).append('\n');
            } else {
                buffer.append(paragraphBuilder.buildParagraph(line)).append('\n');
            }
        });
        return buffer.toString();
    }
}

注意观察上述代码,HtmlBuilder并不是一次性把整个Markdown转换为HTML,而是一行一行转换,并且,它自己并不会将某一行转换为特定的HTML,而是根据特性把每一行都“委托”给一个XxxBuilder去转换,最后,把所有转换的结果组合起来,返回给客户端。

这样一来,我们只需要针对每一种类型编写不同的Builder。例如,针对以#开头的行,需要HeadingBuilder

public class HeadingBuilder {
    public String buildHeading(String line) {
        int n = 0;
        while (line.charAt(0) == '#') {
            n++;
            line = line.substring(1);
        }
        return String.format("<h%d>%s</h%d>", n, line.strip(), n);
    }
}

简化Builder模式(链式调用)

简化Builder模式,以链式调用的方式来创建对象。例如,我们经常编写这样的代码:

StringBuilder builder = new StringBuilder();
builder.append(secure ? "https://" : "http://")
       .append("www.liaoxuefeng.com")
       .append("/")
       .append("?t=0");
String url = builder.toString();

由于我们经常需要构造URL字符串,可以使用Builder模式编写一个URLBuilder,调用方式如下:

String url = URLBuilder.builder() // 创建Builder
        .setDomain("www.liaoxuefeng.com") // 设置domain
        .setScheme("https") // 设置scheme
        .setPath("/") // 设置路径
        .setQuery(Map.of("a", "123", "q", "K&R")) // 设置query
        .build(); // 完成build

上面代码中使用的类似 .setDomain(),都是在URLBuilder这个类的定义中,将这些方法定义好了

小结

  • Builder模式是为了创建一个复杂的对象,需要多个步骤完成创建,
  • 或者需要多个零件组装的场景,且创建过程中可以灵活调用不同的步骤或组件。

1.4 原型模式

原型模式,即Prototype,是指创建新对象的时候,根据现有的一个原型来创建。

对于普通类,我们如何实现原型拷贝?Java的Object提供了一个clone()方法,它的意图就是复制一个新的对象出来,我们需要实现一个Cloneable接口来标识一个对象是“可复制”的:

public class Student implements Cloneable {
    private int id;
    private String name;
    private int score;

    // 复制新对象并返回:
    public Object clone() {
        Student std = new Student();
        std.id = this.id;
        std.name = this.name;
        std.score = this.score;
        return std;
    }
}

使用的时候,因为clone()的方法签名是定义在Object中,返回类型也是Object,所以要强制转型,比较麻烦:

Student std1 = new Student();
std1.setId(123);
std1.setName("Bob");
std1.setScore(88);
// 复制新对象:
Student std2 = (Student) std1.clone();
System.out.println(std1);
System.out.println(std2);
System.out.println(std1 == std2); // false

实际上,使用原型模式更好的方式是定义一个copy()方法,返回明确的类型:

public class Student {
    private int id;
    private String name;
    private int score;

    public Student copy() {
        Student std = new Student();
        std.id = this.id;
        std.name = this.name;
        std.score = this.score;
        return std;
    }
}

原型模式应用不是很广泛,因为很多实例会持有类似文件、Socket这样的资源,而这些资源是无法复制给另一个对象共享的,只有存储简单类型的“值”对象可以复制。

1.5 单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式(Singleton)的目的是为了保证在一个进程中,某个类有且仅有一个实例。

基础实现方式

因为这个类只有一个实例,因此不能让调用方使用new Xyz()来创建实例了。
所以,单例的构造方法必须是private,这样就防止了调用方自己创建实例,但是在类的内部,是可以用一个静态字段来引用唯一创建的实例的:

实例:

public class Singleton {
    // 静态字段引用唯一实例:
    private static final Singleton INSTANCE = new Singleton();

    // 通过静态方法返回实例:
    public static Singleton getInstance() {
        return INSTANCE;
    }

    // private构造方法保证外部无法实例化:
    private Singleton() {
    }
}

或者直接把static变量暴露给外部:

public class Singleton {
    // 静态字段引用唯一实例:
    public static final Singleton INSTANCE = new Singleton();

    // private构造方法保证外部无法实例化:
    private Singleton() {
    }
}

单例模式的实现方式很简单:

  1. 只有private构造方法,确保外部无法实例化;
  2. 通过private static变量持有唯一实例,保证全局唯一性;
  3. 通过public static方法返回此唯一实例,使外部调用方能获取到实例。

利用enum实现

另一种实现Singleton的方式是利用Java的enum,因为Java保证枚举类的每个枚举都是单例,所以我们只需要编写一个只有一个枚举的类即可:

public enum World {
    // 唯一枚举:
	INSTANCE;

	private String name = "world";

	public String getName() {
		return this.name;
	}

	public void setName(String name) {
		this.name = name;
	}
}

枚举类也完全可以像其他类那样定义自己的字段、方法,这样上面这个World类在调用方看来就可以这么用:

String name = World.INSTANCE.getName();

使用枚举实现Singleton还避免了第一种方式实现Singleton的一个潜在问题:即序列化和反序列化会绕过普通类的private构造方法从而创建出多个实例,而枚举类就没有这个问题。

采用"约定"实现

即采用约定的方式,把普通类视作单例

那我们什么时候应该用Singleton呢?实际上,很多程序,尤其是Web程序,大部分服务类都应该被视作Singleton,如果全部按Singleton的写法写,会非常麻烦,所以,通常是通过约定让框架(例如Spring)来实例化这些类,保证只有一个实例,调用方自觉通过框架获取实例而不是new操作符:

@Component // 表示一个单例组件
public class MyService {
    ...
}

因此,除非确有必要,否则Singleton模式一般以“约定”为主,不会刻意实现它。

posted @ 2020-05-08 19:13  唐啊唐囧囧  阅读(286)  评论(0编辑  收藏  举报