OOP之设计模式

前言

 设计模式是面向对象编程里的最佳实践和解决方案,有经验的面向对象开发人员会在合适的场景下采用设计模式来解决问题,这些设计模式和设计原则是众多开发人员经过长时间的试验和错误总结出来的,非常值得学习和借鉴。 本文将学习和讲解面向对象编程中的设计模式。 

面向对象

​ OOP(Object Oriented Programming)即面向对象编程,是软件编程发展到一定阶段的产物。现在面向对象的编程技术已经非常成熟并且广泛应用。像C++、Java 、C#、Python、PHP、JavaScript等都是面向对象的计算机编程语言。

三大特性

面向对象编程的三大特性是封装、继承、多态。

封装

Java的封装很大一部分依靠访问修饰符来完成,Java有四种访问修饰符。

访问修饰符 权限
private 只能在本类中访问
缺省 只能在本包中访问
protected 只能在本包或子类中访问
public 任何类都可以访问

另外 final 关键字可以用来限制继承的重写行为,即final 修饰的方法,子类不可以重写。

继承

类可以继承普通类或抽象类,接口可以继承接口,类可以实现多个接口,但只能继承一个类(单继承)。

继承是实现代码复用的一种方式。

多态

UML 统一建模语言

UML(Unified Modeling Language)统一建模语言是面向对象设计的建模工具,独立于任何具体的程序设计语言。

UML 有三种主要的模型视图:

  • 功能模型
  • 对象模型
  • 动态模型

功能模型

​ 从用户的角度展示系统功能。包括用例图。

**对象模型 **

​ 采用类、对象、属性、方法、关联等概念展示系统的结构。包括类图和对象图。

动态模型

​ 展示系统的内部动态行为。包括时序图、活动图、状态图。

​ 面向对象的建模和程序设计能力不是那么容易掌握的,它不像框架和类库那样几天或者几个星期就可以熟练使用,如何面向对象建模,如何设计程序需要大量的时间和精力去学习、思考和实践,才有可能掌握。这对于喜欢做有挑战性工作的开发人员来说是个值得兴奋的事情。

设计模式

编程语言发展

​ 本文所讲的设计模式只针对于面向对象的编程语言,反言之,不是面向对象的编程语言则不适用于本文的设计模式(比如C语言)。

​ 最早的计算机编程语言并不是面向对象的,而是机器语言,机器语言可以被机器直接识别并执行,后来出现汇编器和汇编语言,再后来才出现B语言和 C语言,C语言是面向过程的高级语言,至今仍被广泛应用于底层开发,如操作系统和数据库等。

​ 但随着计算机技术的广泛普及,互联网应用飞速发展,需要更加高级和抽象的语言支撑日益复杂的业务场景,使用C语言开发底层程序确实有着不错的性能优势,但是用来开发大多数软件项目太吃力,开发和维护成本很高,于是面向对象的C++语言诞生,C++不仅包含了几乎所有的C语言功能,还添加了面向对象的设计,引入了类和对象的概念,这就使得软件开发容易了很多。

​ 随后,面向对象的编程语言如雨后春笋般涌现,Java 、C#、Python、PHP、JavaScript等,同时管理内存的虚拟机技术也越来越成熟,很多语言在开发过程中根本不需要关心内存的分配和释放,由虚拟机根据算法自动管理内存,如Java语言的JVM。

​ 本文使用Java语言讲述面向对象的设计模式。

Design Patterns

​ 软件设计模式这一术语是Erich Gamma 等人从建筑设计领域引入到计算机科学中来的。

​ 1995年有四位博士合著了一本名叫《Design Patterns》的技术书籍(当年Java语言刚刚诞生),里面介绍了使用面向对象语言解决常见问题的23种设计模式,四位作者是国际上公认的面向对象软件领域的专家,自此树立起了软件设计模式的里程碑。这本书也被称为四人组设计模式、GoF设计模式(Gang of Four)。

image-20200729222936363

怎么看待设计模式

​ 正如上文所说,最早的计算机编程语言不是面向对象的,更别提面向对象的设计模式了,所以现在开发人员口中的设计模式是在面向对象语言广泛普及后,并经过一段时间的摸索和积累,才总结出来的编程技巧。换言之,如果你面向对象玩的很溜,可以用精通来形容的话,那么你心中可以没有这些设计模式,毕竟这些都是招式,而招式就是原本没有,后来摸索并总结出来的经验和技巧,就像武侠小说里面的高手一样,武学的最高境界是没有招式的,无招胜有招。

​ 可即便是高手,最开始也需要从一招一式学起,最初记住每一个招式,后面会熟练运用招式,再到最后,则会“忘了”这些招式,不去刻意的使用某一招,而是在恰当的时机信手拈来。这和学习设计模式是一样的,初学者应该认识常见的设计模式,先装入脑中,然后慢慢练习,以至于熟练使用它们,尽管这个过程中很有可能会生搬硬套,想在每一个可能的地方使用这些设计模式,但慢慢的就会控制自己,面对问题,不是急于使用这些设计模式,而是着手于把系统设计的更简单,更有弹性。

设计模式和框架、类库

​ 设计模式不会像框架和类库那样容易掌握,看过文档你就可以立即使用框架和类库做一些事情,有立竿见影的效果。但使用设计模式并不会这样,设计模式首先不会直接进入你的代码,而是先进入你的脑子,只有你的脑中装入很多关于模式的东西,才能够在应用中合理的使用它们。

​ 设计模式比框架和类库等级更高,设计模式告诉我们如何组织类和对象以解决某种问题。尽管框架和类库本身的代码也会遵循设计原则,使用设计模式。但更多时候,更多的开发者是使用它们,而不是开发它们。

模式分类

​ 既然软件设计模式是针对面向对象总结出来的,那么从面向对象的角度,可以将设计模式分为三大类:创造型模式,结构型模式和行为型模式。

image-20200729204522209

每种设计模式都会从以下几个方面分别讲解:

  • 主要解决

  • 代码示例

  • 实际应用

  • 优缺点分析

  • 设计原则

设计原则

​ 在学习设计原则之前有必要先讲一个哲学真理,那就是世界上唯一不变的事情是什么?答案是改变(Change)。世界上唯一不变的是改变。软件工程源于人们的需求,尽管最开始对于软件提供的功能很满意,但随着时间的推移和业务的变化,现有的功能不再适用,人门的需求会一直变化。

​ 在软件开发的整个生命周期中,软件开发完成后需要的时间要远大于软件开发完成前的时间。我们总是需要花费大量的时间在系统维护和变化上,比原先开发花的时间更多。所以提高系统的可维护性和可扩展性就显得尤为重要。

面向对象程序设计的几个重要的基本原则:

  1. 单一职责原则:一个类或者方法应该只有一个责任,即只有一个引起它变化的原因。这个原则的核心思想是将一个类或者方法拆分成多个职责单一的类或者方法,以降低它们之间的耦合度。
  2. 开放封闭原则:一个软件实体应该对扩展开发,对修改关闭。这个原则的核心思想是通过扩展已有的代码,而不是修改已有的代码,来满足新的需求。
  3. 里氏替换原则:所有引用基类的地方必须能透明地使用其子类的对象。这个原则的核心思想是子类应该能够替换父类,而不影响程序的正确性。
  4. 依赖倒置原则:高层模块不应该依赖于低层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。这个原则的核心思想是通过抽象来减少模块之间的耦合度。
  5. 接口分离原则:客户端不应该依赖于它不需要的接口。这个原则的核心思想是将大接口拆分成多个小接口,以降低客户端的复杂度和耦合度。
  6. 最少知识原则:一个对象应该对其他对象有尽可能少的了解,即最少知道原则。这个原则的核心思想是降低对象之间的耦合度,使得一个对象对其他对象的依赖降到最低。
  7. 组合复用原则:尽量使用组合,而不是继承。这个原则的核心思想是通过组合已有的对象来实现新的功能,而不是通过继承已有的类来实现新的功能。这样可以更加灵活地组织代码结构,降低模块之间的耦合度。

其中前五条原则是在罗伯特·马丁的著作《敏捷软件开发:原则、模式与实践》中首次提出的。

设计原则和设计模式

​ 这些设计原则不是设计模式,但众多设计模式也都在遵循这些设计原则,当你不知道当前业务场景使用什么设计模式合适时,遵循这些优秀的设计原则,同样可以帮助到你设计出良好的程序。

​ 有些时候,没有必要使用设计模式,使用良好的OO设计原则就可以很好的解决问题,这样其实就已经足够了。

​ 单纯在字面上不容易理解这些设计原则,这是很正常的,毕竟这些设计原则是在长期的OOP实践中总结出来的精华,精华哪有这么好吸收的。在具体的设计模式中去感受它们会有更好的效果,通常一个设计模式会遵循一个或多个设计原则,当你能清楚的找出设计模式中所遵循的设计原则时,你就已经入门了。

