设计模式之组件设计模式

设计模式: 每一个模式描述了一个我们周围不断重复发生的问题,以及该问题的解决方案的核心方法。 这样就能一次一次的使用这个方案而不必做重复劳动。

组件协作模式

现代软件专业分工之后的第一个结果是框架与应用程序的划分,在应用程序调用框架的过程中必然具有大量的协作的问题。在软件构建过程中,对于某一项任务, 它常常有稳定的整体操作结构,但各个子步骤却有很多改变的需求,或者由于固有原因(比如框架与应用之间关系)而无法和任务的整体结构同时实现。如果设计的不好,可能会导致它们之间耦合度非常高,从而导致功能的扩展和改变需求变得十分困难。组件协作模式就用来解决这个问题。

组件协作模式通过晚期绑定,来实现框架与应用程序之间的松耦合,是二者之间协作时常用的模式。

典型模式

  • Template Method
  • Strategy
  • Observer / Event

1、模板方法(Template Method)

模板方法模式:定义一个操作中的算法的骨架(稳定),而将一些步骤延迟(变化)到子类中。Template Method 使得子类可以不改变(复用)一个算法的结构即可重定义该算法的某些特定步骤。(其实在我看来这就相当于Java中的编译时多态,即方法的重载)

类图
在这里插入图片描述
       事实上,模版方法是编程中一个经常用到的模式。先来看一个例子,某日,程序员A拿到一个任务:给定一个整数数组,把数组中的数由小到大排序,然后把排序之后的结果打印出来。经过分析之后,这个任务大体上可分为两部分,排序和打印,打印功能好实现,排序就有点麻烦了。但是A有办法,先把打印功能完成,排序功能另找人做。


abstract class AbstractSort {
	
	/**
	 * 将数组array由小到大排序
	 * @param array
	 */
	 //抽象方法
	protected abstract void sort(int[] array);
	
	//模板方法
	public void showSortResult(int[] array){
		this.sort(array);
		System.out.print("排序结果:");
		for (int i = 0; i < array.length; i++){
			System.out.printf("%3s", array[i]);
		}
	}
}

       写完后,A找到刚毕业入职不久的同事B说:有个任务,主要逻辑我已经写好了,你把剩下的逻辑实现一下吧。于是把AbstractSort类给B,让B写实现。B拿过来一看,太简单了,10分钟搞定,代码如下:


class ConcreteSort extends AbstractSort {
 
	@Override
	protected void sort(int[] array){
		for(int i=0; i<array.length-1; i++){
			selectSort(array, i);
		}
	}
	
	private void selectSort(int[] array, int index) {
		int MinValue = 32767; // 最小值变量
		int indexMin = 0; // 最小值索引变量
		int Temp; // 暂存变量
		for (int i = index; i < array.length; i++) {
			if (array[i] < MinValue){ // 找到最小值
				MinValue = array[i]; // 储存最小值
				indexMin = i; 
			}
		}
		Temp = array[index]; // 交换两数值
		array[index] = array[indexMin];
		array[indexMin] = Temp;
	}
}

写好后交给A,A拿来一运行:


public class Client {
	public static int[] a = { 10, 32, 1, 9, 5, 7, 12, 0, 4, 3 }; // 预设数据数组
	public static void main(String[] args){
		AbstractSort s = new ConcreteSort();
		s.showSortResult(a);
	}
}

运行结果:

排序结果: 0 1 3 4 5 7 9 10 12 32

运行正常。行了,任务完成。没错,这就是模版方法模式。大部分刚步入职场的毕业生应该都有类似B的经历。一个复杂的任务,由公司中的牛人们将主要的逻辑写好,然后把那些看上去比较简单的方法写成抽象的,交给其他的同事去开发。这种分工方式在编程人员水平层次比较明显的公司中经常用到。比如一个项目组,有架构师,高级工程师,初级工程师,则一般由架构师使用大量的接口、抽象类将整个系统的逻辑串起来,实现的编码则根据难度的不同分别交给高级工程师和初级工程师来完成。 怎么样,是不是用到过模版方法模式?

