Java设计模式-合成复用原则

Posted Zip Zou

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java设计模式-合成复用原则相关的知识,希望对你有一定的参考价值。

Java设计模式-合成复用原则

在概述中,为大家列举了在软件设计原则中的几大原则。设计原则(Design Principles)对软件设计过程提出了部分要求,其动机在于为了能够保证系统的灵活性、扩展性及复用性

在Head First原著中,作者以合成复用原则作为开篇,因此在这里基于原著为大家详细介绍为什么合成复用原则如此重要。

鸭子的设计

根据Head First中的小故事,我们对其进行一个复制。首先有个公司做了个模拟类小游戏,该游戏主要模拟鸭子的行为,那么在实现初期,开发者们定义两个一个鸭子的父类,该父类定义了鸭子具有的所有行为:呱呱叫、游泳、展示,并且已经实现的实际的鸭子有绿头鸭、红头鸭两种,其类图如下:

这样看似是个合理的设计,并且每当有新的鸭子产生时,都可以继承该类,实现扩展。

抽象类的思考

久而久之,随着鸭子的增多,现在需要给有的鸭子增加一个行为,需要鸭子会飞。此时问题来了,当我们在接口中添加了飞方法,其子类均以无法正常工作,因为接口改变,他们均需要实现飞方法,但是这种方式破坏了开闭原则,添加了飞方法后,子类需要修改,因此我们考虑将接口改为抽象类,因此有如下类图:

可以看到,我们在接口中新增了fly()方法,并且除飞方方法外,其他方法均为抽象方法,交由子类实现,保证了原有类的正常工作。后来根据需求,该游戏需要添加一个橡皮鸭子,因此我们动态扩展了一个RubberDuck类,现在类图如下:

但是此时可怕的事情发生了,用户在进行游戏时发现,橡皮鸭子也能够飞了。因此开发者知道,对于某些鸭子其不具备会飞的功能,比如橡皮鸭子。因此解决方案也很简单,因为是橡皮鸭子继承了父类的飞方法,那么在橡皮鸭子中重写飞方法,而不做任何操作即可。可这个时候,又新增了一种鸭子DecoyDuck,木头鸭子,它也不能飞,并且不能呱呱叫,经过了橡皮鸭子的“诡异”事件之后,在实现木头鸭子时,便重写了quack()fly()两个方法,并且方法中无任何操作。

貌似合理的实现,系统能够正常运行,并且“具有良好的扩展性”。但是仔细想想可能导致的需求变化:若后续有更多的鸭子需要增加,并且可能不具有飞的功能,或不具有显示功能的隐形鸭子等等,那么很多方法均需要重写,并不加任何实现,做了很多无用功。

考虑使用接口进行重构

此时开发者们考虑对该设计进行重构,既然部分鸭子不具备飞的行为,或不具备呱呱叫的行为,那么可以将飞和呱呱叫都抽象独立出来,考虑使用接口,需要该行为的类实现即可,则有如下类图:

在这种设计下,可以很好的应对部分鸭子没有飞的功能,而部分鸭子能飞的需求。但是这样也存在一个很大的问题:MallarDuckRedheadDuck他们飞的行为是一样的,实现了Flyable接口后,需要实现相同的代码,即经典的Copy & Paste问题。这就导致了面向对象中经典的问题,重复代码(Duplicate Code),无法复用代码。因此上述设计并非一个最优的设计,当需要很多其他鸭子类时,每个会飞的鸭子都需要拷贝一份飞的代码,每个呱呱叫的鸭子都需要拷贝一份quack()方法的实现。

我们参考下原著对该设计的思考:

We know that not all of the subclass have flying and quacking behavior, so inheritance isn’t the right answer. But while having the subclasses implement Flyable and/or Quackable solves part of the problem (no inappropriately flying rubber ducks), it completely destroys code reuse for those behaviors, so it just creates a different maintenance nightmare. And of course there might be more than one kind of flying behavior even among the ducks that do fly…

识别变化

在原著中,对识别变化原则,有如下说明:

Identify the aspects of your application that vary and separate them from what stays the same.

识别你的应用中的变化很大的部分,并且将它们从相同的部分中分离出来。