​ 可以把设计模式和设计原则理解成武功里的招式和心法,设计模式是招式,学会它可以让你在实际场景中运用起来,设计原则是心法,学会它可以让你灵活应对不同的场景。然而二者是相辅相成的关系,招式源于心法,只有招式没有心法,难免有些死板;而招式也是心法的一种有形体现,只有心法没有招式也只能站着背诵课本,不能发挥出实际效果。招式需要练,心法需要悟

单例模式(Singleton)

单例模式是创建型设计模式,它确保该类型有且只有一个对象被创建。根据对象创建的时机不同,单例可分为懒汉模式和饿汉模式。

饿汉模式

饿汉的意思是等不及,要立即创建对象,所以饿汉模式中单例对象在类加载时就创建好了。

//饿汉单例
public class SingletonDemo {
	private static final SingletonDemo INSTANCE = new SingletonDemo();
	private SingletonDemo(){
	}
	public static SingletonDemo getInstance() {
		return INSTANCE;
	}
}

​ 因为类加载过程是线程安全的,所以饿汉模式天然就是线程安全的写法;无论你是否使用这个对象的单例,单例对象都已经在应用启动后就准备好了,所以如果你不使用该单例对象或很晚才去使用单例对象,会造成资源不必要的浪费。

懒汉模式

相比饿汉立即创建单例对象的方式,懒汉则与之相反,懒汉是在获取单例对象时才去创建。

//懒汉单例
public class SingletonDemo {
	private volatile static SingletonDemo INSTANCE;
	private SingletonDemo(){
	}
    //双重检查+互斥锁
	public static SingletonDemo getInstance() {
		if (INSTANCE==null) {
			synchronized (SingletonDemo.class) {
                if (INSTANCE==null) {
                    INSTANCE = new SingletonDemo();
                }
			}
		}
		return INSTANCE;
	}
}

​ 与饿汉天然支持线程安全不同,懒汉的写法必须考虑到线程安全问题,这里使用了双重检查+互斥锁的方式保证线程安全。之所以这么写是为了规避一种情况:线程A和B同时判断单例对象为空,并准备创建对象,A持有类锁开始创建对象,B在临界区外等待,当A释放锁后,单例对象其实创建完毕,不需要B再去创建对象,所以B进入临界区后,再次判断单例对象是否创建,如果已经创建(volatile保证多线程同步的内存可见性),那么就不需要再次创建了。

​ 双重检查+锁的逻辑只会在单例对象为空的情况下运行,一旦单例对象创建成功,获取单例对象只需要一次判断的逻辑即可,因此不存在效率问题。

枚举

​ 此外还可以使用Java5提供的枚举类实现单例模式,枚举是一个语法糖,编译后其实就是一个常量类,而枚举所有的实例都是该类的对象,且是静态全局常量。

//枚举单例
public enum SingletonEnum {
	INSTANCE;
}

应用实例

每一个Java应用只有一个Runtime对象,因此JDK中 java.lang.Runtime 类使用单例模式创建对象。

public class Runtime {
    //饿汉创建单例
    private static Runtime currentRuntime = new Runtime();
    private Runtime() {}
    
    public static Runtime getRuntime() {
        return currentRuntime;
    }
}

破坏单例

​ Java编程中,创建对象不只有使用new关键字一种实现方式,还有可以使用反射或反序列化创建一个新的对象,所以在单例模式中同样应该要考虑到这种情况,为了防止单例模式被破坏,需要额外做一些工作。

反序列化

​ 通过序列化与反序列化得到的对象是一个新的对象,这就破坏了单例性。如果想要防止单例被反序列化破坏。就让单例类实现readResolve()方法。

public class SingletonLazy implements Serializable {
    //...
	private Object readResolve() {
        return SingletonLazy.INSTANCE;
    }
}

反射

即便是私有的构造器,通过反射技术也可以访问到并且实例化一个新的对象。解决方案:使用构造器判断。

/**
 * 单例模式:懒汉
 * 使用构造器判断 + 实现 readResolve() 方法的方式来防止单例被破坏。
 */
public class SingletonHungryPlus implements Serializable {

    private static final SingletonHungryPlus INSTANCE = new SingletonHungryPlus();
    private SingletonHungryPlus(){
        if(INSTANCE!=null){
            throw new RuntimeException("不允许反射调用构造器");
        }
    }
    public static SingletonHungryPlus getInstance() {
        return INSTANCE;
    }

    private Object readResolve() {
        return SingletonHungryPlus.INSTANCE;
    }
}

注意:

​ 使用构造器判断 + 实现 readResolve() 方法的方式在饿汉型单例中可以完美的防止单例被破坏;但在懒汉型单例中,极端情况下(先反射再getInstance() ,顺序反过来)是不起效的。

工厂模式(Factory)

​ 工厂模式属于创建型模式,用于创建对象,使用工厂模式,客户端获取的对象不再由自己创建,而是由工厂负责创建,以达到将对象的创建过程封装的目的。

简单工厂

简单工厂其实不算一种设计模式,而是一种编程习惯,将创建对象的代码从客户代码中剥离出去。

简单示例

//定义产品接口:手机
public interface Phone {
    String getName();
}
//定义产品实现类:华为手机
public class HWPhone implements Phone{
    @Override
    public String getName() {
        return "华为手机";
    }
}
//定义产品实现类:小米手机
public class XMPhone implements Phone{
    @Override
    public String getName() {
        return "小米手机";
    }
}
//工厂类
public class PhoneFactory {
    public static Phone getPhone(String type){
        if("XM".equals(type)){
            return new XMPhone();
        }else if("HW".equals(type)){
            return new HWPhone();
        }
        return null;
    }
}

应用示例

java.util.concurrent包中的 Executors 是创建任务执行器(线程池)的工具类,定义了创建线程池的静态工厂方法。

//静态工厂
public class Executors {
    
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, ...);
    }
    
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1, ...);
    }   
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
}

日志框架Slf4j 使用静态工厂获取日志操作类:

Logger logger = LoggerFactory.getLogger(Test.class);

注:使用静态工厂,可以不用创建工厂对象就可以使用,但也有弊端,就是无法使用对象的继承来扩展方法。在实践中,简单工厂的应用还是非常广泛的。

工厂方法模式(Factory Method)

工厂方法用来处理对象的创建,并将创建对象的代码封装在子类中,这样客户程序中关于超类的代码就和子类创建对象的代码解耦了。

简单栗子

​ 某披萨全国连锁商店生产披萨,各个省市的披萨商店所生产的披萨种类不一样,生产过程也有差异。这种场景我们可以使用工厂方法来生产披萨。

//创建者: 定义生产披萨的工厂方法
public abstract class PizzaStore {
	
	public Pizza orderPizza(String name) {
		Pizza pizza = createPizza(name);
		pizza.prepare();
		pizza.bake();
		pizza.cut();
		pizza.box();
		return pizza;
	}
	//将创建对象的代码抽象
	protected abstract Pizza createPizza(String name);
}

//产品: 披萨超类
public abstract class Pizza {
	private String name;
	public Pizza(String name) {
		this.name=name;
	}
	public abstract void prepare();
	public abstract void bake();
	public abstract void cut();
	
	public void box() {
		System.out.println("包装披萨...");
	};
	public String getName() {
		return name;
	}
}

创建具体产品的代码封装到子类中,这样客户端只需要使用产品的抽象就可以获取产品,而不需要知道具体的产品是什么。

//披萨工厂的具体实现:德州披萨商店
public class DeZhouPizzaStore extends PizzaStore{

	@Override
	protected Pizza createPizza(String name) {
		return new CheesePizza(name);
	}
}
//披萨的具体实现:奶酪披萨
public class CheesePizza extends Pizza {

	public CheesePizza(String name) {
		super(name);
	}
	@Override
	public void prepare() {
		System.out.println("准备芝士和奶油");
	}

	@Override
	public void bake() {
		System.out.println("烘焙15分钟");
	}

	@Override
	public void cut() {
		System.out.println("横着切");
	}
}
//测试
public class PizzaStoreDriver {
	public static void main(String[] args) {
		DeZhouPizzaStore deZhouPizzaStore = new DeZhouPizzaStore();
		ShangHaiPizzaStore shangHaiPizzaStore = new ShangHaiPizzaStore();
		
		deZhouPizzaStore.orderPizza("deZhou");
		shangHaiPizzaStore.orderPizza("shangHai");
	}
}

定义

工厂方法模式定义了一个创建对象的接口,由子类决定要创建的类是哪一个,工厂方法让类把实例化推迟到子类中完成。

工厂方法包含四个角色:

  • 抽象工厂
  • 具体工厂(子类)
  • 抽象产品
  • 具体产品

注:

​ 工厂方法和工厂并不一定总是抽象的,定义一个默认的工厂方法来产生具体的产品,这样即使工厂没有任何子类,依然可以创建产品。

简单工厂和工厂方法

​ 工厂方法是简单工厂的进一步抽象,在工厂方法模式中,核心的工厂类不再负责所有产品的创建,而是将具体的创建工作交给子类去做。这个核心工厂主要负责声明具体工厂所要实现的接口,而不负责创建对象,这使得工厂方法可以在不修改工厂角色的情况下引进新产品。符合“对拓展开发,对修改关闭” 的设计原则。

