设计模式学习笔记(持续更新)

545 阅读16分钟

设计模式

设计模式的原则目的

设计模式是为了让程序有更好的:

  1. 代码重用性(相同的代码,不用多次编写)
  2. 可读性(编写规范性,便于其他程序员的阅读和理解)
  3. 可扩展性(当需要增加新的功能时候,非常的方便和容易)
  4. 可靠性(当增加新的功能时候,对原来的功能没有影响)
  5. 使程序呈现高内聚,低耦合的特性

设计模式七大设计原则

单一职责原则

基本介绍

对类来说,即一个类只负责一项职责。如类A负责两个不同职责:职责1和职责2。当职责1需求变更而改变A时,也有可能导致职责2执行错误,因此需要把类A的粒度分解为A1与A2。

注意事项和细节
  1. 降低类的复杂度,一个类只负责一项职责。
  2. 提高类的可读性,可维护性。
  3. 降低变更引起的风险。
  4. 通常情况下,我们应当遵循单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则;只有类中方法数量足够少,可以在方法级别保持单一职责原则。

接口隔离原则

基本介绍

客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。接口尽量细化,同时接口中的方法尽量少。

依赖倒置原则

基本介绍

通过抽象(接口或者抽象类)使各个类或模块的实现彼此独立,不相互影响,实现模块间的松耦合。

中心思想

面向接口编程

注意事项和细节
  1. 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好。
  2. 变量的声明类型尽量是抽象类或接口,这样我们的变量引用和实际对象间就存在一个缓冲层,利于程序的扩展和优化。
  3. 继承是遵循里式替换原则。

里式替换原则

基本介绍

父类能出现的地方子类就可以出现,而且替换成子类也不会出现任何错误或者异常,而使用者也无需知道父类还是子类。

注意事项和细节
  1. 在使用继承时,子类尽量不要重写父类的方法。
  2. 里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当的情况下,可以通过聚合、组合、依赖来解决问题。

开闭原则

基本介绍

软件实体(包括类、模块、功能等)应该对扩展开放,但是对修改关闭。

迪米特法则(最少知道法则)

基本介绍
  1. 一个类应该对自己需要耦合或调用的类知道得最少。
  2. 类与类的关系越密切,耦合度越大。
  3. 迪米特法则又称最少知道法则,即一个类对自己依赖的类知道的越少越好
  4. 迪米特法则还有个更简单的定义:只与直接的朋友通信

UML类图

UML基本介绍

UML--Unified modeling language(统一建模语言),是一种用于软件系统分析和设计的语言工具,它用于帮助软件开发人员进行思考和思路的结果。 ####UML类图分类

  1. Dependency 表示依赖(使用)---->只要是在类中用到了对方,那么他们之间就存在依赖关系。如果A用到了B,表明A依赖B,即A——>B。
  2. Association 表示关联----->表示类与类之间的联系是依赖的特例,如果A用到了B,即表示A——>B。
  3. Generalization 表示泛化(继承)---->实际上就是继承关系,是依赖关系的特例。如果A继承B,即表明A——>B。
  4. Realization 表示实现---->A类实现B接口,是依赖关系的特例。如果A实现B,即表明A——>B。
  5. Aggregation 表示聚合----->表示整体和部分的关系,整体与部分可以分开是关联关系的特例
  6. Composite 表示组合----->表示整体和部分的关系,整体与部分不可以可以分开是关联关系的特例

设计模式概述

设计模式介绍

  1. 设计模式是程序员在面对同类软件工程设计问题所总结出来的有用的经验,模式不是代码,而是某类问题的通用解决方案,设计模式(Design Pattern) 代表了最佳的实践。
  2. 设计模式的本质提高软件的维护性,通用性和扩展性,并降低软件的复杂度。

设计模式的类型

  1. 创建型模式:单例模式、抽象工厂模式、原型模式、建造者模式、工厂模式。
  2. 结构性模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
  3. 行为型模式:模板方法模式、命令模式、访问者模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式。

