设计模式用前须知

版权声明:本文为博主学习笔记, 注明来源情况下随意转载 https://blog.csdn.net/lengxiao1993/article/details/72915459
  1. 参考书籍: 《Design Patterns: Elements of Reusable Object-Oriented Software》

设计模式用前须知1: 为什么有人说设计模式故弄玄虚

大部分程序员编写的程序可以分为三类(应用程序、工具包 、框架),使用设计模式的目的是提高代码的可复用性和可扩展性(灵活性), 但是设计模式在这三类软件中所发挥的效果是不一样的。

很多有经验的程序员会得出“使用了设计模式,反而降低了代码的可读性,增加了复杂度”的结论, 并把这种问题总结为过度设计。 这种总结其实有失偏颇, 因为设计模式所提供的灵活性本身就是有代价的。 很多程序员所编写的商业应用程序, 其问题领域其实相对固定,对代码的灵活性和重用性要求并不高(相对于与工具包和框架而言), 所以应用程序的编写者往往在不了解设计模式的情况下,也可以很好地解决一些灵活性需求和扩展性需求。

具体来说使用设计模式的必要性的程度是逐级递增的:应用程序(Application) < 工具包/类库(ToolKit/Library) < 框架(Framework)
这里写图片描述

  • 应用程序

    • 在编写应用程序的时候使用设计模式, 设计模式可以帮助的是内部重用,管理和扩展问题。或者使得一个应用减少对于平台的依赖。
  • 工具包

    • 工具包或库是依赖相互关联的可重用的类, 用来提供一些有用的且非常通用的功能, 例如集合类库(提供了集合类List, Stack, Set 等)。 这一类代码是为了帮助应用程序完成其功能而编写的, 所以非常调代码复用。 因此, 工具包设计通常是要比应用程序的设计困难的, 毕竟, 作为工具包的设计者,要向千差万别的应用程序提供尽可能通用的功能, 因此, 在编写过程中, 工具包设计过程中要避免场景假设, 避免有可能会限制工具包灵活度的依赖。相较于应用程序, 在工具包/类库中有效地使用特定的设计模式,就成了一件几乎不可避免的事。
  • 框架

    • 框架和工具包很是类似, 都实一系列相互关联,重用性很高的类和功能的封装, 但是区别是框架的核心是提供一种针对特定类别软件来说可重用的设计。例如Java程序员所熟知的SSH框架宏观上来看, 就是提供了一种广泛应用于商业网站程序的MVC设计。 框架实际上阐释了你的应用程序的架构, 它会定义总体的结构,把不同的职责划分到不同的类和对象中。 所以当我们在使用框架的时候, 其实框架就已经帮我们预定义好了在该问题领域很多通用的设计决策。 所以框架的精髓是设计重用 而非 代码重用, 尽管框架中往往包含了很多可以复用的代码。
    • 所以很多程序员在不熟知设计模式技巧的时候也能编写有效解决问题的另一个原因就是, 很多时候,框架已经帮我们完成了很多设计决策, 我们只是在编写被框架所调用的代码而已。

设计模式用前须知2: 为什么有“组合优于继承”的说法

首先需要注意, 广为流传的“组合优于继承” 的说法是一种不严谨的翻译, 其来源如下

众多设计模式包含的2个最核心原则(引自参考书籍1)

  • Program to an interface, not an implementation. (面向接口编程,而不是具体的实现)
  • Favor object composition over class inheritance.(如果某个场景的代码复用既可以通过类继承实现, 也可以通过对象组合实现, 尽量选择对象组合的设计方式)

第一个原则的好处非常明显: 可以极大程度地减少子系统具体实现之间的相互依赖。
第二个原则则不那么容易理解, 下面展开叙述 。

对象组合与类继承的对比

