Skip to content

设计模式之基础篇

x

认识「设计模式」

什么是「模式」

  • C. Alexander《建筑的永恒之道》: “一些问题及其解决方案不断变换面孔「重复出现」,其后是「共同的本质」。这就是模式。”
  • 模式三部分

    • Context:问题所在的上下文(Context),即当前模式所面对的此类问题所在的周围环境和状况,也就是说模式在什么状况下发生作用;
    • Motivation:动机(Motivation),即此模式的目的或预期的目标是什么;
    • Solutions:解决方案(Solutions),即为达到预期目标或解决此类问题所采用解决方案的核心。
  • 模式的实质,就是从不断重复出现的事件中发现和抽象出的规律,是解决问题所形成的「经验的高度归纳、总结」。

什么是「设计模式」

  • “每一个模式描述了一个在我们周围 「不断重复」 发生的问题,以及该问题的 「解决方案」 的核心。这样你就能一次又一次地使用该方案,而不必做重复劳动。”
  • 上世纪 50年代 C. Alexander 《建筑的永恒之道》
  • 关键词:不断重复(问题的重复性)、解决方案

软件领域的设计模式

  • 上世纪 90 年代最早从建筑领域引入软件工程领域 by GoF
  • 《Design Patterns: Elements of Reusable Object-Oriented Software》
  • 《设计模式:可复用面向对象软件的基本元素》
  • 关键词:可复用(目标)、 面向对象(手段)
  • 此书阐明了设计模式的目标 : 复用。

复杂性管理与「面向对象」

从「面向对象」谈起

  • 两种思维方式

    • 当程序员面对一个复杂问题时的两种思维方式:机器思维和抽象思维;两种都很重要,本文重点关注第二种。
    • 机器思维(向下分解)

      • 如何把握机器底层原理,从微观理解对象构造
      • 如语言构造、编译转换、内存模型、运行时机制等。
    • 抽象思维(向上抽象)

      • 如何将周围世界抽象为程序代码。
      • 如组件封装、面向对象、设计模式、架构模式等
  • 两个视角深入理解「面向对象」

    • 三大特性(向下理解)

      • 封装,隐藏内部实现
      • 继承,复用现有代码
      • 多态,改写对象行为
    • 抽象意义(向上理解)

      • 深刻把握面向对象机制所带来的抽象意义,理解如何这些机制来表达现实世界,掌握什么是「好的面向对象设计」。

软件设计复杂的根本原因

  • 软件设计复杂的根本原因:变化。
  • 客户需求变化
  • 技术平台变化
  • 开发团队变化
  • 市场环境变化
  • ......

如何管理复杂性?

  • 思维模型

    • 分解思维

      • 常见做法: 分而治之,将大问题「分解」为多个小问题,将复杂问题分解为多个简单问题。
    • 抽象思维

      • 由于不能掌握全部的复杂对象,我们选择 忽视它的非本质细节 ,而去处理「泛化和理想化」了的对象模型。
  • 结构化(SD) vs 面向对象(OOD)

    • 诞生背景

      • OOD 和 SD (Structured Design,结构化设计)的概念几乎同时诞生,分别以不同的方式来表现数据结构和算法。
      • NATO 会议采纳了Dijkstra的思想,整个软件产业都同意 goto 语句的确是有害的,结构化方法、瀑布模型从70年代开始大行其道。
      • 到90年代,OOD突然风靡了整个软件行业。
    • SD的问题

      • SD中模块被组织成一个树型结构,棵树的根就是主模块。
      • 顶端模块关心规模最大的问题,负责最重要的策略;底层模块只实现最小的细节。
      • 体系结构中越靠上,概念的抽象层次就越高,也越接近问题领域。
      • 但是,由于上方的模块需要调用下方的模块,所以这些 「上方的模块就依赖于下方的细节」。 也就是说,当实现细节变化时,抽象也会受到影响。
    • OOD的精髓

      • 我们希望 倒转这种依赖关系:我们创建的抽象不依赖于任何细节,而细节则高度依赖于上面的抽象。
      • 依赖关系的倒转正是 OOD 和传统 SD 之间根本的差异,也正是 OOD 思想的精华所在。
  • Show Me the Code

    • SD(分解)

    • OOD(抽象)

    • 结论:用面向对象设计的方式,能更好的抵御软件的变化所造成的影响。

理解「设计原则」

