从封装变化的角度看设计模式——组件协作

编程

设计模式">什么是设计模式

​ 要了解设计模式,首先得清楚什么是模式。什么是模式?模式即解决一类问题的方法论,简单得来说,就是将解决某类问题的方法归纳总结到理论高度,就形成了模式。

​ 设计模式就是将代码设计经验归纳总结到理论高度而形成的。其目的就在于:1)可重用代码,2)让代码更容易为他人理解,3)保证代码的可靠性。

​ 使用面向对象的语言很容易,但是做到面向对象却很难。更多人用的是面向对象的语言写出结构化的代码,想想自己编写的代码有多少是不用修改源码可以真正实现重用,或者可以实现拿来主义。这是一件很正常的事,我在学习过程当中,老师们总是在说c到c++的面向对象是一种巨大的进步,面向对象也是极为难以理解的存在;而在开始的学习过程中,我发现c++和c好像差别也不大,不就是多了一个类和对象吗?但随着愈发深入的学习使我发现,事实并不是那么简单,老师们举例时总是喜欢用到简单的对象群体,比如:人,再到男人、女人,再到拥有具体家庭身份的父亲、母亲、孩子。用这些来说明类、对象、继承......似乎都显得面向对象是一件轻而易举的事。

​ 但事实真是如此吗?封装、粒度、依赖关系、灵活性、性能、演化、复用等等,当这些在一个系统当中交错相连,互相耦合,甚至有些东西还互相冲突时,你会发现自己可能连将系统对象化都是那么的困难。

​ 而在解决这些问题的过程当中,也就慢慢形成了一套被反复使用、为多数人知晓、再由人分类编目的代码设计经验总结——设计模式。

设计原则

​ 模式既然作为一套解决方案,自然不可能是没有规律而言的,而其所遵循的内在规律就是设计原则。在学习设计模式的过程当中,不能脱离原则去看设计模式,而是应该透过设计模式去理解设计原则,只有深深地把握了设计原则,才能写出真正的面向对象代码,甚至创造自己的模式。

  1. 开闭原则(Open Close Principle)

    ​ 开闭原则的意思是:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不要去修改原有的代码。这样是为了使程序的扩展性更好,更加易于维护和升级。而想要达到这样的效果,就需要使用接口和抽象类。

  2. 里氏替换原则(Liskov Substitution Principle)

    ​ 里氏替换原则中说,任何基类可以出现的地方,子类一定可以出现。也就是说只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

  3. 依赖倒置原则(Dependence Inversion Principle)

    ​ 依赖倒置原则是开闭原则的基础,具体内容:抽象不应该依赖具体,而是具体应当依赖抽象;高层模块不应该依赖底层模块,而是高层和底层模块都要依赖抽象。因为抽象才是稳定的,这个原则想要说明的就是针对接口编程。

  4. 接口分离原则(Interface Segregation Principle)

    ​ 这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。这个原则所要求的就是尽量将接口最小化,避免一个接口当中拥有太多不相关的功能。

  5. 迪米特法则,又称最少知道原则(Demeter Principle)

    ​ 最少知道原则是指:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。迪米特法则在解决访问耦合方面有着很大的作用,但是其本身的应用也有着一个很大的缺点,就是对象之间的通信造成性能的损失,这是在使用过程中,需要去折衷考虑的。

  6. 组合复用原则(Composite Reuse Principle)

    ​ 组合复用原则或者说组合优先原则,也就是在进行功能复用的过程当中,组合往往是比继承更好的选择。这是因为继承的形式会使得父类的实现细节对子类可见,从而违背了封装的目的。

  7. 单一职责原则(Single Responsibility Principle)

    ​ 一个类只允许有一个职责,即只有一个导致该类变更的原因。类职责的变化往往就是导致类变化的原因:也就是说如果一个类具有多种职责,就会有多种导致这个类变化的原因,从而导致这个类的维护变得困难。