面向对象设计的过程中, 两个最常用的技巧就是类继承对象组合,同一个场景下的代码复用,这两个技巧基本上都可以完成。 但是他们有如下的区别:

  • 通过继承实现的代码复用常常是一种“白盒复用”, 这里的白盒指的是可见性: 对于继承来说,父类的内部实现对于子类来说是不透明的(实现一个子类时, 你需要了解父类的实现细节, 以此决定要复用哪些父类方法, 以及要重写哪些父类方法)
  • 对象组合实现的代码复用则是一种“黑盒复用”“: 对象的内部细节不可见,对象仅仅是以“黑盒”的方式出现(仅仅需要关注接口定义, 可以通过改变对象引用来改变其行为方式

这里通过汽车的刹车逻辑进行说明。 对于汽车来说, 存在多种不同的型号, 我们会很自然的希望定义一个类 Car 来描述所有汽车通用的刹车行为 brake(), 然后通过某种方式(继承/组合)来为不同的型号的汽车提供不同的刹车行为。

  • 如果通过继承来实现, 思路就是定义一个Car, 实现不同子类 CarModelA, CarModelB 来重写父类的 brake() 方法以体现不同型号车的刹车行为区别。
public abstract class Car {

    // 也可以将该方法设置成抽象方法, 强迫子类来实现该方法
    public void brake() {
      // 提供一个默认的刹车实现
      ...
    }
}

public class CarModelA extends Car {

    public void brake() {
      aStyleBrake();// A 风格的刹车行为
    }
}

public class CarModelB extends Car {

    public void brake() {
      bStyleBrake(); // B 风格的刹车行为
    }
}

上述的例子展现了如何通过继承来完成不同型号车辆刹车行为的变化。但是可以注意到, 每一个型号的车的刹车行为是在编译时就确定好的 , 没有办法在运行时刻将 CarModelB 的刹车行为赋予 CarModelA 。

  • 如果通过对象组合的实现方式, 则需要为 Car 定义一个引用, 该引用的类型是一个为刹车行为定义的接口。
public interface IBrakeBehavior {
    public void brake();
}

public class AStyleBrake implements IBrakeBehavior {
    public void brake() {
        aStyleBrake(); // A 风格的刹车行为
    }
}

public class BStyleBrake implements IBrakeBehavior {
    public void brake() {
        bStyleBrake(); // B 风格的刹车行为
    }
}

//通过给下面的类赋予 AStyleBrake 或 BStyleBrake 可以完成不同 Model 的刹车行为的切换 

// 同理, 汽车其他的行为(如启动 launch) 也可以用类似的方法实现
// 不同型号的汽车实现, 可以通过赋予不同风格的行为实例来 “组装” 出来的, 也就不需要为 Car 定义不同的子类了 
public class Car{
    protected IBrakeBehavior brakeBehavior;

    public void brake() {
        brakeBehavior.brake();
    }

    public void setBrakeBehavior(final IBrakeBehavior brakeType) {
        this.brakeBehavior = brakeType;
    }
}

值得注意的是, 上面的刹车行为不一定需要通过接口来实现, 定义一个 BrakeBehaviour 的父类, 然后再定义AStyleBrake , BAStyleBrake 来继承该类, 实现不同的行为, 同样是组合方式的应用。

所以不难发现, 当我们拿类继承组合在一起进行对比时, 并不是以实现方式中是否有用到类继承而区分的。

我们真正关注的是行为的继承行为的组合 :需要变化的行为是通过 继承后重写的方式 实现, 还是通过 赋予不同的行为实例 实现。

继承与组合的优缺点对比

类继承优点:

  • 类之间的继承关系时在编译时刻静态地定义好的, 因此使用起来也非常直观, 毕竟继承是被编程语言本身所支持的功能。
  • 类继承也使得修改要重用的代码变得相对容易, 因为可以仅仅重写要更改的父类方法。

类继承缺点:

  • 第一个缺点是伴随第一个优点而生的: 没有办法在运行时刻改变继承了父类的子类行为。
    • 这一点在之前汽车的例子中已经进行了说明
  • 第二个缺点与第一个缺点相比往往更严重: 通过继承实现的代码复用,本质上把父类的内部实现细节暴露给了子类, 子类的实现会和父类的实现紧密的绑定在一起, 结果是父类实现的改动,会导致子类也必须得改变。
    • 以之前的例子进行说明, 如果是通过继承的方式来实现不同型号汽车的刹车行为变化, 假设现在我们基于 Car 这个父类实现了 10 种不同型号的汽车 CarModel( A, B, C, D, E, F, G,H ,I , J ), 其中前 5 个型号( A、B、C、D、E) 都没有重写父类的刹车方法, 直接使用了父类 Car 提供的默认方法, 后 5 个型号均提供了自己独特的 brake 实现 。 现假设, 我们希望对 Car 中的 brake 方法进行升级改造, 然而,升级改造后的 brake 行为只适用于C,D , 最早的两种型号A, B 并不兼容升级后的刹车行为。 这样, 我们为了保证 A, B 依旧能正常工作, 就不得不把旧的 brake 实现挪到 A、B 中。 或者, 分别去升级 C、 D、E 中的 brake 方法。

对象组合优点:

  • 对象的组合是在运行时刻通过对象之间获取引用关系定义的,所以对象组合要求不同的对象遵从对方所实现的接口来实现引用传递, 这样反过来会要求更加用心设计的接口,以此支持你在使用一个对象时, 可以把它和很多其他的对象组合在一起使用而不会出现问题。
  • 对象的组合由于是通过接口实现的, 这样在复用的过程中是不会打破其封装的。 任意一个对象都可以在运行时刻被替换成另外一个实现了相同接口且类型相同对象, 更重要的是,由于一个对象的实现是针对接口而编写的, 具体实现之间的依赖会更少。
  • 对象组合的方式可以帮助你保持每个类的内聚性,让每个类专注实现一个任务。 类的层次会保持的很小,不会增长到一种无法管理的恐怖数量。 (这也是为什么Java语言支持单继承的原因

对象组合缺点:

  • 不具备之前所罗列的类继承的优点

猜你喜欢

转载自blog.csdn.net/lengxiao1993/article/details/72915459