单例设计模式

单例设计模式介绍

所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。

单例设计模式八种方式

  1. 饿汉式(静态常量)
  2. 饿汉式(静态代码块)
  3. 懒汉式(线程不安全)
  4. 懒汉式(线程安全,同步方法)
  5. 双重检查
  6. 静态内部类
  7. 枚举

饿汉式(静态常量)

步骤
  1. 构造类私有化(防止外部调用产生新的对象)
  2. 类的内部创建对象
  3. 向外暴露一个静态公共方法。
代码
class Singleton {
    private Singleton() {

    }

    private static final Singleton singleton = new Singleton();

    public static Singleton getInstance() {
        return singleton;
    }
}
优缺点
优点

这种写法比较简单,就是在类装载的时候就完成实例化,避免了线程同步的问题。

缺点

在类加载的时候就完成了实例化,没有达到懒加载的效果。如果从始至终都没有用过这个实例,则会造成内存的浪费。

结论

这种单例设计模式可用,但是可能会出现内存浪费。

饿汉式(静态代码块)

代码
class Singleton {
    private Singleton() {

    }
    private static Singleton singleton;
    static {
        singleton = new Singleton();
    }

    public static Singleton getInstance() {
        return singleton;
    }
}
优缺点

与饿汉式(静态常量)的方式类似,只不过将类实例化的过程放在了静态代码块中,优缺点与饿汉式(静态常量)一样。

懒汉式(线程不安全)

代码
class Singleton {
    private Singleton() {

    }
    private static Singleton singleton;
    /**
     * 当调用的时候再创建对象,但是线程不安全。比如线程1执行到singleton为空,此时线程2也执行到此为止,然后就会产生两个对象。
     * @return
     */
    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}
优缺点
  1. 起到了懒加载(Lazy Loading)的效果,但是线程不安全,只能在单线程下使用。
  2. 如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这是便会产生多个实例。因此在多线程的环境下不可以使用这个方式。

懒汉式(线程安全,同步方法)

代码
class Singleton {
    private Singleton() {

    }
    private static Singleton singleton;

    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}
优缺点
  1. 解决了线程安全问题
  2. 效率低下,每个线程在获取类的实例的时候,都需要执行同步方法。
  3. 不推荐这种方法。

双重检查

代码
class Singleton {
    private Singleton() {

    }
    //为了避免初始化操作的指令重排序
    private static volatile Singleton singleton;

    public static  Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }

            }
        }
        return singleton;
    }
}
优缺点
  1. Double-Check概念是多线程开发中经常使用到的,进行两次检查,就可以保证线程安全了。
  2. 实例代码只用执行一次,实现了懒加载。
  3. 线程安全,延迟加载,效率高。

静态内部类

代码
class Singleton {
    private Singleton() {

    }
    private static class SingletonInstance{
        private static final Singleton INSTANCE = new Singleton();
    }

    public static  Singleton getInstance() {
        return SingletonInstance.INSTANCE;
    }
}
优缺点
  1. 这种方式采用了类装载的机制来保证初始化实例时只有一个线程。
  2. 当调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
  3. 类的静态属性只会在第一次加载类的时候初始化,JVM帮助我们保证了线程的安全性。
  4. 推荐使用

枚举

代码
enum Singleton{
    INSTANCE;
}
分析
  1. 枚举类实现其实省略了private的构造方法
  2. 枚举类的域(field)其实是相应的enum类型的一个实例对象。

单例模式在JDK应用的源码分析

java.lang.Runtime就是经典的单例模式(饿汉式),代码如下:

 private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}

单例模式注意事项和细节说明

  1. 单例模式保证系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。
  2. 单利模式使用的场景,需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多,但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象。

工厂模式

简单工厂模式

通过举个例子说明一下: 我喜欢养宠物,抽象一个宠物父类或者接口。