​ 设计模式是设计原则在应用体现,设计原则是解决面向对象问题处理方法。在面对访问耦合的情况下,有针对接口编程、接口分离、迪米特法则;处理继承耦合问题,有里氏替换原则、优先组合原则;在保证类的内聚时,可以采用单一职责原则、集中类的信息与行为。这一系列的原则都是为了一个目的——尽可能的实现开闭。设计模式不是万能的,它是设计原则互相取舍的成果,而学习设计模式是如何抓住变化和稳定的界线才是设计模式的真谛。

GOF-23 模式分类

​ 从目的来看,即模式是用来完成什么工作的;可以划分为创建型、结构型和行为型。创建型模式与对象的创建有关,结构型模式处理类或对象的组合,行为型模式对类和对象怎样分配职责进行描述。

​ 从范围来看,即模式是作用于类还是对象;可以划分为类模式和对象模式。类模式处理类和子类之间的关系,这些关系通过继承建立,是静态的,在编译时刻就确定下来了;对象模式处理对象间的关系,这些关系可以在运行时刻变化,更加具有动态性。

组合之下,就产生了以下六种模式类别:

  1. 类创建型模式:将对象的创建工作延迟到子类中。

  2. 对象创建型模式:将对象的创建延工作迟到另一个对象的中。

  3. 类结构型模式:使用继承机制来组合类。

  4. 对象创建型模式:描述对象的组装形式。

  5. 类行为型模式:使用继承描述算法和控制流。

  6. 对象行为型模式:描述了一组对象怎样协作完成单个对象所无法完成的任务。

从封装变化的角度来看

​ GOF(“四人组”)对设计模式的分类更多的是从用途方法进行划分,而现在,我们希望从设计模式中变化和稳定结构分隔上来理解所有的设计模式,或许有着不同的收获。

​ 首先要明白的是,获得最大限度复用的关键在于对新需求和已有需求发生变化的预见性,这也就要求系统设计能够相应地改进。而设计模式可以确保系统以特定的方式变化,从而避免系统的重新设计,并且设计模式同样允许系统结构的某个方面的变化独立于其他方面,这样就在一定程度上加强了系统的健壮性。

​ 根据封装变化,可以将设计模式划分为:组件协作、单一职责、对象创建、对象性能、接口隔离、状态变化、数据结构、行为变化以及领域问题等等。

设计模式之组件协作

​ 现代软件专业分工之后的第一个结果就是“框架与应用程序的划分”,“组件协作”就是通过晚期绑定,来实现框架与应用程序之间的松耦合,是二者之间协作时常用的模式。其典型模式就是模板方法、策略模式和观察者。

模板方法——类行为型模式

  1. 意图

​ 定义一个操作中的算法的骨架,并将其中一些步骤的实现延迟到子类中。模板方法使得子类可以重定义一个算法的步骤而不会改变算法的结构。

  1. 实例

​ 程序开发库和应用程序之间的调用。假设现在存在一个开发库,其内容是实现对一个文件或信息的操作,操作包含:open、read、operation、commit、close。但是呢!只有open、commit、close是确定的,其中read需要根据具体的operation来确定读取方式,所以这两个方法是需要开发人员自己去实现的。

​ 那我们第一次的实现可能就是这种方式:

//标准库实现

public class StdLibrary {

public void open(String s){

System.out.println("open: "+s);

}

public void commit(){

System.out.println("commit operation!");

}

public void close(String s){

System.out.println("close: "+s);

}

}

//应用程序的实现

public class MyApplication {

public void read(String s,String type){

System.out.println("使用"+type+"方式read: "+s);

}

public void operation(){

System.out.println("operation");

}

}

//或者这样实现

public class MyApplication extends StdLibrary{

public void read(String s,String type){

System.out.println("使用"+type+"方式read: "+s);

}

public void operation(){

System.out.println("operation");

}

}

//这里两种实现方式的代码调用写在一起,就不分开了。