设计原则

依赖倒置原则

要依赖抽象,不要依赖具体类。

应用示例

Spring 框架中的 FactoryBean

public interface FactoryBean<T> {
	T getObject() throws Exception;
}

抽象工厂模式(Abstract Factory)

​ 工厂方法模式通过引入工厂等级结构(父子关系),解决了简单工厂中工厂类职责太重的问题。但工厂方法的每个工厂只能生产一类产品,如果产品种类很多,可能会导致系统中存在大量的工厂类,

工厂方法和抽象工厂

工厂方法和抽象工厂都是负责创建对象,工厂方法通过类的继承,抽象工厂通过对象的组合;

技术总结

  1. 所有的工厂都是用来封装对象的创建。都是通过减少应用程序与具体类之间的依赖而达到松耦合的目的。
  2. 简单工厂,虽然不算真正的设计模式,但仍不失为一个简单有效的方法,可以将客户程序从具体类中解耦。
  3. 工厂方法使用继承:把对象的创建委托给子类,子类实现工厂方法来创建对象。(将类的实例化放在子类中完成)
  4. 抽象工厂使用对象组合:对象的创建被实现在工厂接口所暴露出来的方法中。
  5. 工厂方法生产一类产品,而抽象工厂生产一系列相关联的产品,可以理解为抽象工厂是工厂方法的纵向优化(满足一定场景下,如生产一系列相关的产品,如果使用工厂方法则会产生大量的工厂类,因为工厂方法中每种工厂原则上只生产一类产品,而将这一系列产品的创建接口写在一个工厂中的话,这就是抽象工厂了)。
  6. 依赖倒置原则,指导我们避免依赖具体类型,而要尽量依赖抽象。工厂是很有威力的技巧,帮助我们针对抽象编程,而不是针对具体类编程。

构建器模式(Builder)

​ 构建器模式(Builder Pattern)是一种创建型设计模式,它允许你逐步创建复杂对象,同时保持灵活性。

​ 在许多情况下,创建一个对象需要多个步骤,而且每个步骤都可能有多种实现方式。如果直接在一个类中定义构造函数或者工厂方法来创建对象,可能会导致构造函数或工厂方法的参数过多,或者不够灵活。构建器模式通过将对象的构造过程拆分成多个步骤,并且在每个步骤中允许使用不同的实现方式,来解决这个问题。

简化Builder模式,以链式调用的方式来创建对象。

public class Student {

    private String code;
    private String name;
    private int age;
    private String sex;

    private Student() {}

    public static Student newBuilder(){
        return new Student();
    }
    public Student setCode(String code) {
        this.code = code;
        return this;
    }

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

    public Student setAge(int age) {
        this.age = age;
        return this;
    }

    public Student setSex(String sex) {
        this.sex = sex;
        return this;
    }
    //getter()...
}
public class TestBuilder {

    public static void main(String[] args) {
        Student student = Student.newBuilder().setCode("001").setName("Jackpot").setAge(18).setSex("男");
    }
}

使用场景

​ 复杂的对象通常会有多个成员变量,在外部调用时,不需要或者不方便一次性创建出所有的成员变量,这种情况下,使用多个构造方法去构建对象,很难维护,构建器模式可以解决这个问题,

LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(Duration.ofMinutes(5))
    .refreshAfterWrite(Duration.ofMinutes(1))
    .build(key -> createExpensiveGraph(key));

享元模式(Flyweight )

​ 享元模式(Flyweight Pattern)的核心思想很简单:如果一个对象实例一经创建就不可变,那么反复创建相同的实例就没有必要,直接返回一个共享的实例就行,这样既节省内存,又可以减少创建对象的过程,提高运行效率。

​ 享元模式尝试重用现有的缓存对象,如果未找到匹配的对象,则创建新对象。

主要解决:在有大量对象时,频繁创建有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。

示例

public class Staff {

    private int id;
    private String name;

    public Staff(int id, String name) {
        this.id = id;
        this.name = name;
    }

    private static final Map<String, Staff> cache = new HashMap<>();

    // 静态工厂方法:
    public static Staff create(int id, String name) {
        String key = id + "" + name;
        // 先查找缓存:
        Staff staff = cache.get(key);
        if (staff == null) { // 未找到,创建新对象
            System.out.println(String.format("create new Student(%s, %s)", id, name));
            staff = new Staff(id, name);
            cache.put(key, staff);// 放入缓存
        } else {// 缓存中存在
            System.out.println(String.format("return cached Student(%s, %s)", staff.id, staff.name));
        }
        return staff;
    }
}

应用实践

​ 享元模式在Java标准库中有很多应用。我们知道,包装类型如ByteInteger都是不变类,因此,反复创建同一个值相同的包装类型是没有必要的。以Integer为例,如果我们通过Integer.valueOf()这个静态工厂方法创建Integer实例,当传入的int范围在-128~`+127之间时,会直接返回缓存的Integer`实例:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Integer n1 = Integer.valueOf(100);
        Integer n2 = Integer.valueOf(100);
        System.out.println(n1 == n2); // true
    }
}

对于Byte来说,因为它一共只有256个状态,所以,通过Byte.valueOf()创建的Byte实例,全部都是缓存对象。

原型模式(Prototype )

​ 与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的。浅拷贝实现 Cloneable 接口然后重写 clone方法,深拷贝可以通过实现 Serializable 读取二进制流来实现。

​ 当直接创建对象的代价比较大时,可以考虑采用这种模式。

简单示例

public class Staff implements Cloneable {

    String name;
    int age;
    Dept dept;

    @Override
    public Staff clone() {
        try {
            return (Staff) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }

    static class Dept{
        String name;

        public Dept(String name) {
            this.name = name;
        }
    }
}

​ Java 中,Object 的 clone() 方法就属于原型模式。值得注意的是,Java 自带的 clone 方法是浅拷贝的。也就是说调用此对象的 clone 方法,只有基本类型的参数会被拷贝一份,非基本类型的对象不会被拷贝一份,而是继续使用传递引用的方式。如果需要实现深拷贝,必须要自己手动修改 clone 方法或者使用序列化生成对象。

应用场景

Spring 中的Bean 范围属性可以设置为原型模式。

适配器模式(Adapter)

主要解决

​ 适配器模式(Adapter Pattern)将某个接口转换成客户端希望的另一个接口,主要目的是为了支持兼容性,让原本因接口不匹配而不能一起工作的两个类可以协同工作。适配器模式属于结构型模式,有两种实现方式,分别是类适配器和对象适配器。

类适配器

插座提供220V电压,而手机只能使用5V电压进行充电,所以需要一个适配器将220V电压转化为5V电压。

//5V充电接口
public interface Charge5V {
    void charging();
}
//充电宝提供5V充电接口
public class MobilePower implements Charge5V{
    @Override
    public void charging() {
        System.out.println("使用充电宝给手机充电(5V)");
    }
}
//手机充电需要5V电压
public class  Phone {
    public void charge(Charge5V c) {
        c.charging();
    }
}

准备插座和适配器:

public class Socket {
    public void chargingWith220V() {
        System.out.println("使用插座直接充电(220V)");
    }
}
//适配器
public class ChargerAdapter extends Socket implements Charge5V {
    @Override
    public void charging() {
        chargingWith220V();
        System.out.println("将220V转换为5V");
    }
}
//使用适配器给手机充电
public class Client {
    public static void main(String[] args) {
        MobilePower mobilePower = new MobilePower();
        Phone phone = new Phone();
        phone.charge(mobilePower); //手机可以直接使用充电宝充电

        ChargerAdapter chargerAdapter = new ChargerAdapter();
        phone.charge(chargerAdapter); //在220V插座和充电线之间加一个适配器,才能给手机充电;
    }
}

​ 类适配器的实现使用了继承机制:适配器同时继承两个对象的接口。请注意, 这种方式仅能在支持多重继承的编程语言中才能实现,例如 C++。

使用Java实现类适配器的注意细节:

  • Java是单继承,所以类适配器需要继承被适配类型这一点算是一个缺点,因为这要求目标类型必须是接口,有一定的局限性。
  • 被适配类型的方法在Adapter中都会暴露出来,也增加了使用成本。
  • 由于其继承了被适配类型,所以它可以根据需求重写被适配类型的方法,使得Adapter类的灵活性增强了

对象适配器

​ 基本思路和类适配器相同,只是将Adapter类做修改,不是继承被适配的类型,而是持有被适配类型的实例,以解决兼容性的问题。根据组合复用原则,在系统中尽量使用组合来替代继承关系。

public interface ISocket {
    void chargingWith220V();
}
public class SocketImpl implements ISocket{
    @Override
    public void chargingWith220V() {
        System.out.println("使用插座直接充电(220V)");
    }
}
//适配器
public class ChargerAdapter implements Charge5V {

    private ISocket iSocket;