/**
 * 描述要养的宠物
 *
 * @author bf
 * @date 2019/9/18 17:01
 */
public interface Animal {
    void getAnimal();
}

先养一个小狗吧:

public class Dog implements Animal {
    @Override
    public void getAnimal() {
        System.out.println("养了一只小狗");
    }
}

再养一只小猫:

public class Cat implements Animal {
    @Override
    public void getAnimal() {
        System.out.println("养了一只小猫");
    }
}

再养一只竹鼠:

public class Mouse implements Animal {
    @Override
    public void getAnimal() {
        System.out.println("养一只竹鼠");
    }
}

准备工作完成了,我们去宠物馆(简单工厂类),宠物种类如下:

public class SimpleAnimalFactory {
    private static final int TYPE_DOG = 1;
    private static final int TYPE_CAT = 2;
    private static final int TYPE_MOUSE = 3;

    public static Animal createAnimals(int type) {
        switch (type) {
            case TYPE_DOG:
                return new Dog();
            case TYPE_CAT:
                return new Cat();
            case TYPE_MOUSE:
                return new Mouse();
            default:
                return null;
                
        }
    }

}

简单宠物馆就提供三种动物,你说要什么,他就给你什么。这里我要了一只狗:

public static void main(String[] args) {
        Animal animals = SimpleAnimalFactory.createAnimals(1);
        animals.getAnimal();
    }

输出如下:

养了一只小狗

特点
  1. 简单工厂模式是一个具体的类,并不是接口抽象类。
  2. 由于createAnimals()方法是静态的,所以也称之为静态工厂
缺点
  1. 扩展性差(比如我要添加一个宠物的种类,还需要修改工厂类的方法)。

通过反射创建的简单工厂模式

使用反射实现简单工厂:

 public static <T> T createAnimals(Class<T> clz) throws Exception {
        T result = null;
        result = (T) Class.forName(clz.getName()).newInstance();
        return result;
    }

买宠物时调用:

 Dog dog = SimpleReflectAnimalFactory.createAnimals(Dog.class);
 dog.getAnimal();
优缺点
  1. 当需要添加宠物的种类时,不需要修改工厂类的代码
  2. Class.forName(clz.getName()).newInstance()调用的是无参构造方法,它和new对象是一样的性质,而工厂方法应该用于复杂对象的初始化,当需要调用有参的构造方法时便无能为力了。

工厂方法模式

工厂方法模式是简单工厂的进一步深化,在工厂方法模式中,我们不再提供一个统一的工厂类来创建所有的对象,而是针对不同的对象提供不同的工厂。也就是说每一个对象都有一个与之对应的工厂

定义

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

我们可以先通过实例来详细解释一下这个定义:

实例

依然使用养宠物的例子: 首先,编写一个宠物接口:

public interface Animal {
    void getAnimal();
}

养狗的代码:

public class Dog implements Animal {
    @Override
    public void getAnimal() {
        System.out.println("养了一只小狗");
    }
}

养猫的代码:

public class Cat implements Animal {
    @Override
    public void getAnimal() {
        System.out.println("养了一只小猫");
    }
}

养竹鼠的代码:

public class Mouse implements Animal {
    @Override
    public void getAnimal() {
        System.out.println("养一只竹鼠");
    }
}

现在我们按照定义所说定义一个抽象的工厂接口GetAnimalFactory

public interface GetAnimalFactory {
    Animal getAnimal();
}

Dog加载器工厂

public interface GetAnimalFactory {
    Animal getAnimal();
}

Cat加载器工厂

public class GetCat implements GetAnimalFactory {
    @Override
    public Animal getAnimal() {
        return new Cat();
    }
}

Mouse加载器工厂

public class GetMouse implements GetAnimalFactory {
    @Override
    public Animal getAnimal() {
        return new Mouse();
    }
}

和简单工厂对比一下,最根本的区别在于简单工厂只有一个统一的工厂类,而工厂方法是针对每个要创建的对象都会提供一个工厂类。