public class MyClient {

public static void main(String[] args){

//方式1

String file = "ss.txt";

StdLibrary lib = new StdLibrary();

MyApplication app = new MyApplication();

lib.open(file);

app.read(file,"STD");

app.operation();

lib.commit();

lib.close(file);

//方式2

MyApplication app = new MyApplication();

app.open(file);

app.read(file,"STD");

app.operation();

app.commit();

app.close(file);

}

}

​ 这种实现,无论是方式1还是方式2,对于仅仅是作为应用来说,当然是可以的。其问题主要在什么地方呢?就方式1 而言,他是必须要使用者了解开发库和应用程序两个类,才能够正确的去应用。

​ 方式2相较于方式1,使用更加的简单些,但是仍然有不完善的地方,就是调用者,需要知道各个方法的执行顺序,这也是1和2共同存在的问题。而这刚好就是Template Method发挥的时候了,一系列操作有着明确的顺序,并且有着部分的操作不变,剩下的操作待定。

//按照Template Method结构可以将标准库作出如下修改

public abstract class StdLibrary {

public void open(String s){

System.out.println("open: "+s);

}

public abstract void read(String s, String type);

public abstract void operation();

public void commit(){

System.out.println("commit operation!");

}

public void close(String s){

System.out.println("close: "+s);

}

public void doOperation(String s,String type){

open(s);

read(s,"STD");

operation();

commit();

close(s);

}

}

​ 在修改过程中,将原来的类修改成了抽象类,并且新增了两个抽象方法和一个doOperation()。通过使用抽象操作定义一个算法中的一些步骤,模板方法确定了它们的先后顺序,但它允许Library和Application子类改变这些具体的步骤以满足它们各自的需求,并且还对外隐藏了算法的实现。当然,如果标准库中的不变方法不能被重定义,那么就应该将其设置为private或者final。

//修改过后的Appliaction和Client

public class MyApplication extends StdLibrary {

@Override

public void read(String s, String type){

System.out.println("使用"+type+"方式read: "+s);

}

@Override

public void operation(){

System.out.println("operation");

}

}

public class MyClient {

public static void main(String[] args){

String file = "ss.txt";

MyApplication app = new MyApplication();

app.doOperation(file,"STD");

}

}

​ 模板方法的使用在类库当中极为常见,尤其是在c++的类库当中,它是一种基本的代码复用技术。这种实现方式,产生了一种反向的控制结构,或者我们称之为“好莱坞法则”,即“别找我们,我们找你”;换名话说,这种反向控制结构就是父类调用了子类的操作(父类中的doOperation()调用了子类实现的read()operation()),因为在平时,我们的继承代码复用更多的是调用子类调用父类的操作。

  1. 结构

  2. 参与者

    • AbstractClass(StdLibrary)

      定义抽象的原语操作(可变部分)。

      实现一个模板方法(templateMethod()),定义算法的骨架。

    • ConcreteClass(具体的实现类,如MyApplication)

      实现原语操作以完成算法中与特定子类相关的步骤。

    除了以上参与者之外,还可以有OperatedObject这样一个参与者即被操作对象。比如对文档的操作,文档又有不同的类型,如pdf、word、txt等等;这种情况下,就需要根据不同的文档类型,定制不同的操作,即一个ConcreteClass对应一个OperatedObject,相当于对结构当中由一个特定操作对象,扩展到多个操作对象,并且每个操作对象对应一个模板方法子类。

  3. 适用性

    对于模板方法的特性,其可以应用于下列情况:

    • 一次性实现一个算法的不变部分,并将可变的行为留给子类来实现。
    • 各子类中公共的行为应被提取出来并集中到一个公共父类中,以避免代码重复。重构方式即为首先识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后用一个模板方法调用这些新的操作,来替换这些不同的代码。
    • 控制子类的扩展。模板方法只有特定点调用"hook"操作,这样就只允许在这些扩展点进行相应的扩展。

  4. 相关模式

    ​ Factory Method经常被Template Method所调用。比如在参与者当中提到的,如果需要操作不同的文件对象,那么在操作的过程中就需要read()方法返回不同的文件对象,而这个read()方法不正是一个Factory Method。

    ​ Strategy:Template Method使用继承来改变算法的一部分,而Strategy使用委托来改变整个算法。

  5. 思考

    • 访问控制 在定义模板的时候,除了简单的定义原语操作和算法骨架之外,操作的控制权也是需要考虑的。原语操作是可以被重定义的,所以不能设置为final,还有原语操作能否为其他不相关的类所调用,如果不能则可以设置为protected或者default。模板方法一般是不让子类重定义的,因此就需要设置为final.
    • 原语操作数量 定义模板方法的一个重要目的就是尽量减少一个子类具体实现该算法时,必须重定义的那些原语操作的数目。因为,需要重定义的操作越多,应用程序就越冗长。
    • 命名约定 对于需要重定义的操作可以加上一个特定的前缀以便开发人员识别它们。
    • hook操作 hook操作就是指那些在模板方法中定义的可以重定义的操作,子类在必要的时候可以进行扩展。当然,如果可以使用父类的操作,不扩展也是可以的;因此,在Template Method中,应该去指明哪些操作是不能被重定义的、哪些是hook(可以被重定义)以及哪些是抽象操作(必须被重定义)。

