什么是设计模式
设计模式(Design Pattern)是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。
1995 年,GoF(Gang of Four,四人组/四人帮)合作出版了《设计模式:可复用面向对象软件的基础》一书,共收录了 23 种设计模式,从此树立了软件设计模式领域的里程碑,人称:GoF设计模式。
设计模式的六大原则
在讲到常用的设计模式之前,首先介绍设计模式的六大原则,它们分别是单一职责原则、开放封闭原则、里氏替换原则、依赖倒置原则、迪米特原则和接口隔离原则。
单一职责原则
定义: 就一个类而言, 应该仅有一个引起它变化的原因。
从这句定义我们很难理解它的含义, 这通俗地讲就是我们不要让一个类承担过多的职责。 如果一个类承担的职责过多, 就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。
这种耦合会导致脆弱的设计, 当变化发生时, 设计会遭受到破坏。比如我们会看到一些Android开发者在Activity中写Bean文件、 网络数据处理, 如果有列表的话 Adapter 也写在 Activity 中。
开放封闭原则
定义: 类、 模块、 函数等应该是可以拓展的, 但是不可修改。
开放封闭有两个含义: 一个是对于拓展是开放的, 另一个是对于修改是封闭的。用开放封闭原则解决就是增加一个抽象的功能类。当需求变化时,这样我们再新增功能, 就会发现自己无须修改原有的类, 只需要添加一个功能类的子类实现功能类的方法就可以了。
因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。
里氏替换原则
定义: 所有引用基类(父类) 的地方必须能透明地使用其子类的对象。
里氏替换原则告诉我们, 在软件中将一个基类对象替换成其子类对象, 程序将不会产生任何错误和异常; 反过来则不成立, 如果一个软件实体使用的是一个子类对象的话, 那么它不一定能够使用基类对象。里氏替换原则是实现开放封闭原则的重要方式之一。
通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。 如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。
在运用里氏替换原则时, 尽量把父类设计为抽象类或者接口, 让子类继承父类或实现父接口, 并实现在父类中声明的方法。 运行时, 子类实例替换父类实例, 我们可以很方便地扩展系统的功能, 同时无须修改原有子类的代码; 增加新的功能可以通过增加一个新的子类来实现。 里氏替换原则是开放封闭原则的具体实现手段之一。
依赖倒置原则
定义: 高层模块不应该依赖低层模块, 两者都应该依赖于抽象。 抽象不应该依赖于细节, 细节应该依赖于抽象。其核心思想是:要面向接口编程,不要面向实现编程。
在Java中, 抽象指接口或者抽象类, 两者都是不能直接被实例化的; 细节就是实现类, 实现接口或者继承抽象类而产生的就是细节, 也就是可以加上一个关键字new产生的对象。 高层模块就是调用端, 低层模块就是具体实现类。
依赖倒置原则在Java中的表现就是, 模块间的依赖通过抽象发生, 实现类之间不发生直接依赖关系, 其依赖关系是通过接口或者抽象类产生的。 如果类与类直接依赖细节, 那么就会直接耦合。
在实际编程中只要遵循以下4点,就能在项目中满足这个规则。
- 每个类尽量提供接口或抽象类,或者两者都具备。
- 变量的声明类型尽量是接口或者是抽象类。
- 任何类都不应该从具体类派生。
- 使用继承时尽量遵循里氏替换原则。
迪米特原则
定义: 一个软件实体应当尽可能少地与其他实体发生相互作用。
这也被称为最少知识原则。 如果一个系统符合迪米特原则, 那么当其中某一个模块发生修改时, 就会尽量少地影响其他模块。 迪米特原则要求我们在设计系统时, 应该尽量减少对象之间的交互。
如果其中的一个对象需要调用另一个对象的某一个方法, 则可以通过第三者转发这个调用。 简言之, 就是通过引入一个合理的第三者来降低现有对象之间的耦合度。
在将迪米特原则运用到系统设计中时, 要注意下面几点:
- 在类的划分上, 应当尽量创建松耦合的类。 类之间的耦合度越低, 就越有利于复用。 一个处在松耦合中的类一旦被修改, 则不会对关联的类造成太大波及。
- 在类的结构设计上, 每一个类都应当尽量降低其成员变量和成员函数的访问权限。
- 在对其他类的引用上, 一个对象对其他对象的引用应当降到最低。
接口隔离原则
定义: 一个类对另一个类的依赖应该建立在最小的接口上。
建立单一接口, 不要建立庞大臃肿的接口; 尽量细化接口, 接口中的方法尽量少。 也就是说, 我们要为各个类建立专用的接口, 而不要试图建立一个很庞大的接口供所有依赖它的类调用。 采用接口隔离原则。
对接口进行约束时, 要注意以下几点:
- 接口尽量小, 但是要有限度。 对接口进行细化可以提高程序设计的灵活性; 但是如果过小, 则会造成接口数量过多, 使设计复杂化。 所以, 一定要适度。
- 为依赖接口的类定制服务, 只暴露给调用的类它需要的方法, 它不需要的方法则隐藏起来。 只有专注地为一个模块提供定制服务, 才能建立最小的依赖关系。
- 提高内聚, 减少对外交互。 接口方法尽量少用public修饰。 接口是对外的承诺, 承诺越少对系统的开发越有利, 变更风险也会越少。
接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:
- 单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
- 单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。
总结
设计类时明确功能职责不宜过度复杂,对功能进行抽象化接口化提高其扩展性当需求出现变化时不可修改封装类可通过扩展子类来实现。并且子类可以扩展父类的功能,但不能改变父类原有的功能,否则将导致整个继承体系的可复用性变差。
降低模块间的耦合度,模块间的依赖通过接口或者抽象类产生, 具体的实现类之间不发生直接依赖关系。而且应当尽可能少地与其他模块实体发生相互作用。定义单一接口, 不要建立庞大臃肿的接口; 尽量细化接口, 接口中的方法尽量少,但是接口数量过多, 使设计复杂化。
设计模式
GoF提出的设计模式总共有23种, 根据目的准则分类, 分为三大类。
- 创建型设计模式, 共5种: 单例模式、 工厂方法模式、 抽象工厂模式、 建造者模式、 原型模式。
- 结构型设计模式, 共7种: 适配器模式、 装饰模式、 代理模式、 外观模式、 桥接模式、 组合模式、 享元模式。
- 行为型设计模式, 共11种: 策略模式、 模板方法模式、 观察者模式、 迭代器模式、 责任链模式、 命令模式、 备忘录模式、 状态模式、 访问者模式、 中介者模式、 解释器模式。
另外, 随着设计模式的发展也涌现出很多新的设计模式: 它们分别是规格模式、 对象池模式、 雇工模式、 黑板模式和空对象模式等。
创建型设计模式
创建型设计模式, 顾名思义就是与对象创建有关。 创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节,对象的创建由相关的工厂来完成。就像我们去商场购买商品时,不需要知道商品是怎么生产出来一样,因为它们由专门的厂商生产。
- 单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例。
- 工厂方法(FactoryMethod)模式:定义一个用于创建产品的接口,由子类决定生产什么产品。
- 抽象工厂(AbstractFactory)模式:提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。
- 建造者(Builder)模式:将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。
- 原型(Prototype)模式:将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。
以上 5 种创建型模式,除了工厂方法模式属于类创建型模式,其他的全部属于对象创建型模式
单例模式
定义: 某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例。
单例模式有6种写法并且其各有利弊,分别是:饿汉模式、懒汉模式(线程不安全)、懒汉模式( 线程安全)、双重检查模式( DCL)、静态内部类单例模式、枚举单例。
饿汉模式
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
这种方式在类加载时就完成了初始化, 所以类加载较慢, 但获取对象的速度快。 这种方式基于类加载机制, 避免了多线程的同步问题。 在类加载的时候就完成实例化, 没有达到懒加载的效果。 如果从始至终未使用过这个实例, 则会造成内存的浪费。
懒汉模式(线程不安全)
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
懒汉模式声明了一个静态对象, 在用户第一次调用时初始化。 这虽然节约了资源, 但第一次加载时需要实例化, 反应稍慢一些, 而且在多线程时不能正常工作。
懒汉模式( 线程安全)
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
这种写法能够在多线程中很好地工作, 但是每次调用 getInstance 方法时都需要进行同步。 这会造成不必要的同步开销, 而且大部分时候我们是用不到同步的。 所以, 不建议用这种模式。
双重检查模式( DCL)
public class Singleton {
private volatile static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null){
synchronized (Singleton.class){
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这种写法在getSingleton方法中对Singleton进行了两次判空: 第一次是为了不必要的同步, 第二次是在Singleton等于null的情况下才创建实例。 在这里使用volatile会或多或少地影响性能, 但考虑到程序的正确性, 牺牲这点性能还是值得的。
DCL的优点是资源利用率高。 第一次执行getInstance时单例对象才被实例化, 效率高。 其缺点是第一次加载时反应稍慢一些, 在高并发环境下也有一定的缺陷。 DCL虽然在一定程度上解决了资源的消耗和多余的同步、 线程安全等问题, 但其还是在某些情况会出现失效的问题, 也就是DCL失效。 这里建议用静态内部类单例模式来替代DCL。
静态内部类单例模式
public class Singleton {
private Singleton (){}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
}
该模式是利用类加载的特性,当加载一个类时,其内部类不会同时被加载。一个内部类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。
所以只有第一次调用getInstance方法时虚拟机才开始加载 SingletonHolder 并初始化 instance。 这样不仅能确保线程安全, 也能保证 Singleton 类的唯一性。 所以, 推荐使用静态内部类单例模式。
枚举单例
public enum EnumInstance {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumInstance getInstance() {
return INSTANCE;
}
}
默认枚举实例的创建是线程安全的, 并且在任何情况下都是单例。 在上面讲的几种单例模式实现中,有一种情况下其会重新创建对象, 那就是反序列化: 将一个单例实例对象写到磁盘再读回来, 从而获得了一个实例。
反序列化操作提供了readResolve方法, 这个方法可以让开发人员控制对象的反序列化。 在上述几个方法示例中, 如果要杜绝单例对象被反序列化时重新生成对象, 就必须加入如下方法:
private Object readResolve () throws ObjectStreamException {
return instance;
}
枚举单例的优点就是简单, 但是大部分应用开发很少用枚举, 其可读性并不是很高。
单例模式的使用场景
在一个系统中, 要求一个类有且仅有一个对象, 它的具体使用场景如下:
- 整个项目需要一个共享访问点或共享数据。
- 创建一个对象需要耗费的资源过多, 比如访问I/O或者数据库等资源。
- 工具类对象。
简单工厂模式
简单工厂模式(又叫作静态工厂方法模式) , 其属于创建型设计模式, 但是并不属于 23种GoF设计模式之一。 提到它是为了让大家能够更好地理解后面讲到的工厂方法模式。
定义: 简单工厂模式属于创建型模式, 其又被称为静态工厂方法模式, 这是由一个工厂对象决定创建出哪一种产品类的实例。
在简单工厂模式中有如下角色。
- Factory: 工厂类, 这是简单工厂模式的核心, 它负责实现创建所有实例的内部逻辑。 工厂类的创建产品类的方法可以被外界直接调用, 创建所需的产品对象。
- IProduct: 抽象产品类, 这是简单工厂模式所创建的所有对象的父类, 它负责描述所有实例所共有的公共接口。
- Product: 具体产品类, 这是简单工厂模式的创建目标。
抽象产品类
//接口化或抽象化产品:提供了产品的接口
public interface IProduct {
void product();
}
实现具体产品类
//具体产品1:实现抽象产品中的接口方法或抽象方法
public class ConcreteProduct1 implements IProduct {
public void product() {
System.out.println("具体产品1...");
}
}
//具体产品2:实现抽象产品中的接口方法或抽象方法
public class ConcreteProduct2 implements IProduct {
public void product() {
System.out.println("具体产品2...");
}
}
定义工厂类
public class ProductFactory {
public static IProduct createProduct(String type) {
IProduct product = null;
switch (type) {
case "pruduct1":
product = new ConcreteProduct1();
break;
case "pruduct2":
product = new ConcreteProduct2();
break;
}
return product;
}
}
客户端调用工厂类
public class ProductTest {
public static void main(String[] args) {
ProductFactory.createProduct("product1").product();
}
}
使用简单工厂模式的场景和优缺点
使用场景:工厂类负责创建的对象比较少。
客户只需知道传入工厂类的参数, 而无须关心创建对象的逻辑。
- 优点: 使用户根据参数获得对应的类实例, 避免了直接实例化类, 降低了耦合性。
- 缺点: 可实例化的类型在编译期间已经被确定。 如果增加新类型, 则需要修改工厂增加新类型判断改变了原有逻辑, 这违背了开放封闭原则。 简单工厂需要知道所有要生成的类型, 其当子类过多或者子类层次过多时不适合使用。
工厂方法模式
工厂方法模式的定义:定义一个创建产品对象的工厂接口,将产品对象的实际创建工作推迟到具体子工厂类当中。这满足创建型模式中所要求的“创建与使用相分离”的特点。
工厂方法模式的主要角色如下
-
抽象工厂(AbstractFactory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法 newProduct() 来创建产品。
-
具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
-
抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
-
具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。
抽象产品类
//接口化或抽象化产品:提供了产品的接口
public interface IProduct {
public void product();
}
实现具体产品类
//具体产品1:实现抽象产品中的接口方法或抽象方法
public class ConcreteProduct1 implements IProduct {
public void product() {
System.out.println("具体产品1...");
}
}
//具体产品2:实现抽象产品中的接口方法或抽象方法
public class ConcreteProduct2 implements IProduct {
public void product() {
System.out.println("具体产品2...");
}
}
抽象工厂方法
//抽象工厂:提供了产品的生成方法
public interface AbstractFactory {
IProduct newProduct();
}
实现具体工厂类
//具体工厂1:实现了产品的生成方法
public class ConcreteFactory1 implements AbstractFactory {
public IProduct newProduct () {
System.out.println("具体工厂1生产-->具体产品1...");
return new ConcreteProduct1();
}
}
//具体工厂2:实现了产品的生成方法
public class ConcreteFactory2 implements AbstractFactory {
public IProduct newProduct () {
System.out.println("具体工厂2生产-->具体产品2...");
return new ConcreteProduct2();
}
}
客户端调用工厂类
public class ProductTest {
public static void main(String[] args) {
// 具体工厂1生产-->具体产品1
AbstractFactory factory1 = new ConcreteFactory1();
factory1.newProduct().product();
// 具体工厂2生产-->具体产品2
AbstractFactory factory2 = new ConcreteFactory2();
factory2.newProduct().product();
}
}
优点
- 更符合开放封闭原则:新增一种产品时,只需要增加相应的具体产品类和相应的工厂子类即可。简单工厂模式需要修改工厂类的判断逻辑。
- 符合单一职责原则:每个具体工厂类只负责创建对应的产品。简单工厂中的工厂类存在复杂的switch逻辑判断。
总结:工厂方法模式可以说是简单工厂模式的进一步抽象和拓展,在保留了简单工厂的封装优点的同时,让扩展变得简单,让继承变得可行,增加了多态性的体现。
缺点
- 添加新产品时,除了增加新产品类外,还要提供与之对应的具体工厂类,系统类的个数将成对增加,在一定程度上增加了系统的复杂度;同时,有更多的类需要编译和运行,会给系统带来一些额外的开销。
- 由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到DOM、反射等技术,增加了系统的实现难度。
- 虽然保证了工厂方法内的对修改关闭,但对于使用工厂方法的类,如果要更换另外一种产品,仍然需要修改实例化的具体工厂类。
- 一个具体工厂只能创建一种具体产品。
工厂方法模式通常适用于以下场景
- 客户只知道创建产品的工厂名,而不知道具体的产品名。如 TCL 电视工厂、海信电视工厂等。
- 创建对象的任务由多个具体子工厂中的某一个完成,而抽象工厂只提供创建产品的接口。
- 客户不关心创建产品的细节,只关心产品的品牌。
抽象工厂模式
前面介绍的工厂方法模式中考虑的是一类产品的生产,如畜牧场只养动物、电视机厂只生产电视机等。
工厂方法模式只考虑生产同等级的产品,但是在现实生活中许多工厂是综合型的工厂,能生产多种类的产品,如农场里既养动物又种植物,电器厂既生产电视机又生产洗衣机或空调等。
定义:抽象工厂模式提供一个创建一系列相关或相互依赖对象的接口,无须指定它们具体所要产品的类。
抽象工厂模式的主要角色如下
- 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法 newProduct(),可以创建多个不同等级的产品。
- 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系。
抽象产品类
//接口化或抽象化产品:提供了产品1的接口
public interface IProduct1 {
public void product1();
}
//接口化或抽象化产品:提供了产品2的接口
public interface IProduct2 {
public void product2();
}
实现具体产品类
//具体产品1:实现抽象产品中的接口方法或抽象方法
public class ConcreteProduct1 implements IProduct1 {
public void product1() {
System.out.println("同一族但不同等级的具体产品1...");
}
}
//具体产品2:实现抽象产品中的接口方法或抽象方法
public class ConcreteProduct2 implements IProduct2 {
public void product2() {
System.out.println("同一族但不同等级的具体产品2...");
}
}
抽象工厂方法
// 抽象工厂:提供了生产不同类别产品的生成方法
public interface AbstractFactory {
// 同一个工厂生产同一族但不同等级产品1
public IProduct1 newProduct1();
// 同一个工厂生产同一族但不同等级产品2
public IProduct2 newProduct2();
}
实现具体工厂类
public class ConcreteFactory1 implements AbstractFactory {
public IProduct1 newProduct1() {
System.out.println("具体工厂 1 生成-->同一族但不同等级的具体产品1...");
return new ConcreteProduct1();
}
public IProduct2 newProduct2() {
System.out.println("具体工厂 1 生成-->同一族但不同等级的具体产品2...");
return new ConcreteProduct2();
}
}
客户端调用工厂类
public class ProductTest {
public static void main(String[] args) {
// 具体工厂1
AbstractFactory factory1 = new ConcreteFactory1();
// 同一个工厂生产同一族但不同等级的具体产品1
factory1.newProduct1().product1();
// 同一个工厂生产同一族但不同等级的具体产品2
factory1.newProduct2().product2();
}
}
抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。其方法就是在抽象工厂中多扩展几个抽象方法来生成不同的产品能力。
特点
使用抽象工厂模式一般要满足以下条件。
- 系统中有多个产品族,每个具体工厂创建同一族但属于不同等级结构的产品(如:农场,养牛、种饲料同属于养植类但不同等级产品)。
- 系统一次只可能消费其中某一族产品,即同族的产品一起使用。
抽象工厂模式除了具有工厂方法模式的优点外,其他主要优点如下。
- 可以在类的内部对产品族中相关联的多等级产品共同管理,而不必专门引入多个新的类来进行管理。
- 当增加一个新的产品族时不需要修改原代码逻辑,只需要扩展一个方法来生成新产品并没有修改到其它产品的相关功能,满足开放封闭原则。
其缺点是:当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。