抽象工厂模式

定义

提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。(在抽象工厂模式中,每一个具体工厂都提供了多个工厂方法用于产生多种不同类型的对象) 抽象工厂可以划 分为4大部分:

  1. AbstractFactory(抽象工厂):声明了一组用于创建对象的方法,注意是一组。
  2. ConcreteFactory(具体工厂):它实现了抽象工厂中声明的创建对象的方法,生成一组具体对象。
  3. AbstractProduct(抽象产品):它为每种对象声明接口,在其中声明了对象所具有的业务方法。
  4. ConcreteProduct(具体产品):它定义具体工厂生产的具体对象。
实例

现在需要做一款跨平台的游戏,需要兼容Android和Ios两个移动操作系统,该游戏针对每个系统都设计了一套操作控制器(OperationController)和界面控制器(UIController),下面通过抽象工厂方式完成这款游戏的架构设计。 先创建两个抽象产品接口。

抽象操作控制器

public interface OperationController {
    void control();
}

抽象界面控制器

public interface UIController {
    void display();
}

然后完成这两个系统平台的具体操作控制器和界面控制器

Android平台

public class AndroidOperationController implements OperationController {
    @Override
    public void control() {
        System.out.println("AndroidOperationController");
    }
}
public class AndroidUIController implements UIController {
    @Override
    public void display() {
        System.out.println("AndroidUIController");
    }
}

Ios

public class IosOperationController implements OperationController {
    @Override
    public void control() {
        System.out.println("IosOperationController");
    }
}

public class IosUIController implements UIController {
    @Override
    public void display() {
        System.out.println("IosUIController");
    }
}

下面定义一个抽象工厂,该工厂可以创建OperationController和UIController

public interface SystemFactory {
    OperationController createOperationController();
    UIController createUIController();
}

然后在各平台具体的工厂类中完成操作控制器和界面控制器的创建过程

Android

public class AndroidFactory implements SystemFactory {
    @Override
    public OperationController createOperationController() {
        return new AndroidOperationController();
    }

    @Override
    public UIController createUIController() {
        return new AndroidUIController();
    }
}

Ios

public class IOSFactory implements SystemFactory {
    @Override
    public OperationController createOperationController() {
        return new IosOperationController();
    }

    @Override
    public UIController createUIController() {
        return new IosUIController();
    }
}

方法调用:

public static void main(String[] args) {
        //android
        AndroidFactory androidFactory = new AndroidFactory();
        OperationController androidOperationController = androidFactory.createOperationController();
        UIController androidUiController = androidFactory.createUIController();
        androidOperationController.control();
        androidUiController.display();

        //ios
        IOSFactory iosFactory = new IOSFactory();
        OperationController iosFactoryOperation = iosFactory.createOperationController();
        UIController iosUiController = iosFactory.createUIController();
        iosFactoryOperation.control();
        iosUiController.display();
    }

针对不同平台只通过创建不同的工厂对象就完成了操作和UI控制器的创建。

使用场景
  1. 和工厂方法一样,客户端不需要知道他所创建的对象的类。
  2. 需要一组对象共同完成某种功能是,并且可能存在多组对象完成不同功能的情况。
  3. 系统接口稳定,不会频繁的增加对象。因为一旦增加就需要修改原有代码,不符合开闭原则

模板方法模式(Template Method Pattern)

定义

定义一个操作中的算法的框架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤。

举例

举个骑共享单车的例子,不管是骑哈罗单车还是骑摩拜单车,我们都要经历扫码解锁、骑车、上锁、支付这四个过程,因此可以设计成一个抽象方法。

public abstract class RideBike {
    /**
     * 扫码解锁
     */
    public abstract void unlock();

    /**
     * 骑车
     */
    public abstract void ride();

    /**
     * 上锁
     */
    public abstract void lock();

    /**
     * 支付
     */
    public abstract void pay();

    /**
     * 模拟使用共享单车
     */
    public abstract void use();

}