策略模式——对象行为型模式

  1. 意图

    ​ 定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。Strategy使得算法可以独立于使用它的客户而变化。

  2. 实例

    ​ 策略模式是一种非常经典的设计模式,可能也是大家经常所见到和使用的设计模式;重构过程中选择使用策略模式的一个非常明显的特征,就是代码当中出现了多重条件分支语句,这种时候为了代码的扩展性,就可以选择使用策略模式。

    ​ 比如正面这样的代码,实现一个加减乘除运算的操作。

     public class Operation {

    public static void main(String[] args) {

    binomialOperation(1,1,"+");

    binomialOperation(1,3,"-");

    binomialOperation(1,2,"*");

    binomialOperation(1,1,"/");

    binomialOperation(1,0,"/");

    }

    public static int binomialOperation(int num1,int num2,char ch){

    switch(ch){

    case "+":

    return num1+num2;

    case "-":

    return num1+num2;

    case "*":

    return num1*num2;

    case "/":

    if(num2!=0){return num1/num2;}

    else {

    System.out.println("除数不能为0!");

    }

    }

    return num2;

    }

    }

    ​ 上面的代码完全可以实现我们想要的功能,但是如果现在需求有变,需要再增加一个‘与’和‘或’的二目运算;那在这种情况下,势必需要去修改源码,这样就违背了开闭原则的思想。因此,使用策略模式,将上面代码修改为下列代码。

 //Strategy

public interface BinomialOperation {

public int operation(int num1,int num2);

}

public class AddOperation implements BinomialOperation {

@Override

public int operation(int num1, int num2) {

return num1+num2;

}

}

public class SubstractOperation implements BinomialOperation {

@Override

public int operation(int num1, int num2) {

return num1-num2;

}

}

public class MultiplyOperation implements BinomialOperation {

@Override

public int operation(int num1, int num2) {

return num1*num2;

}

}

public class DivideOperation implements BinomialOperation {

@Override

public int operation(int num1, int num2) {

if(0!=num2){

return num1/num2;

}else{

System.out.println("除数不能为0!");

return num2;

}

}

}

//Context

public class OperatioContext {

BinomialOperation binomialOperation;

public void setBinomialOperation(BinomialOperation binomialOperation) {

this.binomialOperation = binomialOperation;

}

public int useOperation(int num1,int num2){

return binomialOperation.operation(num1,num2);

}

}

public class Client {

public static void main(String[] args) {

OperatioContext oc = new OperatioContext();

oc.setBinomialOperation(new AddOperation());

oc.useOperation(1,2);

//......

}

}

	代码很简单,就是将运算类抽象出来,形成一种策略,每个不同的运算符对应一个具体的策略,并且实现自己的操作。Strategy和Context相互作用以实现选定的算法。当算法被调用时,Context可以将自身作为一个参数传递给Strategy或者将所需要的数据都传递给Strategy,也就是说 `OperationContext`中`useOperation()`的`num1`和`num2`可以作为为`OperationContext`类的属性,在使用过程中直接将`OperationContext`的对象作为一个参数传递给`Strategy`类即可。