    public ChargerAdapter(ISocket iSocket) {
        this.iSocket = iSocket;
    }
    @Override
    public void charging() {
        iSocket.chargingWith220V();
        System.out.println("将220V转换为5V");
    }
}
//使用适配器给手机充电
public class Client {
    public static void main(String[] args) {
        ISocket socket = new SocketImpl();
        ChargerAdapter chargerAdapter = new ChargerAdapter(socket); //充电器将22V适配为5V
        Phone phone = new Phone();
        phone.charge(chargerAdapter);
    }
}

​ 类适配器实现方式使用继承来进行适配,它通过继承适配者类,并实现目标接口来完成适配的过程。适配器类充当了一个桥梁的角色,将适配者类和目标接口连接起来,使得适配者类能够适应目标接口的调用方式。

​ 对象适配器实现方式则是通过组合来进行适配。适配器类持有一个适配者类的实例,并实现目标接口来完成适配的过程。适配器类同样充当了一个桥梁的角色,将适配者类和目标接口连接起来。

​ 相比较而言,对象适配器方式更加灵活,可以在运行时动态替换适配者类的实例,而且它不需要使用多重继承,从而避免了一些继承带来的问题。但是,类适配器方式也有它的优点,比如它可以重定义适配者类的一些行为,从而具有一定的灵活性。

区别:

  • 类适配器使用继承来进行适配,而对象适配器使用组合来进行适配。
  • 类适配器只能适配一个适配者类,而对象适配器可以适配多个适配者类。
  • 类适配器可以重定义适配者类的行为,而对象适配器则不能。

应用实例

并发包中 RunnableAdapter 的实现是适配器模式, 将一个Runnable包装成一个 Callable;

   public static <T> Callable<T> callable(Runnable task, T result) {
       if (task == null) throw new NullPointerException();
       return new RunnableAdapter<T>(task, result);
   }   

static final class RunnableAdapter<T> implements Callable<T> {
       final Runnable task;
       final T result;
       RunnableAdapter(Runnable task, T result) {
           this.task = task;
           this.result = result;
       }
       public T call() {
           task.run();
           return result;
       }
   }

装饰者模式(Decorator)

装饰者设计模式(Decorator Pattern)是一种结构型设计模式,它允许向一个现有对象添加新的功能,同时不改变其结构。

在装饰者模式中,有一个基础的对象(Component),我们可以通过为其添加一个或多个装饰器(Decorator)来扩展其功能,每个装饰器包装一个基础对象,使其具有新的功能。装饰器和被装饰对象有着相同的接口,这样就可以无限地进行装饰。

该模式主要有以下角色:

  • Component(组件):定义一个接口,被装饰的对象和装饰器都实现该接口,该接口定义了被装饰对象和装饰器都需要实现的操作。
  • ConcreteComponent(具体组件):实现组件接口的类,它是被装饰的原始对象,可以被一个或多个装饰器装饰。
  • Decorator(装饰器):也是组件的子类,通常含有一个指向组件对象的引用,在具体装饰器中调用装饰对象的操作,并在调用前后添加自己的功能,以达到扩展组件功能的目的。
  • ConcreteDecorator(具体装饰器):实现装饰器接口的类,它具体实现装饰器定义的功能。

代码示例

下面是一个使用Java实现的简单装饰者模式的示例:

// 定义一个组件接口
interface Component {
    void operation();
}
// 具体组件类
class ConcreteComponent implements Component {
    @Override
    public void operation() {
        System.out.println("ConcreteComponent operation");
    }
}

// 装饰器类
abstract class Decorator implements Component {
    protected Component component;

    public Decorator(Component component) {
        this.component = component;
    }

    @Override
    public void operation() {
        component.operation();
    }
}
// 具体装饰器类
class ConcreteDecoratorA extends Decorator {
    public ConcreteDecoratorA(Component component) {
        super(component);
    }

    @Override
    public void operation() {
        super.operation();
        addedFunction();
    }

    private void addedFunction() {
        System.out.println("Added function from ConcreteDecoratorA");
    }
}
class ConcreteDecoratorB extends Decorator {
    public ConcreteDecoratorB(Component component) {
        super(component);
    }

    @Override
    public void operation() {
        super.operation();
        addedFunction();
    }

    private void addedFunction() {
        System.out.println("Added function from ConcreteDecoratorB");
    }
}
// 测试代码
public class DecoratorPatternDemo {
    public static void main(String[] args) {
        Component component = new ConcreteComponent();
        Component decoratorA = new ConcreteDecoratorA(component);
        decoratorA.operation();
        Component decoratorB = new ConcreteDecoratorB(component);
        decoratorB.operation();
    }
}

定义

​ 装饰者模式又名包装类模式(Wrapper),在不改变原有对象的基础之上,动态的将功能附加到对象上,以达到扩展原有对象功能的目的。装饰者提供了比继承更有弹性的替代方案。

适用场景

  1. 扩展一个类的功能或者给一个类添加附加职责
  2. 给一个对象动态的添加功能,或动态撤销功能。

优点

  1. 继承的有力补充,比继承灵活,不改变原有对象的情况下给一个对象扩展功能。(继承在扩展功能是静态的,必须在编译时就确定好,而使用装饰者可以在运行时决定,装饰者也建立在继承的基础之上的)
  2. 通过使用不同装饰类以及这些类的排列组合,可以实现不同的效果。
  3. 符合开闭原则

缺点

  1. 会出现更多的代码,更多的类,增加程序的复杂性。
  2. 动态装饰时,多层装饰时会更复杂。(使用继承来拓展功能会增加类的数量,使用装饰者模式不会像继承那样增加那么多类的数量但是会增加对象的数量,当对象的数量增加到一定的级别时,无疑会大大增加我们代码调试的难度)

实例

​ 正像我们最初认识IO包的类一样, java.io包中的类实在是太多了, 很难一时间搞清整体结构, 也不知道是怎么设计的. 这也正是装饰者模式所带来的问题, 装饰者会使类结构复杂化, 类数目增多。

​ 但是当我们明白了io包中运用了装饰者模式之后,再分析这些类,就会变得清晰多了:

  • InputStream 是顶级抽象类
  • FileInputStream,StringBufferInputStream,ByteArrayInputStream 都是继承自InputStream 的组件,这些类都提供了最基本的字节读取的功能。
  • FilterInputStream 是装饰基类
  • BufferedInputStream 是装饰基类的子类, 是一个具体的装饰者,它加入两种行为: 利用缓冲输入来改进性能;
    用一个readLine() 方法 (用来一次读取一行文本输入数据) 来增强接口。

接口使得安全地增强类的功能成为可能。如果使用抽象类来定义类型,那么程序员除了使用继承的手段来增强功能,再没有其他的选择了。这样的得到的类与包装类相比,功能更差,也更加脆弱。

​ 但是通过对接口提供一个抽象的骨架实现类,可以把接口和抽象类的优点结合起来。

代理模式(Proxy)

​ 代理模式的优点在于它可以降低系统的耦合度,增强系统的灵活性和扩展性。它可以使客户端和目标对象分离,客户端只需要知道代理对象而不需要知道目标对象,从而可以避免因为直接访问目标对象而引起的一些问题,比如访问权限、安全性等问题。同时,代理对象可以在目标对象之前或之后执行一些额外的操作,比如在方法调用前进行参数校验或者进行日志记录等,从而增强了目标对象的功能。

​ 代理模式的缺点在于它会增加系统的复杂性。由于代理对象需要实现目标对象的接口,并持有目标对象的引用,因此在系统中可能会产生很多代理对象,从而增加了系统的复杂性和维护成本。

​ 另外,代理模式还有一个比较常见的应用场景就是远程代理。远程代理是指代理对象可以在网络上的另一台机器上,客户端可以通过网络来访问代理对象,代理对象再将请求转发给目标对象,从而实现在不同机器上的对象访问。远程代理可以有效地解决分布式系统中的一些问题,比如网络延迟、传输速度等问题。

主要解决

代码示例

在Java中,代理模式有两种实现方式:静态代理和动态代理。

​ 静态代理是指代理对象在编译时就已经确定了,代理对象需要实现目标对象的接口,并在代理对象中调用目标对象的方法。静态代理的优点是实现简单,容易理解,缺点是代理对象只能代理一个接口,如果要代理多个接口,需要编写多个代理对象。

​ 动态代理是指代理对象在运行时动态生成,不需要在编译时就确定代理对象。动态代理可以代理多个接口,可以在代理对象中实现一些通用的功能,比如日志记录、性能统计等。

静态代理

编写目标接口和实现类:

public interface UserService {
    void save();
}
public class UserServiceImpl implements UserService {
    @Override
    public void save() {
        System.out.println("保存用户信息");
    }
}

编写代理类:

public class ProxyUserService implements UserService {

    private UserService userService;

    public ProxyUserService(UserService userService) {
        this.userService = userService;
    }

    @Override
    public void save() {
        System.out.println("ProxyUserService record logs");
        userService.save();
    }
}
public class Test {