另一方面,我们从上面代码中可以看出,抽象方法是我们逻辑结构中稳定的部分,它定义好了我们业务的骨架和操作流程, 而具体类是我们实现逻辑中需要变化的部分,它主要定义一些具体实现步骤。我们应该是用稳定的部分(模板方法)来调用不稳定的部分,让不稳定的部分(抽象方法,钩子方法)来进行具体的变化,这样我们就可以在变化和稳定之间寻找隔离点,然后来分离它们,从而来管理变化,增强代码的可扩展性及减少耦合性。

模版方法模式的结构

   模版方法模式由一个抽象类和一个(或一组)实现类通过继承结构组成,抽象类中的方法分为三种:

  • 抽象方法:父类中只声明但不加以实现,而是定义好规范,然后由它的子类去实现。
  • 模版方法:由抽象类声明并加以实现。一般来说,模版方法调用抽象方法来完成主要的逻辑功能,并且,模版方法大多会定义为final类型,指明主要的逻辑功能在子类中不能被重写。
  • 钩子方法:由抽象类声明并加以实现。但是子类可以去扩展,子类可以通过扩展钩子方法来影响模版方法的逻辑。
  • 抽象类的任务是搭建逻辑的框架,通常由经验丰富的人员编写,因为抽象类的好坏直接决定了程序是否稳定性。

实现类用来实现细节。抽象类中的模版方法正是通过实现类扩展的方法来完成业务逻辑。只要实现类中的扩展方法通过了单元测试,在模版方法正确的前提下,整体功能一般不会出现大的错误。

模版方法的优点及适用场景

  • 容易扩展。一般来说,抽象类中的模版方法是不易反生改变的部分,而抽象方法是容易反生变化的部分,因此通过增加实现类一般可以很容易实现功能的扩展,符合开闭原则。
  • 便于维护。对于模版方法模式来说,正是由于他们的主要逻辑相同,才使用了模版方法,假如不使用模版方法,任由这些相同的代码散乱的分布在不同的类中,维护起来是非常不方便的。
  • 比较灵活。因为有钩子方法,因此,子类的实现也可以影响父类中主逻辑的运行。但是,在灵活的同时,由于子类影响到了父类,违反了里氏替换原则,也会给程序带来风险。这就对抽象类的设计有了更高的要求。
  • 在多个子类拥有相同的方法,并且这些方法逻辑相同时,可以考虑使用模版方法模式。在程序的主框架相同,细节不同的场合下,也比较适合使用这种模式。

要点总结

  • Template Method模式是一种非常基础性的设计模式,在面向对象系统中有着大量的应用。它用最简洁的机制(多态性),为很多应用程序框架提供了灵活的扩展点,是代码复用方面的基本实现结构。
  • 除了可以灵活应对子步骤的变化外, “不要调用我,让我来调用你” 的反向控制(稳定的部分调用不稳定的部分,即抽象类调用具体类的方法)结构是Template Method的典型应用。
  • 在具体实现方面,被Template Method调用的虚方法可以具有实现,也可以没有任何实现(抽象方法、纯虚方法),但一般推荐将它们设置为protected方法。

2、策略模式(Strategy)

在软件构件过程中,某些对象使用的算法可能多种多样,经常改变,如果将这些对象变得异常复杂;而且有时候支持不使用的算法也是一个性能负担。这时候就需要使用策略模式。即定义一系列的算法,把它们一个个封装起来,并且是他们可以互相替换。该模式使得算法可独立于使用它的客户程序(稳定)而变化(扩展,子类化)。其实在我看来这就相当于Java中的运行时多态,即对象引用所指向的具体类型在运行期间才确定

在这里插入图片描述