通过策略模式的实现,使得增加新的策略变得简单,但是其缺点就在于客户必须了解 不同的策略。

  1. **结构 **

  2. 参与者

  • Strategy (如BinomialOperation)

    定义所有支持的算法的公共接口。Context使用这个接口来调用某具体的Strategy中定义的算法。

  • ConcreteStrategy(如AddOperation...)

    根据Strategy接口实现具体算法。

  • Context(如OperationContext)

    • 需要一个或多个ConcreteStrategy来进行配置,使用多个策略时,这些具体的策略可能是不同的策略接口的实现。比如,实现一个工资计算系统,工人身份有小时工、周结工、月结工,这种情况下,就可以将工人身份独立为一个策略,再将工资支付计划(用以判断当天是否为该工人支付工资日期)独立为一个策略,这样Context中就需要两个策略来配置。
    • 需要存放或者传递Strategy需要使用到的所有数据。

  1. 适用性

    当存在以下情况时,可以使用策略模式:

    • 许多相关的类仅仅是行为有异。“策略”提供了一种多个行为中的一些行为来配置一个类的方法。
    • 需要使用一个算法的不同变体。例如,你可以会定义一些反映不同空间/时间权衡的算法,当这些变体需要实现为一个算法的类层次时,就可以采用策略模式。
    • 算法使用客户不应该知道的数据。可以采用策略模式避免暴露复杂的、与算法相关的数据结构。
    • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现。

  2. 相关模式

​ Flyweight(享元模式)的共享机制可以减少需要生成过多Strategy对象,因为在使用过程中,策略往往是可以共享使用的。

  1. 思考

    • Strategy和Context之间的通信问题。在Strategy和Contex接口中,必须使得ConcreteStrategy能够有效的访问它所需要的Context中的任何数据,反之亦然。这种实现一般有两种方式:

      ​ 1)让Context将数据放在参数中传递给Strategy——也就是说,将数据直接发送给Strategy。这可以使得Strategy和Context之间解耦(印记耦合是可以接受的),但有可能会有一些Strategy不需要的数据。

      ​ 2)将Context自身作为一个参数传递给Strategy,该Strategy显示的向Context请求数据,或者说明在Strategy中保留一个Context的引用,这样便不需要再传递其他的数据了。

    • 让Strategy成为可选的。换名话说,在有些实现过程中,客户可以在不指定具体策略的情况下使用Context完成自己的工作。这是因为,我们可以为Context指定一个默认的Strategy的存在,如果有指定Strategy就使用客户指定的,如果没有,就使用默认的。