    public static void main(String[] args) {
        UserService proxyUserService = new ProxyUserService(new UserServiceImpl());
        proxyUserService.save();
    }
}

JDK 动态代理

使用Java动态代理为目标类生成接口的代理类,首先被代理的类必须实现接口

public interface UserService {
    void save();
}
public class UserServiceImpl implements UserService {
    @Override
    public void save() {
        System.out.println("保存用户信息");
    }
}

第二步,编写调用处理器,每一个代理实例都会关联一个调用处理器,生成代理实例需要用到:

public class UserServiceProxy implements InvocationHandler {

    private Object target;

    public UserServiceProxy(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("开始事务");
        Object result = method.invoke(target, args);
        System.out.println("提交事务");
        return result;
    }
}

第三步,生成代理类:

public static void main(String[] args) {

	UserService userService = new UserServiceImpl();
        UserServiceProxy proxy = new UserServiceProxy(userService);
        UserService proxyInstance = (UserService) Proxy.newProxyInstance(
            userService.getClass().getClassLoader(), userService.getClass().getInterfaces(), proxy);
        proxyInstance.save();
}

Proxy.newProxyInstance 方法需要传入3个参数:类加载器、代理类的接口列表和代理类处理器

实际应用

Spring 代理

ProxyFactoryBean

AopProxy

优缺点分析

策略模式(Strategy)

​ 策略模式(Strategy Pattern)是一种常用的面向对象设计模式,其目的是让算法的变化独立于使用算法的客户端。该模式定义了一系列的算法,将每一个算法封装起来,并且使它们可以互换。

​ 在策略模式中,一组算法被定义在一个接口中,然后每个具体算法都实现这个接口。客户端可以选择不同的算法实现,并在运行时动态地切换这些算法。这种做法使得客户端和算法之间解耦,可以独立地变化,同时也使得代码更加灵活和易于维护。

策略模式一般包含以下几个角色:

  1. 策略接口(Strategy Interface):定义一组算法的接口,各个具体策略实现该接口。
  2. 具体策略类(Concrete Strategy Classes):实现策略接口,包含具体的算法实现。
  3. 环境类(Context Class):包含一个策略接口的引用,它可以接受客户端的请求,并将请求委托给策略对象处理。

实际应用

​ 我们知道企业员工的薪资计算方式大体一致,但每个组织又有自己不同的地方,如工资项的种类不同,对于相同的工资项算法有所差异等等。

//抽象出薪资计算器接口
public interface SalaryCalculator {
	//计算工资
	void calculate();
}
//实现一个默认的薪资计算器
public class DefaultSalaryCalculator implements SalaryCalculator{

	@Override
	public void calculate() {
		doCalculate();
	}
}
//实现一个定制化的薪资计算器: 中关村薪资计算器
public class ZGCSalaryCalculator implements SalaryCalculator{

	@Override
	public void calculate() {
		doCalculate1();
	}
}
//薪资计算执行器
public class SalaryExecutor {

	private SalaryCalculator salaryCalculator;
    
	public SalaryExecutor(SalaryCalculator salaryCalculator) {
		this.salaryCalculator = salaryCalculator;
	}
	public void setSalaryCalculator(SalaryCalculator salaryCalculator) {
		this.salaryCalculator = salaryCalculator;
	}
	public void execute() {
		salaryCalculator.calculate();
	}
}
//使用示例
public class SalaryCalculateDemo{
    public static void main(String[] args){

		SalaryExecutor salaryExecutor = new SalaryExecutor(new DefaultSalaryCalculator());
		salaryExecutor.execute();
		//动态替换算法
		salaryExecutor.setSalaryCalculator(new ZGCSalaryCalculator());
		salaryExecutor.execute();
    }
}

​ 上面的示例中,首先抽象出一个薪资计算器接口,把变化的部分抽象出来,定义抽象方法,也就是 calculate() 计算薪资的逻辑,这部分计算逻辑在不同的组织下会有所不同。然后定义N个类实现接口,复写抽象方法,也就是封装一个个计算逻辑(策略),最后再定义一个类,使用组合的方式实现薪资计算执行器 SalaryExecutor,SalaryExecutor 的构造方法需要一个薪资计算的具体实现,在使用时实例化想要的计算逻辑对象即可,而且可以随时更换策略(算法)。

应用场景:

​ 在许多算法相似的情况下,将这些算法封装成一个一个的类,实现相同的接口,已达到相互替换的目的。这些算法就是一个个策略,因为实现了相同的接口,所以可以互相替换,这就是策略模式。

设计原则

开放封闭原则:

​ 把变化的部分提取并封装起来,与其它不变的部分区分开来。

组合复用原则:

​ 多用组合,少用继承。

状态模式(State)

​ 状态模式是一种行为型设计模式,它允许对象在其内部状态改变时改变它的行为。在状态模式中,对象会将自己的行为委托给当前的状态对象,从而实现不同状态下的不同行为。

状态模式中的主要角色包括:

  1. Context上下文:上下文是一个包含状态对象的类,它允许状态对象访问上下文中的数据,同时它也包含了一个指向当前状态对象的引用。
  2. State状态:状态是一个接口或抽象类,它定义了一个对象的所有状态所共有的方法,并且可以通过这些方法来处理上下文中的请求。
  3. ConcreteState(具体状态):具体状态是实现了 State 接口的具体类,它表示对象的某个状态,包含了该状态下的所有行为实现。

主要解决

状态模式的主要目的是将复杂的条件分支语句转化为基于状态的行为。

简单示例

下面是一个简单的Java实现状态模式的示例:

//首先定义一个状态接口:
public interface State {
    void handle();
}
public class StartState implements State {
    public void handle() {
        System.out.println("Starting the engine.");
    }
}
//然后定义不同的状态实现类:
public class StopState implements State {
    public void handle() {
        System.out.println("Stopping the engine.");
    }
}
//最后定义一个上下文类,用于管理状态的转换:
public class Context {
    private State state;

    public Context() {
        state = null;
    }
    public void setState(State state) {
        this.state = state;
    }

    public State getState() {
        return state;
    }

    public void handle() {
        state.handle();
    }
}

在上面的示例中,我们定义了两个具体的状态类:StartStateStopState,它们都实现了状态接口State。然后我们定义了一个上下文类Context,用于管理状态的转换,其中包括了当前状态的状态变量state和一个handle()方法,用于执行当前状态的行为。

public class StatePatternDemo {
    public static void main(String[] args) {
        Context context = new Context();
        context.setState(new StartState());
        context.handle();
        context.setState(new StopState());
        context.handle();
    }
}

实际应用

状态模式在实际应用中也有很多场景,比如:

  1. 状态机:状态机可以用状态模式来实现,每个状态可以表示状态机的一个状态。
  2. 游戏中的角色状态:游戏中的角色有不同的状态,比如攻击状态、防御状态、待机状态等等,可以用状态模式来实现不同状态下的不同行为。
  3. 线程的状态:线程有不同的状态,比如就绪状态、运行状态、阻塞状态等等,可以用状态模式来实现不同状态下的不同行为。

优缺点分析

状态模式的优点包括:

  1. 将与特定状态相关的行为局部化并且独立化。
  2. 简化了条件语句。
  3. 更好地分离了状态和行为,使得代码更加清晰。
  4. 可以使得状态转换变得更加明确。

总之,状态模式可以使得对象在不同状态下表现不同的行为,从而简化代码并且使得代码更加清晰易懂,因此在许多实际应用中都有广泛的应用。

设计原则

状态模式和策略模式的UML几乎一样。

模板方法模式(Template Method)

​ 模板方法(Template Method)定义一个操作中的算法骨架,而将一些步骤延迟到子类中去实现,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

​ 模板方法是一个比较简单的模式。它的主要思想是,定义一个操作的一系列步骤,对于某些暂时确定不下来的步骤,就留给子类去实现好了,这样不同的子类就可以定义出不同的步骤。

简单栗子

​ 如下,SuperClassTemplate 是一个抽象的父类模板,定义了一个算法 calculate(),且子类不可以修改算法的执行步骤(声明为final),算法分为三步,第一步和第三步需要子类实现,第二步则只能使用父类的计算逻辑。

//抽象的父类模板
public abstract class SuperClassTemplate {

	final void calculate() {
		stepOne();
		stepTwo();
		stepThree();
	}
	protected abstract void stepOne();
	final void stepTwo() {
		doSomeing();
	}
	protected abstract void stepThree();
}
//子类实现算法中某的几个步骤
public class SubClassTemplate extends SuperClassTemplate{

	@Override
	protected void stepOne() {
		doSomeing0();
	}

	@Override
	protected void stepThree() {
		doSomeing1();
	}
}

模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。

实际应用中,父类也不一定非得是抽象类,普通类也可以,使用抛异常的方式强制子类提供实现。

//父类模板
public class SuperClassTemplate {