take the parts that vary and encapsulate them, so that later you can alter or extend the parts that vary without affecting those that don’t.

原著中对该原则的解读是,对于变化的部分,我们要将其拿出来,并对它们进行封装,即对变化的部分进行封装封装变化了之后我们可以选择性地修改或扩展这种差异很大的变化,而不会影响原有的行为。这样做的好处在于,代码更改后只会产生少量的意外结果,但提高了系统的灵活性。

Fewer unintended consequence from code changes and more flexibility in your systems.

根据上述实现,我们可以发现,对于鸭子而言,flyquack行为是可变的行为,对于不同类型的鸭子,其这两方面的行为差异很大,部分鸭子有上述行为而部分鸭子却完全没有,或部分鸭子在行为上的表现完全不同。因此上述行为便是变化的行为,根据识别变化的原则,我们需要对fly行为和qucak行为进行分离,并对它们进行封装。

在上一部分中,我们考虑过使用接口对行为进行抽象,而我们在进行变化封装时可以借鉴该思想。由于行为存在变化,因此需要对其进行封装,而在进行封装时,抽象是保证封装可以扩展的常用手段,因此我们需要对行为加以抽象。但是同一个行为是存在差异,因此同一个行为,需要一系列的类来进行实现,因此就有了在Head First中如下的结构图:

通过对变化的封装,我们对行为进行了抽象,这便体现了又一个软件设计原则:

Program to an interface, not an implementation.

面向接口编程,而不是面向实现编程,注意这里的接口指的是更上一层的抽象,指的是超类或父类,而不仅仅是Java语言中的接口!

“Program to an interface” really means “Program to a supertype”.

There’s a concept of interface, but there’s also the Java construct interface. You can program to an interface, without having to actually use a Java interface.

因此有了上述两个接口后,针对不同的行为我们可以做具体的泛化,如FlyWithWingsFlyNoWay,表示使用翅膀飞和无法飞行的两个不同行为,对于叫的行为,其有呱呱叫、吱吱叫、以及无声三种行为,其类图如下:

如此我们便可以将不同的行为进行封装起来,并动态地扩展叫和飞的行为,并且扩展新的行为,完全不用修改原有的行为实现,这里就体现了“开闭原则”。当其他对象存在上述的行为时,便可重用一个行为,因为对于这些可变的行为来说,它们不再隐藏在鸭子类的内部,而是独立出来单独提供服务。

With this design, other types of objects can reuse our fly and quack behaviors because these behaviors are no longer hidden away in our Duck classes!

And we can add new behaviors without modifying any of the Duck classes that use flying behaviors.

接下来我们应该思考,如何将这些行为与鸭子耦合起来呢?

仍然是用继承吗?如果使用继承,在Java环境下无法多继承,飞和叫如何同时继承?

因此继承无法实现,我们考虑使用聚合代替原有的继承实现,这样可行吗?

We’d like to keep things flexible, after all, it was the inflexibility in the duck behaviors that got us into trouble in the first place. And we know that we want to assign behaviors to the instances of Duck.

why not make sure that we can change the behavior of a duck dynamically?

我们存在这样的需求,那便是需要能够灵活的将行为分配给鸭子类的实例,那么我们为何不考虑我们设计一种方案使得我们可以动态地改变鸭子的行为呢?

注意上面加粗的文字,这是一种模式:策略模式(Strategy),但是本部分主要介绍设计原则,该模式后续再给大家介绍。

那么我们回到我们的问题,用聚合的方式代替继承可以实现吗?答案是必然的!

我们在鸭子类中,若需要某种行为,则该类中聚合该行为接口,使得其可以通过制定具体的行为对象,来动态地改变其行为,并且行为相互独立,具有相同行为的鸭子实例,只需要使用同一个或同一种行为对象即可。我们画出这种结构下的类图:

从图中可以清楚的看出,行为从相同部分中分离出来,并独立扩展服务,每个不同的鸭子都会有不同的行为。

其实这里便是合成复用原则的体现,对于可以变化的行为,使用聚合代替了原有的继承,使得行为的变化更加具有灵活性,并且可以在运行时动态地修改行为的具体实现。