在前面说过的模版方法模式也是关注对算法的封装,对照类图可以看到,策略模式与模版方法模式的区别仅仅是多了一个单独的封装类Context,它与模版方法模式的区别在于:在模版方法模式中,调用算法的主体在抽象的父类中,而在策略模式中,调用算法的主体则是封装到了封装类Context中,抽象策略Strategy一般是一个接口,目的只是为了定义规范,里面一般不包含逻辑。 其实,这只是通用实现,而在实际编程中,因为各个具体策略实现类之间难免存在一些相同的逻辑,为了避免重复的代码,我们常常使用抽象类来担任Strategy的角色,在里面封装公共的代码,因此,在很多应用的场景中,在策略模式中一般会看到模版方法模式的影子。

策略模式的结构

  • 封装类:也叫上下文,对策略进行二次封装,目的是避免高层模块对策略的直接调用。
  • 抽象策略:通常情况下为一个接口,当各个实现类中存在着重复的逻辑时,则使用抽象类来封装这部分公共的代码,此时,策略模式看上去更像是模版方法模式。
  • 具体策略:具体策略角色通常由一组封装了算法的类来担任,这些类之间可以根据需要自由替换

代码实现


interface IStrategy {
	public void doSomething();
}
class ConcreteStrategy1 implements IStrategy {
	public void doSomething() {
		System.out.println("具体策略1");
	}
}
class ConcreteStrategy2 implements IStrategy {
	public void doSomething() {
		System.out.println("具体策略2");
	}
}
class Context {
	private IStrategy strategy;
	
	public Context(IStrategy strategy){
		this.strategy = strategy;
	}
	
	public void execute(){
		strategy.doSomething();
	}
}
 
public class Client {
	public static void main(String[] args){
		Context context;
		System.out.println("-----执行策略1-----");
		context = new Context(new ConcreteStrategy1());
		context.execute();
 
		System.out.println("-----执行策略2-----");
		context = new Context(new ConcreteStrategy2());
		context.execute();
	}
}

策略模式的优缺点

   策略模式的主要优点有:

  • 策略类之间可以自由切换,由于策略类实现自同一个抽象,所以他们之间可以自由切换。
  • 易于扩展,增加一个新的策略对策略模式来说非常容易,基本上可以在不改变原有代码的基础上进行扩展。
  • 避免使用多重条件,如果不使用策略模式,对于所有的算法,必须使用条件语句进行连接,通过条件判断来决定使用哪一种算法,在上一篇文章中我们已经提到,使用多重条件判断是非常不容易维护的。

   策略模式的缺点主要有两个:

  • 维护各个策略类会给开发带来额外开销,可能大家在这方面都有经验:一般来说,策略类的数量超过5个,就比较令人头疼了。

  • 必须对客户端(调用者)暴露所有的策略类,因为使用哪种策略是由客户端来决定的,因此,客户端应该知道有什么策略,并且了解各种策略之间的区别,否则,后果很严重。例如,有一个排序算法的策略模式,提供了快速排序、冒泡排序、选择排序这三种算法,客户端在使用这些算法之前,是不是先要明白这三种算法的适用情况?再比如,客户端要使用一个容器,有链表实现的,也有数组实现的,客户端是不是也要明白链表和数组有什么区别?就这一点来说是有悖于迪米特法则的。

要点总结

  • Strategy 及其子类为组件提供了一系列可重用的算法,从而可以使得类型在运行时方便的根据需求在各个算法之间进行切换。
  • Strategy 模式提供了用条件判断语句以外的另一种选择,消除条件判断语句,就在解耦合。含有许多条件判断语句的代码通常都需要Strategy模式。
  • 如果Strategy对象没有实例变量,那么各个上下文可以共享同一个Strategy对象,从而节省对象开销。

3、观察者模式(Observer)

在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系” ------- 一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使得软件不能很好的抵御变化。