	final void calculate() {
		stepOne();
		stepTwo();
		stepThree();
	}
    //想正常使用calculate()算法,则必须让子类覆盖重写
	protected void stepOne(){
        throw new UnsupportedOperationException();
    }
	final void stepTwo() {
		doSomeing();
	}
	protected void stepThree(){
        throw new UnsupportedOperationException();
    }
}

​ 模板方法模式是在一个方法中定义一个算法骨架,而将一些步骤延迟到子类中。模板方法使得子类在不改变算法结构的情况下,重新定义算法的某些步骤。

模板方法模式,简单且实用。

接口使得安全地增强类的功能成为可能。如果使用抽象类来定义类型,那么程序员除了使用继承的手段来增强功能,再没有其他的选择了。这样的得到的类与包装类相比,功能更差,也更加脆弱。

​ 但是通过对接口提供一个抽象的骨架实现类,可以把接口和抽象类的优点结合起来。

封装算法;如工资计算器,员工工资计算的大体逻辑相似,但每个企业都或多或少的有不同的地方。

Collections Framework 为每个重要的集合接口都提供了一个骨架实现,包括

AbstractCollection、AbstractSet、AbstractList 和 AbstractMap 。

模板方法和钩子

线程池中使用钩子函数

public class ThreadPoolExecutor extends AbstractExecutorService {

    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        while (task != null || (task = getTask()) != null) {
            beforeExecute(wt, task);
            task.run();
            afterExecute(task, thrown);
        }
    }
    
    protected void beforeExecute(Thread t, Runnable r) { }
    
    protected void afterExecute(Runnable r, Throwable t) { }
}

​ 当子类必须提供算法中的某个方法或步骤的实现时,就是用抽象方法;如果算法的这部分是可选的,就用钩子。

​ 钩子有很多用途,常用的有两种,钩子可以充当算法中可选的部分,子类可以选择实现,也可以置之不理。第二种就是程序运行时对算法的步骤做出反应和选择。

实际应用

应用一:AQS(JDK)

​ Java并发包中的队列同步器AQS采用的就是模板方法模式,想要实现一个自定义的同步组件、锁,需要实现Lock接口,而Lock接口方法的具体实现是由队列同步器来提供的,于是作者就设计了一个抽象的队列同步器,供实现者使用,通过继承这个抽象的队列同步器来实现自己的同步器。

​ 父类定义算法步骤,acquire 独占式获取锁,方法声明为 final ,表示不允许子类修改。该算法可以看做三个步骤,其中tryAcquire 方法指定必须由子类实现,这里可以看到指定父类方法必须由子类实现不单单是声明为抽象方法这一种方法,还可以声明为普通的protected方法,并在方法中直接抛出异常。这样要想算法能够正常运行,子类则必须实现。

public abstract class AbstractQueuedSynchronizer{
    
    public final void acquire(int arg) {
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
            selfInterrupt();
        }
    }
    
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    
    final boolean acquireQueued(final Node node, int arg) {
        //doSomething();
    }
}

NonfairSync 继承父类AQS,并复写tryAcquire方法。

public class ReentrantLock implements Lock{
	private final Sync sync;
    
	static final class NonfairSync extends AbstractQueuedSynchronizer {
        
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }  
    }
}

应用二 业务模型的发布动作

​ OA或大多数办公软件,都有支持工作流审批的业务单据(请假单、报销单、采购订货单等),在工作流审批通过的时机定义一个发布逻辑。

public class BusinessModel{

	//发布: release()定义了发布算法,子类不允许修改,只能外部调用
	public final boolean release() {
		if (!canRelease()) {
			return false;
		}    
        doRelease();
        updateWorkFlowState();
        afterRelease();
    }
    //钩子函数,用于控制是否可以发布
    protected boolean canRelease() {
        return true;
    }
    //允许子类重新定义发布逻辑,或添加发布逻辑
    protected boolean doRelease() {
        return doSomething();
    }
    //修改工作流状态,这步逻辑定义为私有,不允许修改
    private void updateWorkFlowState(){
        doWork();
    }
    //钩子函数,父类为空实现,子类可以选择实现或不实现,用于添加发布成功的后续业务逻辑
    protected void afterRelease() {
    }
}

迭代器模式(Iterator)

​ 迭代器设计模式(Iterator Pattern)是一种行为型设计模式,它提供一种顺序访问一个聚合对象中各个元素的方法,而又不需要暴露该对象的内部表示。

迭代器模式通常包含以下角色:

  • 迭代器:定义遍历元素所需的方法,一般来说会有 hasNext() 方法用于判断是否还有下一个元素,以及 next() 方法用于获取下一个元素。
  • 具体迭代器:实现迭代器接口中定义的方法,完成集合元素的遍历。
  • 聚合对象:定义创建相应迭代器对象的接口,一般来说会有一个类似于 createIterator() 的方法。
  • 具体聚合对象:实现聚合对象接口,返回一个具体迭代器的实例。

实际应用

Java 集合库中就提供了迭代器,方便开发者访问各种类型的集合,下面以ArrayList 的迭代器为例讲解:

有意思的是,在我刚刚接触Java的时候,没觉得迭代器是一种设计模式,而是把它当做一种理所应当的设计。

//可迭代的接口
public interface Iterable<T> {
    Iterator<T> iterator();   
}
public class ArrayList<E> implements Iterable{//这里做了简化,实际ArrayList的继承关系比这个复杂
    
    public Iterator<E> iterator() {
        return new Itr();
    }
}
//迭代器
public interface Iterator<E> {
    boolean hasNext();
    E next();
    void remove();
}
private class Itr implements Iterator<E> {
    //省略实现逻辑,具体细节请查看源码
}
//客户端调用
List<String> strings = Arrays.asList("apple", "banana", "cherry");
    Iterator<String> iterator = strings.iterator();
    while (iterator.hasNext()){
    System.out.println(iterator.next());
}

注:

​ Java中的集合类型有很多,从面向对象的角度来讲,它们都属于聚合对象,每个聚合对象的迭代器实现都不太相同,但思想却是一样的,都是为了提供顺序访问内部所有元素的方法,而又不暴露其内部实现。

​ ArrayList 内部的迭代器属于fail-fast 迭代器,即在迭代期间不允许修改集合内部元素的内容,否则迭代的内容和实时数据就会不一致,Java中很多集合都采用了这种方式,以保证数据安全。

观察者模式(Observer)

​ 观察者模式是非常常见的一种设计模式,它的应用场景很广泛。观察者模式中定义了两个角色:主题和订阅者,就像报刊和订阅人一样。多个订阅者可以订阅一个主题,主题一旦发生变化,就会通知所有的订阅者知晓,订阅者根据得到的信息作出自己的反应。

​ 观察者模式是一种行为型设计模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象,当主题对象发生改变时,它的所有观察者都会收到通知并进行相应的更新。

观察者模式的核心是主题对象(Subject)和观察者对象(Observer)之间的关系。主题对象维护一个观察者列表,当主题对象状态改变时,它会通知观察者,让它们更新自己。观察者对象则负责更新自己的状态,以使得它们与主题对象保持一致。

观察者模式的结构包括:

  • Subject(主题):定义了一个抽象的主题接口,它可以添加、删除、通知观察者。
  • ConcreteSubject(具体主题):实现了主题接口,并维护了观察者列表。
  • Observer(观察者):定义了一个抽象的观察者接口,它可以接收主题对象的通知并更新自己的状态。
  • ConcreteObserver(具体观察者):实现了观察者接口,具体观察者会在接收到主题对象的通知后,更新自己的状态。

主要解决

简单示例

主题是气象信息,订阅者是当前气象显示板。气象站一旦发布最新的气象信息(温度、湿度、气压),就会在气象显示板上立即更新。

//主题接口
public interface Subject {

	void registerObserver(Observer o);
	
	void removeObserver(Observer o);
	
	void notifyObservers();
}
//主题:气象信息
public class WeatherData implements Subject {

	private List<Observer> observers;
	private float temp;//温度
	private float humidity;//湿度
	private float perssure;//气压

	public WeatherData() {
		observers = new ArrayList<Observer>();
	}

	@Override
	public void registerObserver(Observer o) {
		observers.add(o);
	}

	@Override
	public void removeObserver(Observer o) {
		observers.remove(o);
	}

	@Override
	public void notifyObservers() {
		for (Observer observer : observers) {
			observer.update(temp, humidity, perssure);
		}
	}

	public void setData(float temp, float humidity, float perssure) {
		this.temp = temp;
		this.humidity = humidity;
		this.perssure = perssure;
		notifyObservers();
	}
}

观察者

//观察者接口
public interface Observer {
	void update(float temp,float humidity,float perssure);
}
//观察者:气象信息显示板
public class CurrentDisplay implements Observer{
	
	private float temp;
	private float humidity;
	private float perssure;
	private Subject subject;
	
	public CurrentDisplay(Subject subject) {
		this.subject = subject;
		subject.registerObserver(this);//自动订阅主题
	}

	@Override
	public void update(float temp, float humidity, float perssure) {
		this.temp = temp;
		this.humidity = humidity;
		this.perssure = perssure;
		display();
	}