观察者模式——对象行为型模式

  1. 意图

    ​ 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

  2. 实例

    ​ 观察者模式很常见于图形用户界面当中,比如常见的Listener。观察者模式可以使得应用数据的类和负责界面表示的类可以各自独立的复用。比如,当界面当中存在一个输入表单,在我们对表单进行输入的时候,界面上又会显示这样一个数据的柱状图,以些来对比各项数据。其伪码可以描述成下列这种形式:Histogram作为柱状图类只需要负责接收数据并且显示出来,InputForm作为一个输入表单。在这个 过程中,只要InputForm中的数据发生变化,就相应的改变Histogram 的显示。

    ​ 这种实现方式,明显在InputForm中产生了一种强耦合,如果显现图形发生变化,现在不需要显示为一个柱状图而是一个饼状图,势必又要去修改源码。

    public class Histogram {

    public void draw(int[]nums){

    for (int i:nums ) {

    System.out.print(i+" ");

    }

    }

    }

    public class InputForm {

    private int[] data;

    Histogram histogram;

    public InputForm(Histogram histogram){

    this.histogram = histogram;

    show();

    }

    public void change(int... data){

    this.data = data;

    show();

    }

    public void show(){

    histogram.draw(data);

    }

    }

    public class Client {

    public static void main(String[] args) {

    InputForm inputForm = new InputForm(new Histogram());

    inputForm.change(3,4,5);

    inputForm.change(5,12,13);

    }

    }

    ​ 同时,InputForm 和显示图形之间的关系,刚好符合观察者模式所说的一个对象的状态变化,引起其他对象的更新,同时兼顾考虑开闭问题,可以将HistogramPieChart公共特性提取出来,形成一个Graph接口。另外,有可能InputFrom不只需要显示一种图表,而是需要同时将柱状图和饼状图显示出来,因此在InputFrom中定义的是一个List的结构来存放所有的相关显示图形。

    //Observer

    public interface Graph {

    public void update(Input input);

    public void draw();

    }

    public class Histogram implements Graph {

    private InputForm inputForm;

    public Histogram(InputForm inputForm){

    this.inputForm = inputForm;

    }

    @Override

    public void update(Input inputForm) {

    if(this.inputForm == inputForm){

    draw();

    }

    }

    @Override

    public void draw(){

    System.out.println("柱状图:");

    for (int i: inputForm.getData()) {

    System.out.println(i+" ");

    }

    System.out.println();

    }

    }

    public class PieChart implements Graph {

    private InputForm inputForm;

    public PieChart(InputForm inputForm){

    this.inputForm = inputForm;

    this.inputForm.addGraph(this);

    draw();

    }

    @Override

    public void update(Input inputForm) {

    if(this.inputForm == inputForm){

    draw();

    }

    }

    @Override

    @Override

    public void draw(){

    System.out.println("饼状图:");

    for (int i: inputForm.getData()) {

    System.out.println(i+" ");

    }

    System.out.println();

    }

    }

    ​ 在实际的应用过程中,既然有输入表单的形式,也有可能以其他的形式输入数据,为了以后的扩展,可以将输入形式抽象出来,形成一个Input接口,以便后续的扩展。

    //Subject 目标对象

    public interface Input {

    public void addGraph(Graph graph);

    public void removeGraph(Graph graph);

    public void notifyGraphs();

    }

    public class InputForm implements Input {

    private int[] data;

    private List<Graph graphs = new List;

    public void change(int...data){

    this.data = data;

    notifyGraphs();

    }

    public int[] getData() {

    return data;

    }

    @Override

    public void addGraph(Graph graph){

    graphs.add(graph);

    }

    @Override

    public void removeGraph(Graph graph){

    graphs.remove(graph);

    }

    @Override

    public void notifyGraphs(){

    for (Graph g:graphs ) {

    g.update(this);

    }

    }

    }

    public class Client {

    public static void main(String[] args) {

    InputForm inputForm = new InputForm();

    Histogram h = new Histogram(inputForm);

    PieChart p = new PieChart(inputForm);

    inputForm.change(1,5,6,9,8);

    inputForm.change(2,4,6,8);

    }

    }

  3. 结构

  4. 参与者

    • Subject(目标,如Input)

      • 目标需要知道自己所有的观察者对象。
      • 提供注册和删除观察者对象的接口

    • Observer(观察者,如Graph)

      为那些在目标发生变化时需要获取通知的对象定义一个更新接口。

    • ConcreteSubject(具体目标,如InputForm)

      • 将有关状态(或数据)存放到各ConcerteObserver对象中。
      • 当它的状态发生改变时,向它的各个观察者发出通知。

    • ConcreteObserver(具体观察者,如Histogram)

      • 维护一个指向ConcerteSubject的引用,或者是有关状态的引用。
      • 实现Observer的更新接口以使自身保存的目标状态与目标状态保持一致。

  5. 适用性

    在以下任一情况下可以使用观察者模式:

    • 当一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这二者封装在独立的对象中以使它们可以各自独立的改变和复用。
    • 当对一个对象的改变需要同时改变其它对象,而不知道具体有多少对象有待改变。
    • 当一个对象必须通知其它对象,而它又不能假定其它对象是谁。换言之,你不希望这些对象上紧耦合的。

  6. 相关模式

    ​ Mediator(中介者模式):通过封装复杂的更新语义,可以使用一个ChangeManager来充当目标和观察者之间的中介。在目标的状态变化过程中,有些状态变化可能只是中间临时变化,而还未到最终结果,但这可能引起观察者的更新,这种频繁的更新造成的就是通信代价和性能损失。因此,采用一个ChangeManager可以更好去管理更新操作。

    ​ Singleton(单例模式):ChangeManager可以使用Singleton模式来保证它是唯一的并且是可全局访问的。

  7. 思考

    • 目标与观察者之间的映射。一个目标对象跟踪它应通知的观察者的最简单方法是显式地在目标当中保存对它们的引用,但当目标过多而观察者少时,这样存储的结构可能代价过高。其一个解决办法就是用时间换空间,用一个关联查找机制(例如一个hash表的形式)来维护目标到观察者的映射。这样没有观察者的目标自然不会产生存储上的开销,但是由于关联机制的存在,就相当于在访问观察者的过程中多了一个步骤,就增加了访问观察者的开销。

    • 一个目标可以有很多观察者,一个观察者也同样可以观察很多目标。这种情况下,就需要多观察者的update接口作出一定的改变,使得观察者能够知道是那个目标对象发来通知。

    • 谁来触发更新。一是在对目标状态值进行设定时,自动去调用通知信息。这样客户就不需要去调用Notify(),缺点就在于多个连续的操作就会产生连续的更新,造成效率低下。二是客户自己选择合适的情况下去调用Notify(),这种触发方式优点在于客户可以在操作完成目标对象之后,一次性更新,避免了中间无用的更新。缺点在于一旦客户可能没有调用通知,就容易出错。

    • 如何保证发出通知前目标的状态自身是一致的。确保发出通知前目标状态一致这很重要,因为观察者在更新状态时,需要查询目标的当前状态。这就需要在代码序列中,保证通知是在目标状态修改完成之后进行的,这时就可以采用Template Method来固定操作的顺序。