类图
在这里插入图片描述
在软件系统中经常会有这样的需求:如果一个对象的状态发生改变,某些与它相关的对象也要随之做出相应的变化。比如,我们要设计一个右键菜单的功能,只要在软件的有效区域内点击鼠标右键,就会弹出一个菜单;再比如,我们要设计一个自动部署的功能,就像eclipse开发时,只要修改了文件,eclipse就会自动将修改的文件部署到服务器中。这两个功能有一个相似的地方,那就是一个对象要时刻监听着另一个对象,只要它的状态一发生改变,自己随之要做出相应的行动。其实,能够实现这一点的方案很多,但是,无疑使用观察者模式是一个主流的选择。

观察者模式的结构

在最基础的观察者模式中,包括以下四个角色:

  • 被观察者: 从类图中可以看到,类中有一个用来存放观察者对象的Vector容器(之所以使用Vector而不使用List,是因为多线程操作时,Vector在是安全的,而List则是不安全的),这个Vector容器是被观察者类的核心,另外还有三个方法:attach方法是向这个容器中添加观察者对象;detach方法是从容器中移除观察者对象;notify方法是依次调用观察者对象的对应方法。这个角色可以是接口,也可以是抽象类或者具体的类,因为很多情况下会与其他的模式混用,所以使用抽象类的情况比较多。
  • 具体的被观察者:使用这个角色是为了便于扩展,可以在此角色中定义具体的业务逻辑。
  • 观察者: 观察者角色一般是一个接口,它只有一个update方法,在被观察者状态发生变化时,这个方法就会被触发调用。
  • 具体的观察者: 观察者接口的具体实现,在这个角色中,将定义被观察者对象状态发生变化时所要处理的逻辑。

观察者代码实现

//被观察者
abstract class Subject {
    private Vector<Observer> obs = new Vector<Observer>();
    public void addObserver(Observer obs){
        this.obs.add(obs);
    }
    public void delObserver(Observer obs){
        this.obs.remove(obs);
    }
    protected void notifyObserver(){
        for(Observer o: obs){
            o.update();
        }
    }
    public abstract void doSomething();
}
//具体的被观察者
public class ConcreteSubject extends Subject {
    public void doSomething(){
        System.out.println("被观察者事件反生");
        this.notifyObserver();
    }
}
//观察者
interface Observer {
    public void update();
}
//具体的观察者1
class ConcreteObserver1 implements Observer {
	public void update() {
		System.out.println("观察者1收到信息,并进行处理。");
	}
}
//具体的观察者2
class ConcreteObserver2 implements Observer {
	public void update() {
		System.out.println("观察者2收到信息,并进行处理。");
	}
}
//客户端
public class Client {
    public static void main(String[] args) {
        Subject sub = new ConcreteSubject();
        ConcreteObserver1 concreteObserver1 = new ConcreteObserver1();
        sub.addObserver(concreteObserver1);
        sub.addObserver(new ConcreteObserver2());
        sub.doSomething();
    }
}

运行结果

被观察者事件反生
观察者1收到信息,并进行处理。
观察者2收到信息,并进行处理。

       通过运行结果可以看到,我们只调用了Subject的方法,但同时两个观察者的相关方法都被同时调用了。仔细看一下代码,其实很简单,无非就是在Subject类中关联一下Observer类,并且在doSomething方法中遍历一下Observer的update方法就行了。

观察者模式的优缺点

       优点:观察者与被观察者之间是属于轻度的关联关系,并且是抽象耦合的,这样,对于两者来说都比较容易进行扩展。

       缺点:观察者模式是一种常用的触发机制,它形成一条触发链,依次对各个观察者的方法进行处理。但同时,这也算是观察者模式一个缺点,由于是链式触发,当观察者比较多的时候,性能问题是比较令人担忧的。并且,在链式结构中,比较容易出现循环引用的错误,造成系统假死。

参考文章:

https://blog.csdn.net/zhengzhb/article/details/7609670

发布了44 篇原创文章 · 获赞 27 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_42784951/article/details/102976106