前文总结:为什么要面向对象设计

  • 软件设计复杂性的根本原因: 变化
  • 「变化」是「复用」的天敌,而「面向对象」设计的优势在于:抵御变化。
  • 不是说有了变化,「面向对象」设计能完全避免做任何改变,而是它能够将变化引起的改变降到最小。
  • 宏观上

    • 面向对象的构建方式更能适应软件的变化;
    • 「隔离」变化,将变化带来的影响降到最小。
  • 微观

    • 面向对象更强调各个类的的责任;需求变化所导致的新增类不应该影响原来类的实现,所谓各司其职、各负其责。
    • 即「各司其职」, 更强调各个类的的责任。
  • 重新理解什么是对象

    • 语言实现层面:封装了代码和数据的结构。
    • 规格层面:可被使用的公共接口。
    • 抽象层面:某种拥有责任的抽象。

依赖倒置原则

  • 依赖倒置原则 (Dependence Inversion Principle, DIP)
  • 高层模式(稳定)不应该依赖底层模(变化)块,两者都应依赖其抽象(稳定);
  • 抽象(稳定)不应该依赖实现细节(变化),细节应该依赖抽象(稳定)。
  • 说人话: 面向接口编程,而不是面向实现类

开放封闭原则

  • 开放封闭原则 (Open Closed Principle, OCP)
  • 软件实体应当对扩展开放,对修改关闭。
  • 说人话: 扩展新类,而不是修改旧类
  • 合成复用原则、里氏替换原则都是开闭原则的具体实现规范。

单一指责原则

  • 单一指责原则 (Single Responsibility Principle, SRP)
  • 一个类应该有且一个引起它变化的原因,否则类应该被拆分。
  • 变化的方向隐含着类的责任。
  • 说人话: 每个类只负责自己的事情,而不是变成万能。

里氏置换原则

  • 里氏置换原则 (Liskov Substitution Principle, LSP)
  • 继承必须确保,超类所拥有的性质在子类中仍然成立。
  • 子类必须能够替换他们的基类(IS-A)。
  • 说人话: 继承父类而不去改变父类。

接口隔离原则

  • 接口隔离原则 (Interface Segregation Principle, ISP)
  • 不应该强迫客户程序依赖他们不用的方法。
  • 接口应用小而完备。
  • 一个类对另一个类的依赖应该建立在最小接口上
  • 说人话: 各个类建立自己的专用接口(多个),而不是建立万能接口(一个)

合成复用原则

  • 又叫组合/聚合复用原则(Composite/Aggregate Reuse Principle, CARP)
  • 软件复用时,要尽量先使用组合或者聚合等关系来实现,其次才考虑使用继承关系来实现。
  • 说人话: 优先对象组合,其次类继承
  • 继承在某种程度上破坏类封装性,子类父类耦合度高;对象组合则只要求被组合的对象具有良好的定义,耦合度低。
  • 合成复用原则(Composite Reuse Principle, CRP)

迪米特法则

  • 迪米特法则 (Law of Demeter, LoD)
  • 最少知识原则(Least Knowledge Principle, LKP)
  • 只与你的直接朋友交谈,不跟"陌生人”说话。Talk only to your immediate friends and not to strangers.
  • 说人话: 无需直接交互的两个类,如果需要交互,使用中介者

封装变化点

  • 使用封装来创建对象之间的分界层,一侧进行修改 (变化),另一侧不受影响(稳定),从而达到松耦合。

面向接口设计

  • 任何时代任何领域,产业强盛的标志:接口标准化
  • 1)秦国为何能一统六国? 兵器接口标准化 、货币、道路等
  • 2)雕版印刷 vs 毕升的活字印刷