小结

​ 在这篇文章当中,没有按照GOF对设计模式的分类来对设计模式进行描述,而是在实例的基础上,运用重构的技巧:从静态到动态、从早绑定到晚绑定、从继承到组合、从编译时依赖到运行时依赖、从紧耦合到松耦合。通过这样一种方式来理解设计模式,寻找设计模式中的稳定与变化。

​ 在上面提到的三种模式中,它们对象间的绑定关系,都是动态的,可以变化的,通过这样的方式来实现协作对象之间的松耦合,这也是“组件协作”一个特点。

​ 还有就是关于耦合的理解,有的时候耦合是不可避免的,耦合的接受程度是相对而言的,这取决于我们在实现过程当中对变化的封装和稳定的抽象折衷,这也是我们学习设计模式的目的,就是如何利用设计模式来实现这样一种取舍。

​ 对设计模式细节描述过程,体现的是我在学习设计模式过程中的一种思路。学习一个设计模式,首先要了解它是要干什么的。然后从一个例子出发,去理解它,思考它的一个实现过程。再然后,归纳它的结构,这个结构不仅仅是类图,还包括类图中的各个协作者是需要完成什么的功能、提供什么样的接口、要保存哪些数据以及各各协作者之间是如何协作的,或者说是依赖关系是怎样的。最后,再考虑与其他模式的搭配,思考模式的实现细节。

这里呢,暂时只写出了三种模式,后续的过程中,将会一一地介绍其他的模式。

公众号:良许Linux

有收获?希望老铁们来个三连击,给更多的人看到这篇文章

原文链接:https://www.cnblogs.com/yychuyu/archive/2020/07/12/13289913.html

以上是 从封装变化的角度看设计模式——组件协作 的全部内容, 来源链接: utcz.com/z/518265.html

回到顶部