上班去了,开始去骑共享单车,发现了一辆哈罗单车,开始使用。

public class RideHelloBike extends RideBike {
    @Override
    public void unlock() {
        System.out.println("扫码解锁哈罗单车");
    }

    @Override
    public void ride() {
        System.out.println("开启骑哈罗单车");
    }

    @Override
    public void lock() {
        System.out.println("给哈罗单车上锁");
    }

    @Override
    public void pay() {
        System.out.println("支付哈罗单车的费用");
    }

    @Override
    public void use() {
        this.unlock();
        this.ride();
        this.ride();
        this.pay();
    }

}

终于下班了,发现公司楼下的哈罗单车都被骑完了,那就骑摩拜单车吧。

public class RideMoBaiBike extends RideBike {
    @Override
    public void unlock() {
        System.out.println("扫码解锁摩拜单车");
    }

    @Override
    public void ride() {
        System.out.println("开启骑摩拜单车");
    }

    @Override
    public void lock() {
        System.out.println("给摩拜单车上锁");
    }

    @Override
    public void pay() {
        System.out.println("支付摩拜单车的费用");
    }

    @Override
    public void use() {
        this.unlock();
        this.ride();
        this.ride();
        this.pay();
    }

}

骑摩拜和骑哈罗都用到了同样的方法,user()方法,代码重复了,这是病得治,药房就是使用模板方法模式。

模板方法模式相信大家都用过,就是抽象类里面的方法,不需要改变的方法。那么,下面我们开始编写代码。

抽象类代码

public abstract class AbstractClass {
    protected abstract void unlock();
    protected abstract void ride();
    protected abstract void lock();
    protected abstract void pay();

    protected final void use() {
        this.unlock();
        this.ride();
        this.lock();
        this.pay();
    }

}

这里说明一下:使用protected,只有同包下的父子类可以访问。 final修饰的方法表明子类不能重写父类方法。

实现类代码

public class ScanBicycle extends AbstractClass {
    @Override
    protected void unlock() {
        System.out.println("扫码解锁");
    }

    @Override
    protected void ride() {
        System.out.println("骑车");
    }

    @Override
    protected void lock() {
        System.out.println("上锁");
    }

    @Override
    protected void pay() {
        System.out.println("支付");
    }



    public static void main(String[] args) {
        ScanBicycle bicycle = new ScanBicycle();
        bicycle.use();
    }
}

运行结果如下:

public class ScanBicycle extends AbstractClass {
    @Override
    protected void unlock() {
        System.out.println("扫码解锁哈罗单车");
    }

    @Override
    protected void ride() {
        System.out.println("骑哈罗车");
    }

    @Override
    protected void lock() {
        System.out.println("上哈罗锁");
    }

    @Override
    protected void pay() {
        System.out.println("支付哈罗费用");
    }
    
    public static void main(String[] args) {
        ScanBicycle bicycle = new ScanBicycle();
        bicycle.use();
    }
}

优缺点及使用场景
优点
  1. 良好的封装性。把共有不变的方法封装在父类,而子类负责其他需要改变方法的实现逻辑。即封装不变部分,扩展可变部分
  2. 良好的扩展性。增加功能由子类实现基本方法拓展,符合单一职责原则和开闭原则。
  3. 复用代码。
缺点
  1. 由于是通过继承实现代码复用来改变算法,灵活度降低。
  2. 子类的执行影响父类的结果,增加代码的阅读难度。
使用场景
  1. 多个子类有公有方法,并且逻辑基本相同时。
  2. 重要、复杂的算法,可以把核心算法设计为模板方法,周边的相关细节功能则由各个子类实现。
总结

模板方法看上去简单,但是整个模式涉及到的都是面向对象设计的核心,比如继承封装、基于继承代码的复用、方法实现等等。当中还有涉及到一些关键词的使用,也是我们在Java编程中需要掌握的基础。

建造者模式