原创
最近更新: 2022/10/17 17:04

设计模式 (一) 创建型模式

设计模式 | 菜鸟教程

黑马程序员Java设计模式详解

目录

设计模式 (一) 创建型模式

设计模式 (二) 结构型模式

设计模式 (三) 行为型模式


软件设计原则

开闭原则

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

在程序需要进行拓展的时候,不能修改原有的代码。使程序的扩展性好,耦合度低,易于维护和升级。

实现方法:接口和抽象类。当程序需要拓展新的功能时,只需要在原有基础上派生出新的类即可。要求在最初设计抽象类时就进行合理抽象。

依赖倒转原则

高层模块不应该依赖低层模块,两者都应该依赖其抽象。抽象不应该依赖细节,细节应该依赖抽象。

简单来说,在设计程序原型时,使用的数据类型(包括成员变量、参数、返回值等)应当是抽象类型,只在实例化时使用具体类。即面向接口编程。

依赖倒转原则是对开闭原则的一个具体描述。

里氏代换原则

任何父类可以出现的地方,子类一定可以出现。子类可以在任何情况下代替父类的位置。

子类可以扩展父类的功能,但不能改变父类原有的功能。这要求子类应当尽量少地对父类的具体实现方法进行重写,尤其减少多态的运用。重写会导致整个体系的复用性变差,特别是多态运用频繁时,程序出错的概率会变大。

接口隔离原则

一个类对另一个类的依赖应该建立在最小的接口上。

一个类不应该拥有它不需要的功能,如果一个类的父类/接口拥有这个类不需要的功能,那么这个继承关系/实现关系就是有问题的。

解决方法:对父类进行进一步的抽象,或更细粒度地拆分接口。

最少知识原则

又叫迪米特法则。一个类应当尽可能少地了解其他的类,如果两个类之间能够不直接通信,那么就不应当互相调用,可以通过第三方转发该调用。目的是降低类之间的耦合度。

一般而言,一个类应当尽可能地只与成员对象、方法参数、自己创建的对象等进行通信。

合成复用原则

尽量使用组合或者聚合等关联关系(一个类是另一个类的成员变量)来实现功能,其次再考虑继承关系。

继承的缺点:

  • 继承复用破坏了类的封装性,将父类的实现细节暴露给子类。又叫白箱复用。
  • 导致父类与子类的耦合度高,当父类需要改动时,会导致子类也发生变化。
  • 限制了复用的灵活性。从父类继承的属性和方法是在编译时确定的。

组合或聚合复用的优点:

  • 维持了类的封装性,因为成员变量无法看到类的内部细节。又叫黑箱复用。
  • 耦合度低,可以在成员变量声明位置使用抽象类。
  • 复用的灵活性高。成员变量可以动态引用对象。

单例模式

当一个类在程序生命周期内只需要/只能有一个实例时,需要使用单例模式进行设计。

单例模式保证了在任何情况下,这个类最多只有一个实例,任何对该类的创建和访问行为都将指向这个实例。

实现要点:

  1. 构造方法私有,不允许外部实例化
  2. 在类内部设置一个私有静态成员变量来存储唯一的实例
  3. 提供一个外部可调用的公共静态方法

单例模式分为两种:

饿汉式

实例化在类加载时完成。

public class Singleton {

    //在类的内部设置一个成员变量用来存储唯一的实例,需要用static修饰
    //因为是饿汉式,此处直接new一个实例
    private static Singleton instance = new Singleton();

    /*Java语言也可以使用静态代码块赋值
    private static Singleton instance;
    static {
        instance = new Singleton();
    }
    */

    //私有构造方法
    private Singleton() {}

    //提供一个外部的访问方法,因为是类方法,所以用static修饰
    public static Singleton getInstance() {
        return instance;
    }
}

饿汉式的方法会在程序加载时就创建单例实例,实例存储在内存中。如果此时程序无须访问这个实例,就会导致内存的浪费。于是可以将懒加载技术引入单例模式,即为懒汉式。