Favor composition over inheritance.

原则的具体介绍见上一篇文章

接下来我们做个简单的实现

Flyable接口:

public interface Flyable 
  public void fly();

Quackable接口:

public interface Quackable 
  public void quack();

FlyWithWings-翅膀飞行为类:

public class FlyWithWings implements Flyable 
  public void fly() 
    System.out.println("用翅膀飞...");
  

FlyNoWay-无法飞行行为类:

public class FlyNoWay implements Flyable 
  public void fly() 
  

Quack呱呱叫行为类:

public class Qucak implements Quackable 
  public void quack() 
    System.out.println("呱呱叫...");
  

Squeak吱吱叫行为类:

public class Squeak implements Quackable 
  public void quack() 
    System.out.println("吱吱叫...");
  

MuteQuack无声叫行为类:

public class MuteQuack implements Quackable 
  public void quack() 
  

Duck鸭子类:

public abstract class Duck 
  public void swim() 
    System.out.println("鸭子游泳...");
  
  public void display() 
    System.out.println("鸭子现身...");
  

MallarDuck-绿头鸭子类:

public class MallarDuck extends Duck 
  private Flyable flyBehavior;
  private Quackable quackBehavior;
  public MallarDuck() 
    flyBehavior = new FlyWithWings();
    quackBehavior = new Quack();
  
  public void performFly() 
    flyBehavior.fly()
  
  public void performQuack() 
    quackBehavior.quack()
  

RubberDuck-橡皮鸭子类:

public class RubberDuck extends Duck 
  private Flyable flyBehavior;
  public RubberDuck() 
    quackBehavior = new Squeak();
  
  public void performQuack() 
    quackBehavior.quack()
  

DecoyDuck-木鸭子类:

public class RubberDuck extends Duck 
  public RubberDuck() 
  

重新审视的设计

现在我们来重新评审下我们现有的设计,我们有了行为的接口:FlyableQuackable,对于不同的行为都是一个接口的具体子类,可以动态扩展,符合“开闭原则”。相比于之前的设计,我们利用在鸭子类中聚合行为类,使得部分没有对应行为的鸭子类,能够不实现相应的方法,避免了重复无效代码。此外对于同一个行为,不同的鸭子可以共享使用,避免了由于实现行为接口,而需要重复编写相同行为代码的问题,实现了代码复用。

总结:

  • 利用合成代替继承,实现了代码复用;
  • 提出了要识别变化的原则,对变化进行封装;
  • 提出了要面向接口编程,而不是面向实现编程,以抽象来实现灵活、动态扩展;

思考下,对于简单工厂模式,其是否满足“开闭原则”?如果不满足,如何设计能够使它满足呢?
答案是不满足的,实现“开闭原则”其实很简单,只要对工厂进行抽象即可,这便是工厂方法模式。

在第一步,我们首先利用接口,通过对多个鸭子的泛化,我们得到了不同的鸭子类,但是该设计会导致接口发生改变时,所有的子类均需要改变,破坏了开闭原则。因此我们采用抽象类,对需要添加的方法,统一实现,有需要的类对其进行重写,在保证原有系统可用的情况下,动态添加新的功能。但这种设计会导致行为不统一,不具备该行为的类需要重写行为方法,但是这样同样存在问题,不存在该行为的类却有该行为的方法。因此我们考虑使用接口对行为进行封装,含有该行为的类实现对应的接口,但是相同行为的类会需要重复编写相同的代码,不能代码重用。因此我们考虑不使用继承的方式,而使用组合代替继承,能够实现“开闭原则”,并保证能够保证类设计的合理性。

以上是关于Java设计模式-合成复用原则的主要内容,如果未能解决你的问题,请参考以下文章

从零开始学习Java设计模式 | 软件设计原则篇:合成复用原则

从零开始学习Java设计模式 | 软件设计原则篇:合成复用原则

面向对象编程原则(08)——合成复用原则

面向对象编程原则(08)——合成复用原则

手撸golang 架构设计原则 合成复用原则

设计模式软件设计七大原则 ( 合成复用原则 | 代码示例 )