	public void display() {
		System.out.println("CurrentDisplay [temp=" + temp + ", humidity=" + humidity + ", perssure=" +			 		perssure + "]");
	}
}
//测试
public class ObserverDemo {
	public static void main(String[] args) {
		WeatherData weatherData = new WeatherData();
		CurrentDisplay currentDisplay = new CurrentDisplay(weatherData);	
		weatherData.setData(12, 23, 33);
	}
}

​ 在上面的代码中,WeatherData 实现了 Subject 接口,维护了一个观察者列表,以及一个数据状态(温度、湿度和气压)。当数据状态发生变化时,WeatherData 会调用 notifyObservers() 方法,通知所有观察者。CurrentConditionsDisplay 实现了 Observer 接口,负责更新自己的状态并显示当前天气状况。在它的构造函数中,它注册了自己作为观察者,并在 update() 方法中接收主题对象的通知并更新自己的状态。

实际应用

观察者设计模式是一种常用的设计模式,在许多实际应用中都有广泛的应用。以下是一些观察者模式的实际应用:

  1. GUI 程序:在 GUI 程序中,通常有很多组件需要关注某个事件(比如按钮被点击、窗口被关闭等等),这些组件就可以通过观察者模式来注册自己并接收事件通知。

  2. 消息队列系统:在消息队列系统中,消息生产者和消息消费者之间通常采用观察者模式来实现解耦。消息生产者将消息发送到队列中,然后通知所有的消息消费者来消费消息。

  3. 订阅服务:在订阅服务中,用户可以订阅某个主题并接收相关的更新通知。这个过程就可以采用观察者模式来实现。

  4. 触发器系统:在触发器系统中,用户可以定义一些触发器,当某个条件满足时触发某个动作。这个过程也可以采用观察者模式来实现。

  5. MVC 模式:在 MVC 模式中,视图是观察者,模型是被观察者,当模型的状态发生改变时,视图会自动更新。

总之,观察者模式在很多实际应用中都有广泛的应用,它可以让代码更加松耦合、可扩展和易维护。

优缺点分析

观察者模式的优点包括:

  • 可以建立对象之间的松耦合关系,主题对象和观察者对象可以独立地改变和复用,不会相互影响。
  • 可以支持广播通信,当一个主题对象发生改变时,它的所有观察者都会收到通知并进行相应的更新。
  • 可以降低系统的耦合度,使得代码更加灵活。

观察者模式的缺点包括:

  • 观察者过多时可能会导致性能问题。
  • 主题对象和观察者对象之间的依赖关系可能会导致代码难以维护。

总体来说,观察者模式适用于主题对象和观察者对象之间的一对多关系,并且需要在不同的时间点通知观察者对象的场景。

设计原则

设计原则1:

​ 找出程序中变化的部分,然后和固定不变的部分相分离。

​ 在观察者模式中,主题的状态会发生变化,观察者的类型和数量也会发生变化,用这个模式,可以在不改变主题的情况下,改变依赖于主题的观察者对象。

设计原则2:

​ 针对接口编程,不针对实现编程。

​ 主题和观察者都使用接口,观察者利用主题接口向主题注册自己,主题利用观察者接口通知观察者,两者之间运作正常,又同时具有松耦合的优点。

设计原则3:

​ 多用组合,少用继承。

​ 观察者模式利用组合将许多观察者注册进主题中,对象之间的这种关系不是通过继承得到的,而是在运行时通过组合的方式产生的。

组合模式(Composite)

组合模式(Composite)将对象组合成树形以表示”部分-整体”的层次结构,使用户以一致的方式处理单个对象和组合对象。

示例

企业的雇佣职员有上下级的层级关系,可以使用组合模式表示。

//雇员
public class Employee {

	private String name;
	private int age;
	List<Employee> subordinates;//下属职员

	public Employee(String name, int age) {
		this.name = name;
		this.age = age;
		subordinates = new ArrayList<Employee>();
	}
	
	public void addSubordinate(Employee e) {
		subordinates.add(e);
	}
	
	public void removeSubordinate(Employee e) {
		subordinates.remove(e);
	}
	
	public List<Employee> getSubordinates(){
		return subordinates;
	}

	@Override
	public String toString() {
		return "Employee [name=" + name + ", age=" + age + "]";
	}
}
//测试
public class CompositeDemo {
	public static void main(String[] args) {
		
		Employee CEO = new Employee("jack", 50);
		Employee manager1 = new Employee("manager1", 35);
		Employee manager2 = new Employee("manager2", 39);
		CEO.addSubordinate(manager1);
		CEO.addSubordinate(manager2);
		
		Employee staff1 = new Employee("staff1", 30);
		Employee staff2 = new Employee("staff2", 23);
		Employee staff3 = new Employee("staff3", 32);
		manager1.addSubordinate(staff1);
		manager1.addSubordinate(staff2);
		manager2.addSubordinate(staff3);
		getAllEmployeeByThis(CEO);
	}
	
	public static void getAllEmployeeByThis(Employee employee) {
		System.out.println(employee.toString());
		List<Employee> subordinates = employee.getSubordinates();
		for (Employee e : subordinates) {
			getAllEmployeeByThis(e);
		}
	}
}

其他应用场景:

  • 文件夹
  • 功能树
  • XML文件
  • 界面组件

命令模式(Command)

责任链模式(Chain )

​ 责任链模式(Chain of Responsibility)是一种行为型设计模式,它允许多个处理器按照顺序处理同一个请求,直到其中一个对象能够处理该请求为止。每个处理请求的对象都可以选择将请求传递给下一个对象,也可以选择自己处理该请求。

​ 在责任链模式中,通常会构建一个处理请求的链条,链条上的每个对象都可以处理该请求,如果一个对象不能处理该请求,则将请求传递给链条上的下一个对象,直到请求被处理为止。这种方式可以很好地将请求的处理逻辑进行解耦,并且可以动态地增加或删除链条上的处理对象。

     ┌─────────┐
     │ Request │
     └─────────┘
          │
┌ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ┐
          ▼
│  ┌─────────────┐  │
   │ ProcessorA  │
│  └─────────────┘  │
          │
│         ▼         │
   ┌─────────────┐
│  │ ProcessorB  │  │
   └─────────────┘
│         │         │
          ▼
│  ┌─────────────┐  │
   │ ProcessorC  │
│  └─────────────┘  │
          │
└ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ┘
          │
          ▼

代码示例

下面是一个使用责任链模式的示例,假设我们需要处理一些请求,这些请求可以被不同的处理器处理。首先,我们定义一个抽象的处理器类,所有的具体处理器都需要继承该类

在实际应用中,我们根据实际需求来决定每个处理器处理成功后是否还要继续传递请求给下一个处理器。

实际应用

责任链模式的实际应用场景比较广泛,以下是一些常见的应用场景:

  1. Web开发中的过滤器:在Web应用中,常常需要对请求进行过滤,比如身份认证、权限验证、参数校验等。这些过滤器可以组成一个责任链,将请求按照顺序传递下去,直到找到能够处理该请求的过滤器为止。
  2. 缓存处理:在系统中,为了提高性能,通常会使用缓存技术。责任链模式可以将缓存处理器组成一个链条,将请求依次传递下去,直到找到能够处理该请求的缓存处理器为止。

​ JavaEE的Servlet 规范定义的Filter就是一种责任链模式,它不但允许每个Filter都有机会处理请求,还允许每个Filter决定是否将请求“放行”给下一个Filter

public class AuditFilter implements Filter {
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException {
        if (check(req)) {      
            chain.doFilter(req, resp);// 放行
        } else {
            sendError(resp);  // 拒绝
        }
    }
}

优缺点分析

​ 责任链模式的优点在于它可以将请求的发送者和接收者解耦,并且可以动态地增加或删除处理器,使得系统更加灵活。同时,责任链模式还可以避免请求发送者和接收者之间的耦合,使得系统更加具有可扩展性。

​ 需要注意的是,责任链模式适用于处理同类型的请求或事件,而不适用于处理不同类型的请求或事件。此外,在使用责任链模式时,需要注意链条长度和处理器顺序的设计,以免影响系统性能和稳定性。

解释器模式(Interpreter)

​ 解释器模式是一种行为型设计模式,它定义了一种语言语法解析的方式,用于解决一些特定问题。解释器模式中,每个符号都有一个特定的含义,而解释器则根据这些符号来执行特定的操作。

​ 在解释器模式中,通常会定义一个抽象语法树 AST,用于表示语法规则和结构。解释器会遍历这个抽象语法树,并根据每个节点的类型来执行相应的操作。这些节点可以是终结符,也可以是非终结符。

​ 解释器模式通常用于解析一些特定的领域语言,例如数学公式、正则表达式等。它可以帮助开发人员更方便地处理这些语言,从而提高代码的可读性和可维护性。

代码示例