懒汉式

类加载时不进行实例化,实例化在第一次使用该对象时完成。

//“双重检查锁”懒汉式

public class Singleton2 {

    //volatile关键字用于禁用Java编译器的指令重排序,保证线程安全
    private static volatile Singleton2 instance;

    private Singleton2() {}

    public static Singleton2 getInstance() {
        //在访问对象时使用懒加载
        /* 线程不安全的写法:当两个线程同时越过判空语句时,会创建两个实例
            if (instance == null) {
                instance = new Singleton2();
            }*/

        //考虑到线程安全问题,需要加锁,使用“双重检查锁”模式
        if (instance == null) {//判断部分无须加锁
            //只需要锁住实例化的部分即可
            synchronized (Singleton2.class) {
                //注意此处需要再次检查空指针,防止多个线程同时越过上面的检查
                if (instance == null) {
                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }
}
//“静态内部类”懒汉式

public class Singleton3 {

    /*
    * 使用一个静态内部类来存储单例
    * 内部类必须是静态的,只有静态内部类才可以拥有静态成员变量
    * 外部类加载时,内部类并不会被立刻加载,单例也就不会被实例化
    * */
    private static class SingletonHolder {
        public static Singleton3 INSTANCE = new Singleton3();
    }

    private Singleton3() {}

    public static Singleton3 getInstance() {
        //在访问对象时会先检查内部类是否被加载,未加载时会先加载内部类,此时单例被实例化
        //因为类的加载是单线程的,不会出现线程同步问题
        return SingletonHolder.INSTANCE;
    }
}

单例模式的破坏

上述单例模式的实现方法存在缺陷,可能会被破坏。

  1. 序列化和反序列化方法
  • 当单例对象通过序列化的方式存储到文件中,再多次读取出来时,一般懒汉式和饿汉式的实现方法所获取的单例对象不是同一个。

  • 可以通过实现readResolve()方法解决。Java反序列化调用readObject()方法时会自动调用该方法。

private Object readResolve() {
    //在此处实现getInstance()方法完全相同的功能
}
  1. 反射方法
  • 使用Java自带的反射方法可以破坏单例类的访问控制,然后通过调用构造函数来实例化多个不同对象。

  • 解决方法:在私有的构造函数中加入判断

private Singleton() {
    if (instance != null) {
        throw new RuntimeException();
    }
    //如果是静态内部类的方式,则可以设置一个flag变量进行判断
    //当然,反射也可以修改flag的值,所以这也并非完全安全,但是无所谓了
}

使用枚举实现

属于饿汉式的一种特殊实现。是所有单例实现中唯一一种不会被破坏的单例模式。

/*
* 枚举饿汉式
* */

class Singleton {}

enum GetSingleton {
    INSTANCE;
    private Singleton instance;
    //此处的GetSingleton()是默认private的
    GetSingleton(){
        instance = new Singleton();
    }
    public Singleton getInstance(){
        return instance;
    }
    //使用GetSingleton.INSTANCE.getInstance();获取单例对象
}

工厂模式

在面向对象编程中,如果要创建对象时都必须将其new出来,就会导致程序和该对象耦合严重。当需求变化时,就需要将对应的代码都修改一遍,这违背了开闭原则。

工厂模式就是用来解决这一问题。设计一个工厂类来实例化对象,当我们需要一个新对象时,只需要从工厂类中获取即可。

简单工厂模式

包含以下类型:

  • 抽象产品:实例的抽象,定义了产品的规范,描述产品的共有特性
  • 具体产品:可实例化的类
  • 具体工厂:提供创建产品的方法
//伪代码
//简单工厂类
public class SimpleFactory {

    //生产方法可以设置为静态static,可以省去创建工厂对象的步骤
    public Item createItem(String type) {
        Item item = null;
        if (type.equals("type1")) {
            item = new ItemType1();
        } else if (type.equals("type2")) {
            item = new ItemType2();
        } else {
            throw new RuntimeException("...");
        }
        return item;
    }
}

//========================

//商店类,调用工厂类进行生产
public class Store {

    public Item newItem(String type) {
        SimpleFactory factory = new SimpleFactory();
        Item item = factory.createItem(type);
        return item;
    }
}

优点:将客户端的业务层逻辑与具体对象的创建解耦合,修改时只需要修改工厂类,降低修改客户端代码的可能性。

缺点:工厂类与具体的对象耦合了,违背了开闭原则。当添加新的产品时,需要修改工厂类的代码。

工厂方法模式

定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象。工厂方法使一个产品的实例化延迟到其工厂的子类.

包含以下类型:

  • 抽象工厂:提供创建产品的接口供外部调用

  • 具体工厂:实现具体的创建产品的方法

  • 抽象产品:实例的抽象,定义了产品的规范,描述产品的共有特性

  • 具体产品:可实例化的类

//伪代码
//抽象工厂接口
public interface Factory {
    //创建对象的方法
    Item createItem();
}

//===========================

//具体工厂类1
public class ItemType1Factory implements Factory {

    public Item createItem() {
        return new ItemType1();
    }
}

//具体工厂类2
public class ItemType2Factory implements Factory {

    public Item createItem() {
        return new ItemType2();
    }
}

//===========================

//商店类
public class Store {

    private Factory factory;

    public void setFactory(Factory factory) {
        this.factory = factory;
    }

    public Item newItem() {
        return factory.createItem();
    }
}

优点:当添加一种新的产品时,不需要再修改已有的工厂类,只需要新建对应的工厂接口的实现即可。

缺点:每增加一个具体产品就要增加一个产品类和工厂类,复杂度高。

抽象工厂模式

考虑更加复杂的情况:工厂不仅仅生产特定的几种产品,而是需要生产若干组不同的产品组,每一组又包含不同种的产品。如果继续按照上面的工厂模式,则需要大量的产品类和工厂类,出现类爆炸情况。

以汽车厂为例子。工厂既要生产发动机,又要生产车壳等;而汽车厂又分为奔驰汽车场、丰田汽车厂等。

包含以下类型:

  • 抽象工厂:提供多个创建产品的接口供外部调用。抽象的汽车工厂要提供发动机、车壳等多个产品的接口。

  • 具体工厂:实现具体的创建产品的方法,每个产品的方法都需要实现。根据不同商标实现不同的工厂。

  • 抽象产品:与抽象工厂是多对一的关系。有发动机、车壳等产品。

  • 具体产品:可实例化的类。每个品牌的每类产品都有对应的产品。

实现略,没太多意思。

优点:当一组产品需要被同时使用的时候,可以保证程序中同时只出现一套产品。 缺点:当产品种类变得复杂,所有的工厂类都要修改。


原型模式

先储备一个实例作为原型,当需要一个新对象的时候,复制这个实例。

使用场景:

  • 涉及多态的场景:一个父类的指针指向子类的实例,此时可能无法获取子类的具体类别从而调用对应的构造函数,此时使用clone方法。
  • 待创建的新对象需要复制某个现有对象的某个运行时状态,此时使用clone方法比new一个再赋值效率要高。
  • 初始化一个新对象要消耗大量资源,或者要求访问权限。
  • 一个对象要分享给不同用户访问,且访问过程可能修改对象状态时,可以分发该对象的克隆。

常常和工厂方法同时使用,工厂通过克隆方法获取新对象,再返回给调用者。

包含以下组件:

  • 抽象原型类:一般是一个接口,规定了clone()方法。Java中提供了Cloneable接口。
  • 具体原型类:实现具体的clone()复制方法

浅克隆:创建的新对象中,引用类型的成员变量与原型共享。适用于不做修改的情况。 深克隆:新对象中所有的成员变量都要迭代地克隆,所有零件都是全新的。

实现代码较为简单,略。


建造者模式

将一个复杂对象的构建与表示分离,同样的构建过程可以创建不同的表示。

包含以下组件:

  • 抽象建造者类:Builder接口,规定了要实现复杂对象的哪些部分。

  • 具体建造者类:实现Builder接口,完成各个组件的创建。

  • 产品类:要创建的复杂对象。

  • 指挥者类:负责装配,调用具体建造者来创建复杂对象的各个组件,协调与保证对象各部分完整建造。

在简化的系统下,指挥者类可以和建造者类相结合。

在使用建造者模式的场景中,产品类和建造者类是较为稳定的,业务模式一般封装在指挥者类中以保持系统整体的稳定性。常用于组件变化复杂,但整体的建造过程相对稳定,且组件与建造过程相互独立的业务场景。

举例来说,烹饪蛋炒饭的过程中,先炒蛋再炒饭是一种产品,先炒饭再炒蛋是另一种产品,此时就可以用建造者模式。

//产品和组件类
public class PartA {}
public class PartB {}

public class Product {
    private PartA partA;
    private PartB partB;

    public void setPartA(PartA a) {
        this.partA = a;
    }
    public void setPartB(PartB b) {
        this.partB = b;
    }
}

//=====================

//抽象建造者类
public abstract class Builder {
	//标记为protected,方便子类使用
    protected Product product = new Product();

    public abstract void bulidPartA();
    public abstract void bulidPartB();
    public abstract Product createProduct();
}

//具体建造者类
public class BuilderA extends Builder {
    //建造
    public void bulidPartA() {
        this.product.setPartA(new PartA());
    }
    public void bulidPartB() {
        this.product.setPartB(new PartB());
    }

    public Product createProduct() {
        return product;
    }
}

//=====================

//指挥者类
public class Director {

    //指挥者不直接持有产品,而是通过建造者间接持有产品
    private Builder builder;

    public Director(Builder builder) {
        this.builder = builder;
    }

    //调用者最终是通过这个方法获取产品
    public Product construct() {
        //指挥者类需要协调建造顺序
        builder.bulidPartA();
        builder.bulidPartB();
        return builder.createProduct();
    }
}

优点:

  • 建造者模式具有良好的封装性,易于扩展。当产品需求的组件发生变化时,只需要增减建造者即可,无须修改已有代码,不易引入新的风险。
  • 将产品与其创建过程解耦,客户端不必知道产品的创建过程。
  • 将复杂产品的创建步骤分解,能够精细地控制生产流程。

缺点:如果不同产品之间工艺流程差异较大,则不适用建造者模式,使用范围受限。

扩展:用建造者模式对构造函数进行重构

当一个类的构造需要传入很多参数的时候,可能会导致创建类实例的代码可读性差,此时可以用建造者模式进行重构。

public class Product {

    private String a;
    private String b;
    private String c;

    /*传统方法需要一个冗长的构造函数
    public Product(String a, ...) {...}
    */

    //使用建造者模式进行初始化
    //注意此处禁止了外部调用构造函数
    private Product(Builder builder) {
        this.a = builder.a;
        this.b = builder.b;
        this.c = builder.c;
    }

    public static final class Builder {
        private String a;
        private String b;
        private String c;

        //返回this以方便链式调用
        public Builder a(String a) {
            this.a = a;
            return this;
        }
        public Builder b(String b) {
            this.b = b;
            return this;
        }
        public Builder c(String c) {
            this.c = c;
            return this;
        }

        //最终通过这个方法返回新的产品实例
        public Product build() {
            return new Product(this);
        }
    }
}
//外部调用的方法
Product product = new Product.Builder()
							 .a("a")
							 .b("b")
							 .c("c")//到此处为止是在构造Builder
							 .build();//使用Builder创造产品

评论区