非面向对象设计原则

  • DRY、KISS、YAGNI三原则

    • SOLID,GRASP设计原则:适用于面向对象设计;
    • DRY、KISS、YAGNI 软件三原则:适用于在软件设计的各个层面的。它不仅适用于面向对象的设计,也适用于面向过程的程序设计;不仅适用于类的设计,也适用于模块、子系统的设计;就连在项目架构运维部署中,也适用这套简单的法则。
  • DRY - Don't Repeat Yourself

    • 第一条准则是千万不要重复你自身。
    • 尽量在项目中减少重复的代码行,重复的方法,重复的模块。其实,许多设计原则和模式最本质的思想都是在消除重复。我们常提起的 重用性可维护性 其实是基于减少重复这一思想。
    • 使其每一个部件都是职责明确的并且可重用的。
  • KISS - Keep It Simple & Stupid

    • 第二条准则是保持简单易懂。
    • 高手高就高在可以将复杂的东西“简单”的实现出来。
    • 们应该致力于代码的 可理解性;降低复杂度也意味着维护变得简单。
    • Martin Flower在《重构》中有一句经典的话:"任何一个傻瓜都能写出计算机可以理解的程序,只有写出人类容易理解的程序才是优秀的程序员。“
  • YAGNI - You Ain’t Gonna Need It

    • 第三条准则是你将不会需要它。千万不要进行过度设计。
    • 一些设计是否必要,更多的应该基于当前的情况。而不是为了应对未来的各种变化,画蛇添足的设计。
    • 因为创业公司的时间是非常宝贵的,早一步推出新的服务就能抢占先机;过度设计往往会延缓开发迭代的速度。

从原则到经验

将设计原则提升为设计经验

  • 设计习语 Design Idioms

    • 描述与特点语言相关的低层模式、技巧、惯用法
  • 设计模式 DesignPattern

    • 主要描述 「类与相互通信的对象之间」 的组织关系,包括它们的角色、职责、协作方式等。
    • 主要解决: 变化中的复用性
  • 架构模式 Architectural Pattern

    • 描述 系统 中与基本结构组织关系密切的高层模式,子系统的划分、责任,以及如何组织它们之间关系的规则。

Refactoring to Patterns

  • 现代软件设计的特征是 「需求的频繁变化」。设计模式的要点是 「寻找变化点,然后在变化点处使用设计模式 ,从而更好应对需求变化」。
  • 所谓「好的面向对象设计」,指那些可以满足 「应对变化,提高复用」 的设计。
  • 设计模式的应用不宜先入为主,没有一步到位的设计。敏捷开发的提倡 「Refactoring to Patterns」,是目前公认的最好应用设计模式的方法。

重构关键技法

  • 静态 -> 动态
  • 早绑定 -> 晚绑定
  • 继承 -> 组合
  • 编译时依赖 -> 运行时依赖
  • 紧耦合 -> 松耦合

UML与依赖

什么是 UML

  • Unified Modeling Language,统一建模语言,用来设计软件模型的图形化建模语言。
  • 9种图:用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图。

类图概述

  • 类图(Class Diagram), 显示了模型的静态结构,包括类、类的内部结构和类之间的关系。
  • 类图是系统分析和设计阶段的产物。

「类」的表示

  • 类名、属性、方法
  • 属性/方法的可见性

    • +:表示 public
    • -:表示 private
    • #:表示 protected
  • 属性:可见性 名称:类型 [= 默认值]

  • 方法:可见性 名称(参数列表)[; 返回类型]

「类之间关系」的表示法

  • 关联关系

    • 图例:带箭头的实线

    x

    • 表示:类与类之间的引用关系,包括:一般关联关系,聚合关系和组合关系。
    • 单向关联、双向关联、自关联
  • 聚合关系

    • 图例:带空心菱形的实线,菱形指向整体

    x

    • 表示:整体与部分之间的关联关系,属于强关联关系。
    • 通过成员对象实现,成员对象可以脱离整体对象而独立存在。如 School <- Teacher。
  • 组合关系

    • 图例:带实心菱形的实线,菱形指向整体

    x

    • 表示:类之间的整体与部分之间的关联关系,一种更强烈的聚合关系。
    • 整体可以控制部分的生命周期,部分对象不可脱离整体对象而存在。如 Head <- Mouth。
  • 依赖关系

    • 图例:带箭头的虚线,箭头指向被依赖对象。

    x

    • 表示:一种使用关系,临时性的关联, 最弱关联关系。
    • 通常表现为:局部变量、方法的参数、或对被依赖对象的静态方法调用。
  • 继承关系(Generalization)

    • 图例:带空心箭头的实线,箭头指向父类。

    x

    • 表示:父类与子类,一般与特殊,泛化或继承,对象间 最强的耦合关系。
  • 实现关系

    • 图例:带空心箭头的虚线,箭头指向接口。

    x

    • 表示:接口与实现类之间的关系。
  • 耦合性依次递增:依赖 < 关联 < 聚合 < 组合 < 继承 < 实现