​ 这里就不额外写解释器模式的示例代码了,如果非要写一个数学公式的解释器,比如计算 3+(5+7)*8-2 的值是多少,其实也可以写出来,但确实没必要。在软件工程中,在很多特定领域已经有大量成熟的解释器存在,我们开发人员可能更多的是认识并学会使用这些解释器工具,并不需要重复造轮子。如果要求工作没几年的开发人员写一个解决特定领域问题的解释器,也有些不切实际,所以最开始领略解释器这种设计思想,用好现成的轮子也很重要。

实际应用

正则表达式解析器(主流编程语言都会内置正则表达式解析器,如JS、Java、Go等)

XML解释器、JSON解释器、SQL解释器等

Spring框架的 SpEL 表达式

定时任务调度框架Quartz 通过解析Cron表达式来控制任务执行。

Hibernate 框架:

​ Hibernate 使用 ASTQueryTranslatorFactory 将 HQL 语句转换成 SQL 语句。它通过 HQLParser 解析 HQL 语句,生成一颗抽象语法树(AST)。具体逻辑可查看源码。

以下是几种语法解析库:

  1. antlr:ANTLR是一个强大的语言识别工具,它能够根据用户定义的语法规则生成语法解析器和词法分析器。ANTLR支持识别LL(*)语法,支持将解析结果以树状结构输出,非常适合构建基于语法的编辑器和IDE。(支持Java、C#、C++、Go、Python、JS、PHP等)

  2. JavaCC:JavaCC是一个生成Java词法分析器和语法解析器的工具,它支持LL(k)语法。JavaCC的语法规则采用BNF(巴克斯-诺尔范式)描述,开发者可以通过JavaCC生成相应的Java代码。

  3. Spoon:Spoon是一种开源的Java语言处理器,它允许开发者分析、转换和生成Java代码。Spoon采用Java语言本身的语法规则,生成抽象语法树,并提供了一系列API可以方便地操作和修改抽象语法树。

  4. Eclipse JDT:Eclipse JDT(Java Development Tools)是一个用于Java开发的工具集,包含了一个Java编译器和一个抽象语法树库。开发者可以使用Eclipse JDT获取Java源代码的抽象语法树,并在此基础上进行代码分析和转换。

  5. ASM:ASM是一个基于Java字节码的代码生成和转换框架,它提供了直接操作字节码的API,并支持生成、转换和分析Java字节码。开发者可以使用ASM生成Java类的字节码,也可以通过ASM解析和修改现有Java类的字节码。

​ 关于抽象语法树,它是一个基于语法规则和代码结构的树状结构,用于描述程序的语法结构。抽象语法树可以表示程序的所有语法结构,包括语句、表达式、函数调用等,开发者可以通过分析和修改抽象语法树实现代码分析和转换。在Java中,可以通过上述工具库获取Java源代码的抽象语法树。

优缺点分析

​ 解释器模式的主要优点是可以灵活地扩展语法,同时可以有效地控制语法规则的复杂度。然而,由于解释器模式需要构建抽象语法树,因此它的运行效率可能会比较低。

外观模式(Facade)

​ 外观模式(Facade Pattern)是一种结构型设计模式,它提供了一个简单的接口,用于访问复杂系统中的一组子系统。它将这些子系统封装在一个单独的类中,并暴露一个高层次的接口,使得客户端代码可以更容易地使用这些子系统。

​ 在外观模式中,客户端与外观类交互,而外观类与子系统交互。外观类将客户端的请求委派给相应的子系统,并将子系统的结果汇总后返回给客户端。这样,客户端不需要了解子系统的复杂性,只需要知道如何使用外观类提供的接口。

外观没有“完全封装”子系统的类,外观只提供简化的接口。所以客户端觉得有必要,依然可以直接使用子系统的类。这是外观模式一个很好的特征,提供简化接口的同时,依然将系统完整的功能暴露出来,以供需要的人使用

简单示例

以下是一个简单的外观模式示例:

// 子系统1
class Subsystem1 {
    public void operation1() {
        System.out.println("Subsystem1 operation1");
    }
}
// 子系统2
class Subsystem2 {
    public void operation2() {
        System.out.println("Subsystem2 operation2");
    }
}
// 外观类
class Facade {
    private Subsystem1 subsystem1;
    private Subsystem2 subsystem2;

    public Facade() {
        this.subsystem1 = new Subsystem1();
        this.subsystem2 = new Subsystem2();
    }

    public void operation() {
        subsystem1.operation1();
        subsystem2.operation2();
    }
}
// 客户端代码
public class Client {
    public static void main(String[] args) {
        Facade facade = new Facade();
        facade.operation();
    }
}

在上述示例中,Subsystem1Subsystem2 是两个子系统,Facade 是一个外观类,用于封装这两个子系统。客户端代码只需要调用 Facadeoperation() 方法,就可以执行子系统1和子系统2的操作。

外观模式的优点包括:

  • 简化客户端代码,使客户端更容易使用复杂的子系统。
  • 降低耦合度,使得子系统的变化不会影响到客户端。
  • 提高了灵活性,可以随时添加或删除子系统,而不需要修改客户端代码。

外观模式的缺点是可能会导致外观类变得庞大,以及限制了客户端的灵活性。因此,需要在设计时权衡各方面的因素,选择合适的设计模式

中介者模式(Mediator )

​ 中介者设计模式(Mediator Pattern)是一种行为型设计模式,它可以让对象间的交互更加松散,避免对象之间的直接耦合。在中介者模式中,对象不再直接相互作用,而是通过中介者对象来协调完成。

中介者模式包含以下角色:

  • 抽象中介者(Mediator):定义了中介者对象的接口,用于与各个同事对象之间进行通信。
  • 具体中介者(Concrete Mediator):实现抽象中介者的接口,协调各个同事对象,从而实现协作行为。
  • 抽象同事类(Colleague):定义了同事类的接口,用于与中介者通信。
  • 具体同事类(Concrete Colleague):实现抽象同事类的接口,与其他同事类进行通信,完成自身的任务。

主要解决

主要解决:对象与对象之间存在大量的关联关系,这样势必会导致系统的结构变得很复杂,同时若一个对象发生改变,我们也需要跟踪与之相关联的对象,同时做出相应的处理。

何时使用:多个类相互耦合,形成了复杂的网状结构。

如何解决:将上述网状结构分离为星型结构。

代码示例

下面是一个使用Java实现的简单示例,模拟两个用户之间通过聊天室进行消息发送和接收的场景。

实际应用

中介者模式在现实生活中的应用非常广泛,以下是一些实际的应用场景:

  1. MVC框架:在MVC(Model-View-Controller)框架中,控制器(Controller)就是一个中介者,负责协调视图(View)和模型(Model)之间的交互。

  2. 航空控制系统:在航空控制系统中,控制塔就是一个中介者,负责协调各个飞机之间的交通流量,以确保安全。

  3. 聊天室:在聊天室中,服务器可以充当中介者的角色,负责将用户之间的消息传递。

  4. 多人协作:在多人协作的系统中,中介者可以协调不同用户之间的交互,以便于协同工作。

  5. 游戏开发:在游戏中,中介者可以协调不同游戏对象之间的交互,以确保游戏规则的顺畅执行。

总的来说,中介者模式可以应用于任何需要协调不同对象之间交互的场景,从而实现松耦合和可维护性。

优缺点分析

优点: 1、降低了类的复杂度,将一对多转化成了一对一。 2、各个类之间的解耦。 3、符合迪米特原则。

缺点:中介者会庞大,变得复杂难以维护。

访问者模式(Visitor )

模式总述

下面是几种常用设计模式的名称和简单描述。

模式 描述 使用频率
单例模式 确保有且只有一个对象被创建
工厂方法 由子类决定要创建的具体对象是哪一个
抽象工厂 允许客户创建对象的家族,而无需指定它们的具体类 一般
适配器模式 将一种接口转换为另一种接口
模板方法模式 由子类决定如何实现一个算法中的步骤 非常高
策略模式 封装可以互换的算法,并使用委托来决定要是用哪一个 非常高
代理模式 包装对象,以控制对此对象的访问 非常高
观察者模式 让对象在状态变化时被通知 非常高
外观模式 让接口访问更简单(在多个子系统的接口之上定义高层接口)
装饰者模式 包装一个对象,以提供新的行为
迭代器模式 提供一种方式顺序访问聚合对象中的每个元素,而又不暴露其内部实现 非常高
状态模式 一般
组合模式
命令模式 . 一般
解释器模式 特定场景
建造者模式 又叫构建器或生成器模式,将一个复杂类的表示与其构造相分离,
使得相同的构建过程能够得出不同的表示;

空对象模式

README

作者:银法王

版权声明:本文遵循知识共享许可协议3.0(CC 协议): 署名-非商业性使用-相同方式共享 (by-nc-sa)

参考:

 《Head First 设计模式》

​ 《深入设计模式》

修改记录:

  2019-12-12 第一次修订

  2020-07-27 排版

  2023-04-21 完善内容


OOP之设计模式
http://jackpot-lang.online/2020/01/08/系统技术架构设计/OOP之设计模式/
作者
Jackpot
发布于
2020年1月8日
许可协议