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


什么是设计模式

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

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

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

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

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

设计原则

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

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

GOF-23 模式分类

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

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

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

从封装变化的角度来看

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

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

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

设计模式之组件协作

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

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

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

  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(可以被重定义)以及哪些是抽象操作(必须被重定义)。
策略模式——对象行为型模式
 //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就使用客户指定的,如果没有,就使用默认的。

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

小结

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

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

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

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

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


—-想了解更多的linux相关异常处理怎么解决关注<计算机技术网(www.ctvol.com)!!>

本文来自网络收集,不代表计算机技术网立场,如涉及侵权请联系管理员删除。

ctvol管理联系方式QQ:251552304

本文章地址:https://www.ctvol.com/linuxsystemusage/124179.html

Like (0)
Previous 2020年7月12日
Next 2020年7月12日

精彩推荐