《敏捷软件开发──原则、模式与实践》阅读笔记

《敏捷软件开发──原则、模式与实践》阅读笔记

Table of Contents

1 敏捷开发

1.1 敏捷联盟宣言

个体和交互赛过过程和工具
人是得到成功的最为重要的因素。若是团队中没有优秀的成员,那么就是使用好的过程也不能从失败中挽救项目。 可是,孬的过程却能够使最优秀的团队成员推进盗用。若是不能做为一个团队进行工做,那么即便拥有最优秀的成员也同样会惨败。
能够工做的软件赛过面面俱到的文档
对于团队来讲,编写并维护一份系统原理和结构方面的文档将老是一个好主意, 可是那套文档应该是短小而且主题突出的。
客户合做赛过合同谈判
成功的项目须要有序、频繁的客户反馈。不是依赖于合同或者关于工做的陈述, 而是让软件的客户和开发团队密切地在一块儿工做,并尽可能常常地提供反馈。
响应变化赛过遵循计划
计划不能考虑得过远。道德,商务环境极可能会变化,这会会引发需求的变更。其次,一旦客户看到系统开始运做, 他们极可能会改变需求。最后,即便咱们熟悉需求,而且确信它们不会发迹,咱们仍然不能很好地估算出开发它们须要的时间。

1.2 敏捷开发的原则

  1. 咱们最优先要作的是经过尽早的、待续的交付有价值的软件来使客户满意
  2. 即便到了开发的后期,也欢迎改变需求。敏捷过程利用变化来为客户创造竞争优点。
  3. 常常性地交付能够工做的软件,交付的间隔能够从几周到几个朋,交付的时间间隔越短越好。
  4. 在整个项目开发期间,业务人员和开发人员必须每天都在一块儿工做。
  5. 围绕被激励起来的我的来构建项目。给他们提供所须要的环境和支持,而且信任他们可以完成工做。
  6. 在团队内部,最具备效果而且富有效率的传递信息的方法,就是面对面的交谈。
  7. 工做的软件是首要的进度度量标准。
  8. 敏捷过程提倡可持续的开发速度。责任人、开发者和用户应该可以保持一个长期的、恒定的开发速度。
  9. 不能地关注优秀的技能和好的设计会加强敏捷能力。
  10. 简单──使未完成的工做最大化的──是根本的。
  11. 最好的构架、需求和设计出自于自组织的团队。
  12. 每隔必定时间,团队会在如何才能更有效地工做方面进行检讨,而后相应地对本身的行为进行调整。

2 极限编程

极限编程是敏捷方法中最著名的一个。它由一系列简单却互相依赖的实践组成。这些实践结合在一块儿造成了一个胜于部分结合的总体。 css

  1. 客户做为团队成员
  2. 用户素材
  3. 短交付周期
  4. 验收测试
  5. 结对编程
  6. 测试驱动的开发方法
  7. 集体全部权
  8. 持续集成
  9. 可持续的开发速度
  10. 开放的工做空间
  11. 计划游戏
  12. 简单的设计
  13. 重构
  14. 隐喻

3 设计原则

3.1 单一职责原则(SRP)

定义
就一个类而言,应该仅有一个引发它变化的缘由。
什么是职责
在SRP中,咱们把职责定义为“变化的缘由”。若是你可以想到多于一个的动机去改变一个类,那么这个类就具备多于一个的职责。 有时,咱们很难注意到这一点。咱们习贯于以组的形式去考虑职责。

3.2 开放——封闭原则(OCR)

定义
软件实体(类、模块、函数等等)应该是能够扩展的,可是不可修改的。

3.2.1 遵循开放──封闭原则设计出的模块具备两个主要的特征

对于扩展开放
模块的行为是能够扩展的。当应用的需求改变时,咱们能够对模块进行扩展,使其具备知足那些改变的新行为。
对于更改是封闭的
对模块行为进行扩展时,没必要改动模块的源代码或者二进制代码。模块的二进制可执行版本, 不管是可连接的库、DLL或者Java的.jar文件,都无需改动。

3.3 Liskov替换原则(LSP)

定义
子类型必须可以替换掉它们的基类型。
相对知足
事实上,一个模型,若是孤立地看,里氏替换并不具备真正意义上的有效性,模型的有效性只能经过它的客户程序来表现。
启发示方法
  1. 在派生类中存在退化函数并不老是表示违反了LSP,可是当这种状况存在时,
  2. 当在派生类中添加了其基类不会抛出的异常时,若是基类的使用者不指望这些异常,那么把它们添加到派生类的方法中应付致使不可替换性。 此时要遵循LSP,要么就必须改变使用者的指望,要么派生类就不该该抛出这些异常。

3.4 依赖倒置原则(DIP)

定义
  • 高层模块不该该依赖于低层模块,两者都应该位赖于抽象。
  • 抽象不该该依赖于细节,细节应该依赖于抽象。
解释
请注意这里的倒置不只仅是依赖关系的倒置,它也是接口全部权的倒置。当应用了DIP时,每每是客户拥有抽象接口, 而它们的服务者则从这些抽象接口派生。
启发示规则──领事于抽象
  • 任何变量都不该该持有一个指向具体类的指针或者引用。
  • 任何类都不该该从具体类派生。
  • 任何方法都不该该覆写它的任何基类中的已经实现了的方法。
  • 若是一个具体类不太会改变,而且也不会建立其余相似的派生类,那么依赖于它并不会形成损害。

3.5 接口隔离原则(ISP)

定义
不该该强制客户领事于它们不用的方法。若是强迫客户程序依赖于那些它们不使用的方法, 那么这些客户程序就面临着因为这些未使用方法的改变所带来的变动,这无心中致使了全部客户程序之间的耦合。

4 经常使用设计模式

4.1 Command模式和Active Object

4.1.1 Command模式的优势

  1. 经过对命令概念的封装,能够解除系统的逻辑互联关系和实际链接的设备以前的耦合。
  2. 另外一个Command模式的常见用法是建立和执行事务操做。
  3. 解耦数据和逻辑,能够将数据放在一个列表中,之后再进行实际的操做。

4.1.2 Active Object模式

描述
Active Object模式是实现多线程控制的一项古老的技术。 控制核心对象维护了一个Command对象的链表。用户能够向链表中增长新的命令,或者调用执行动做,该动做只是遍历链表,执行并去除每一个命令。
RTC任务
采用该技术的变体一去构建多线程系统已是而且将会一直是一个很常见的实践。这种类型的线程被称为run-to-completion任务(RTC), 由于每一个Command实例在下一个Command补全能够运行以前就运行完成了。RTC的名字意味着Command实例不会阻塞。
共享运行时堆栈
Command实例一经运行就必定得完成的的赋予了RTC线程有趣的优势,寻就是它们共享同一个运行时堆栈。和传统的多线程中的线程不一样, 没必要为每一个RTC线程定义或者分配各处的运行时堆栈。这在须要大量线程的内存受限系统中是一个强大的优点。

4.2 Template Method模式和Strategy模式:继承和委托

4.2.1 Template Method模式

描述
Template Method模式展现了面向对象编程上诸多经典重用形式中的一种。其中通用算法被放置在基类中, 而且经过继承在不一样的具体上下文实现该通用算法。
代价
继承是一种很是强的关系,派生类不可避免地要和它们的基类绑定在一块儿。

4.2.2 Strategy模式

描述
Strategy模式使用了一种很是不一样的方法来倒置通用算法和具体实现之间的依赖关系。不是将通用的应用算法放进一个抽象基类中, 而是将它放进一个具体类中,在该具体类中定义一个成员对象,该成员对象实现了实际须要执行的具体算法, 在执行通用算法时,把具体工做委托给这个成员对象的所实现的抽象接口去完成。

4.2.3 对比

共同点
Template Method模式和Strategy模式均可以用来分离高层的算法和的具体实现细节,都容许高速的算法独立于它的具体实现细节重用。
差别
Strategy模式也容许具体实现细节独立于高层的算法重用,不过要唯一些额外的复杂性、内存以及运行时间开销做为代价。

4.3 Facade模式和Mediator模式

4.3.1 facade模式

使用场景
当想要为一组具备复杂且全面的接口的对象提供一个简单且特定的接口时,能够使用Facade模式,以下图所示的场景。

https://img2018.cnblogs.com/blog/1436518/201909/1436518-20190904010411849-1020175305.jpg

Figure 1: Facade模式封装数据库操做html

基于约定
使用Facade模式意味着开发人员已经接受了全部数据库调用都要经过DB类的约定。若是任务一部分代码越过该Facade直接去访问java.sql, 那么就违反了该约定。基于约定,DB类成为了java.sql包的唯一代理。

4.3.2 Mediator模式

示例
图中展现用一个JList和一个JTextField构造了一个QuickEntryMediator类的实例。QuickEntryMediator向JTextField注册了一个匿名的

DocumentListener,每当文本发生变化时,这个listener就调用textFieldChanged方法。接着,该方法在JList中査找以这个文本为前缀的元素并选中它。 JList和JTextField的使用者并不知道该Mediator的存在。它安静地呆着,把它的策略施加在那些对象上,而无需它们的容许或者知晓。 java

https://img2018.cnblogs.com/blog/1436518/201909/1436518-20190904010428928-173900011.jpg

Figure 2: Mediator模式python

4.3.3 对比

相同点
两个模式都有着共同的目的,它们都把某种策略施加到另一组对象上,这些对象不须要知道具体的策略细节。
不一样点
Facade一般是约定的关注点,每一个人都赞成去使用该facade而不是隐藏于其下的对象;而Mediator则对用户是隐藏的,

它的策略是既成事实而不是一项约定事务。 算法

4.4 Singleton模式和Monostate模式

4.4.1 Singleton模式

描述
Singleton是一个很简单的模式。Singleton实例是经过公有的静态方法instance()访问的,即便instance方法被屡次调用,

每次返回的都是指向彻底相同的实例的引用。Singleton类没有公有构造函数,因此若是不使用instance方法,就没法去建立它的实例。 sql

优势
  1. 跨平台。使用合适的中间件(例如RMI),能够把Singleton模式扩展为跨多个JVM和多个计算机工做
  2. 适用于任何类:只需把一个类的构造函数变成私有的,而且在其中增长相应的静态函数和变量,就能够把这个类变为Singleton
  3. 能够透过派生建立:给定一个类,能够建立它的一个Singleton子类。
  4. 延迟求值(Lazy Evaluation):若是Singleton从未使用过,那么就决不会建立它。
代价
  1. 摧毁方法未定义:没有好的方法去推毁(destroy)一个Singleton,或者解除其职责。即便添加一个decommission方法把theInstance置为null,

系统中的其余模块仍然持有对该Singleton实例的引用。这样,随后对instance方法的调用会建立另一个实例,导致同时存在两个实例。 这个问题在C++中尤其严重,由于实例能够被推毁,可能会致使去提领(dereference)一个已被摧毁的对象。 shell

  1. 不能继承:从Singleton类派生出来的类并非Singleton。若是要使其成为Singleton,必需要增长所需的静态函数和变量。
  2. 效率问题:每次调用instance方法都会执行语句。就大多数调用而言,语句是多余的。(使用JAVA的初始化功能可避免)
  3. 不透明性:Singleton的使用者知道它们正在使用一个Singleton,由于它们必需要调用instance方法

4.4.2 Monostate模式

描述
该模式经过把全部的变量都变成静态变量,使全部实例表现得象一个对象同样。
优势
  1. 透明性:使用Monostate对象和使用常规对象没有什么区别,使用者不须要知道对象是Monostate
  2. 可派生性:Monostate的派生类都是Monostate。事实上,Monostate的全部派生类都是同一个Monostate的一部分。它们共享相同的静态变量。
  3. 多态性:因为Monostate的方法不是静态的,因此能够在派生类中覆写它们。所以,不一样的派生类能够基于一样的静态变量表现出不一样的行为。
代价
  1. 不可转换性:不能透过派生把常规类转换成Monostate类。
  2. 效率问题:由于Monostate是真正的对象,因此会致使许多的建立和摧毁开销。
  3. 内存占用:即便从未使用Monostate,它的变量也要占据内存空间。
  4. 平台局限性:Monostate不能跨多个JVM或者多个平台工做。

4.4.3 对比

  1. Singleton模式使用私有构造函数和一个静态变量,以及一下静态方法对实例化进行控制和限制;Monostate模式只是简单地把对象的全部变量变成静态的。
  2. 若是但愿经过派生去约束一个现存类,而且不介意它的全部调用都都必需要调用instance方法来获取访问权,那么Singleton是最合适的。
  3. 若是但愿类的单一性本质对使用者透明,或者但愿使用单一对象的多态派生对象,那么Monostate是最合适的。

4.5 Null Object模式

Employee e = DB.getEmployee("Bob");
if (e != null && e.isTimeToPay(today))
  e.pay();
场景
考虑如上代码,咱们常使用这&&这样的表达式进行空值检查,大多数人也曾因为忘记进行null检查而受挫。该惯用方法虽然常见,

但倒是丑陋且易出错的。经过让getEmployee方法抛出异常,能够减小出错的可能,但try/catch块比null检查更加丑陋。 这种场景下能够使用Null Object模式来解决这些问题(以下图所示)。 数据库

https://img2018.cnblogs.com/blog/1436518/201909/1436518-20190904010446283-591183928.jpg

Figure 3: Null Object模式编程

4.6 Facotry模式

问题示例
依赖倒置原则(DIP)告诉咱们应该优先依赖于抽象类,而避兔依赖于具体类。当这些具体类不稳定时,更应该如此。 所以,该代码片断违反了这个原则: Circle c= new Circle(origin, 1) ,Circle是一个具体类。 因此,建立 Circle类实例的模块确定违反了DIP。事实上,任何一行使用了new关键字的代码都违反了DIP。
应用场景
Factory模式容许咱们只依赖于抽象接口就能建立出具体对象的实例。 因此,在正在进行的开发期间,若是具体类是高度易变的,那么该模式是很是有用的。

4.6.1 可替换的工厂

使用工厂的一个主要好处就是能够把工厂的一种实现替换为另外一种实现。这样,就能够在应用程序中替换一系列相关的对象。 设计模式

https://img2018.cnblogs.com/blog/1436518/201909/1436518-20190904010515261-1337594394.jpg

Figure 4: 可替换的工厂

4.6.2 合理使用工厂模式

严格按照DIP来说,必需要对系统中全部的易变类使用工厂。此外,Factory模式的威力也是诱人的。这两个因素有时会诱使开发者把工厂做为缺省方式使用。 我不推荐这种极端的作法。我不是一开始就使用工厂,只是在很是须要它们的状况下,我才把它们放入到系统中。 例如,若是有必要使用Proxy模式,那么就可能有必要使用工厂去建立持久化对象。或者,在单元测试期间, 若是遇到了必需要欺骗一个对象的建立者的状况时,那么我极可能会使用工厂。可是我不是开始就假设工厂是必要的。

使用工厂会带来复杂性,这种复杂性一般是能够避免的,尤为是在一个正在演化的设计的初期。 若是缺省地使用它们,就会极大地增长扩展设计的难度。为了建立一个新类,就必需要建立出至少4个新类: 两个表示该新类及其工厂的接口类,两个实现这些接口的具体类。

4.7 Composite模式

https://img2018.cnblogs.com/blog/1436518/201909/1436518-20190904010542466-1215965641.jpg

Figure 5: Composite模式

描述
上图展现了Composite模式的基本结构。基类Shape有两个派生类:Circle和Square,第三个派生类是一个组合体。

CompositeShape持有一个含有多个Shape实例的列表。当调用CompositeShape的draw()方法时,它就把这个方法委托给列表中的每个Shape实例。 所以,一个CompositeShape实例就像是一个单一的Shape,能够把它传递给任何使用Shape的函数或者对象,而且它表现得就像是个Shape。 不过,实际上它只是一组Shape实例的代理。

4.8 Observer模式

问题描述
有一个计时器,会捕获来自操做系统的时钟中断,生成一个时间戳。如今咱们想实现一个数字时钟,将时间戳转换为日期和时间,并展现。 一种可行的方式是不停轮询获取最新的时间戳,而后计算时间。但时间戳只有在捕获到时钟中断时,都会发生变化,轮询是会形成CPU的极大浪费。
描述
另外一种解决方案时在计时器时间发生变化时,告知数字时钟,数字时钟既而更新时间。这里,数字时钟为计时器的观察者(Observer)。
示例

https://img2018.cnblogs.com/blog/1436518/201909/1436518-20190904010610601-1689871226.jpg

Figure 6: Observer模式示例

其中MockTimeSink是MockTimeSource的观察者,经过TestClockDriver将MockTimeSink注册到MockTimeSource的观察者队列中。 当MockTimeSource发生变化时,它会调用notifyObservers()方法遍历各个观察者,并调用其update()方法。 MockTimeSource实现观察者(Observer)接口,当被通知时,获取当前时间并展现。

推模型与拉模型
上述示例,观察者在接收到消息后,查询被观察者获得数据,这种模型被称为“拉”模型。相应的若是数据是经过update方法传递, 则为“推”模型。

4.9 Abstract Server模式、Adapter模式和Bridge模式

4.9.1 Abstract Server模式

问题

考虑实现一个简单的开关控制器,能够控制灯泡的开关,一种简单的设计以下

https://img2018.cnblogs.com/blog/1436518/201909/1436518-20190904010326033-513233658.jpg

Figure 7: 一种简单灯泡实现

这个设计违反了两个设计原则:依赖倒置原则(DIP)和开放封闭原则(OCP)。对DIP的违反是明显的,Switch依赖了具体类Light。 DIP告诉咱们要优先依赖于抽象类。对OCP的违反虽然不那么明显,可是更加切中要害:在任何须要Switch的地方都要附带上Light, 不能容易地扩展Switch去管理除Light外的其余对象,如当须要控制音乐的开关时(好比在回家后,打开门,同时打开灯光和音乐的开关)。

描述

为了解决这个问题,能够使用一个最简单的设计模式:Abstract Server模式。在Switch和Light之间引入一个接口, 这样就使得Switch可以控制任何实现了这个接口的东西,这当即就知足了DIP和OCP

https://img2018.cnblogs.com/blog/1436518/201909/1436518-20190904010633963-565970885.jpg

Figure 8: AbstractServer模式

谁拥有接口
接口属于它的客户,而不是它的派生类。 客户和接口之间的逻辑绑定关系要强于接口和它的派生类之间的逻辑绑定关系。 它们之间的关系强到在没有Switchable有的状况下就没法使用Switch;可是,在没有Light的状况下却彻底能够使用Switch。 逻辑关系的强度和实体(physical)关系的强度是不一致的。继承是一个比关联强得多的实体关系。
如何打包
在20世纪90年代初期,咱们一般认为实体关系支配着一切,有使多人都建议把继求层次构一块儿放到同一个实体包中。 这彷佛是合理的,由于继承是一种很是强的实体关系。可是在最近10年中,咱们已经认识到继承的实体强度是一个误导, 而且继承层次结构一般也不该该被打包在起。相反,每每是把客户和它们控制的接口打包在一块儿。

4.9.2 Adapter模式

问题
上述Adapter设计可能会违反单一职责原则(SRP):咱们把Lght和Switchable定在一块儿,而它们可能会由于不一样的缘由改变。 另外,若是没法把继承关系加到Light上该怎么办呢,好比从第三方购买了Light而没有源代码。这个时候能够使用Adapter模式。
描述

定义一个适配器,使其继承Switchable接口,并将全部接口的实现委托给实际的Light执行。 事实上,Light对象中甚至不须要有turnOn和turnOff方法。

https://img2018.cnblogs.com/blog/1436518/201909/1436518-20190904010658179-2087863226.jpg

Figure 9: Adapter模式

  1. 使用Adapter模式隐藏杂凑体
    原设计

    请考虑一下下图中的情形:有大量的调制解调器客户程序,它们都使用Modem接口。 Modem接口被几个派生类HayesModem、UsRoboticsModem和ErniesModem实现。这是常见的方案,它很好地遵循了OCP、LSP和DIP。

    https://img2018.cnblogs.com/blog/1436518/201909/1436518-20190904010720491-1784007674.jpg

    Figure 10: 调制解调器问题

    搅乱设计的需求变更
    如今假定客户提出了一个新的需求:有某些种类的调制解调器是不拨号的,它们被称为专用调制解调器, 由于它们位于一条专用链接的两端。有几个新应用程序使用这些专用调制解调器,它们无需拨号,咱们称这些使用者为DedUser。 可是,客户但愿当前全部的调制解调器客户程序均可以使用这些专用调制解调器,他们不但愿去更改许许多多的调制解调器客户应用程序, 因此彻底能够上这些调制解调器客户程序去拨一些假(dummy)电话号码。
    没法使用的理想解决方案

    若是能选择的话,咱们会把系统的设计更改成下图所示的那样。咱们会使用ISP把拨号和通讯功能分离为两个不一样的接口。 原来的调制解调器实现这两个接口,而调制解调器客户程序使用这两个接口。DedUser只使用Modem接口, 而DedicateModem只实现Modem接口。糟糕的是,这样作会要求咱们更改全部的调制解调器客户程序,这是客户不容许的。

    https://img2018.cnblogs.com/blog/1436518/201909/1436518-20190904010752839-1706370214.jpg

    Figure 11: 理想解决方案

    一种简单的解决方案
    一个可能的解决方案是让DedicatedModem从Modem派生而且把dial方法和hangup方法实现为空
    存在的问题
    两个退化函数预示着咱们可能违反了LSP;另外,基类的使用者可能指望dial和hangup会明显地改变调制解调器的状态。 DedicatedModem中的退化实现可能会违背这些指望:假定调制解调器客户程序指望在调用dial方法前调制解调器处于体眠状态, 而且当调用hangup时返回休眠状态。换句话说,它们指望不会从没有拨号的调制解调器中收到任何字符。 DedicatedModem违背了这个指望。在调用dial以前,它就会返回字符,而且在调用hangup调用以后,仍会不断地返回字符。 因此,DedicatedModem可能会破坏某些调制解调器的使用者。
    杂凑体的出现

    咱们能够在DedicatedModem的dial方法和hangup方法中模拟一个链接状态。 若是尚未调用dial,或者已经调用了hangup,就能够拒绝返回字符。 若是这样作的话,那么全部的调制解调器客户程序均可以正常工做而且也没必要更改。只要让DedUser去调用dial和hangup便可。 你可能认为这种作法会令那些正在实现DedUser的人以为很是沮丧,他们明明在使用DedicatedModem。 为何他们还要去调用dial和hangup呢?不过,他们的软件尚未开始编写,因此还比较容易让他们按照咱们的想法去作。

    https://img2018.cnblogs.com/blog/1436518/201909/1436518-20190904010825901-1986992192.jpg

    Figure 12: 使用杂凑体解决问题

    丑陋的杂凑体
    几个月后,已经有了大量的DedUser,此时客户提出了一个新的更改。客户但愿可以拨打任意长度的电话号码, 他们须要去拨打国际电话、信用卡电话、PIN标识电话等等,而原有的电话号码使用char[10]存储电话号码。 显然,全部的调制解调器客户程序都必须更改,客户赞成了对调制解调器客户程序的更改。 糟糕的是,如今咱们必需要去告诉DedUser的编写者,他们必需要更改他们的代码! 你能够想象他们听到这个会有多气愤,他们之因此调用了dial是由于咱们告诉他们必需要这样作,而他们根本不须要dial和hangup方法。
    使用适配器模式隐藏杂凑体

    DedicatedModem不从Modem继承,调制解调器客户程序经过DedicatedModemAdapter间接地使用DedicatedModem。 在这个适配器的dial和hangup的实现中去模拟链接状态,同时把send和receive调用委托给DedicatedModem。 请注意,杂凑体仍然存在,适配器仍然要模拟链接状态。然而,请注意,全部的依赖关系都是从适配器发起的。 杂凑体和系统隔离,藏身于几乎无人知晓的适配器中,只有在某处的某个工厂才可能会实际依赖于这个适配器。

    https://img2018.cnblogs.com/blog/1436518/201909/1436518-20190904010846725-925132476.jpg

    Figure 13: 使用Adapter模式解决问题

4.9.3 Bridge模式

解决调制解调器问题的另外一种思路

看待调制解调器问题,还有另一个方式,对于专用调制解调器的, 须要向Modem类型层次结构中增长了一个新的自由度,咱们可让DialModem和DedicatedModem从Modem派生。 以下图所示,每个叶子节点要么向它所控制的硬件提供拨号行为,要么提供专用行为。 DedicatedHayesModem对象以专用的方式控制着Hayes品牌的调制解调器,而HayesDialModem则以拨号的方式控制着Hayes品牌的调制解调器。

https://img2018.cnblogs.com/blog/1436518/201909/1436518-20190904010904916-482797967.jpg

Figure 14: 扩展层次结构解决问题

存在的问题
这不是一个理想的结构,每当增长一款新硬件时,就必须建立两个新类个针对专用的状况,一个针对拨号的状况。 而每当增长一种新链接类型时,就必须建立三个新类,分别对应三款不一样的硬件。 若是这两个自由度根本就是不稳定的,那么不用多久,就会出现大量的派生类。
Bridge模式的使用

在类型层次结构具备多个自由度的状况中,Bridge模式一般是有用的,咱们能够把这些层次结构分开并经过桥把它们结合到一块儿, 而不是把它们合并起来。

https://img2018.cnblogs.com/blog/1436518/201909/1436518-20190904010927408-1942548907.jpg

Figure 15: 使用Bridge模式解决问题

Bridge模式的优点
这个结构虽然复杂,可是颇有趣,改造为该模式时,不会影响到调制解调器的使用者,而且还彻底分离了链接策略和硬件实现。 ModemConnectController的每一个派生类表明了一个新的链接策略。 在这个策略的实现中能够使用sending、receiveImp、dialImp和hangup,新imp方法的增长不会影响到使用者。 能够使用ISP来给链接控制类增长新的接口。这种作法能够建立出一条迁移路径, 调制解调器的客户程序能够沿着这条路径慢慢地获得一个比dial和hangup层次更高的API。

4.10 Proxy模式和Stairway To Heaven模式

4.10.1 Proxy模式

问题
假设咱们编写一个购物车系统,这样的系统中会有一些关于客户、订单(购物车) 及订单上的商品的对象。 若是向订单中增长新商品条目,并假设这些对象所表明的数据保存在一个关系数据库中的, 那么咱们在添加商品的代码中就不可避免的使用JDBC去操做关系数据模型──客户、订单、商品属于不一样的表, 添加商品到客户在关系数据库中的体现,就是在创建外键联系。这严重违反了SRP,而且还可能违反CCP。 这样把商品条目和订单的概念与关系模式(schema)和SQL的概念混合在了一块儿。不管任何缘由形成其中的一个概念须要更改, 另外一个概念就会受到影响。
Proxy模式

请考虑一下Product类,咱们经过用一个接口来代替它实现了对它的代理,这个接口具备Product类的全部方法。 ProductImplementation类是一个简单的数据对象,同时ProductDbProxy实现了Product中的全部方法, 这些方法从数据库中取出产品,建立一个ProductImplementation实例,而后再把逻辑操做委托给这个实例。

https://img2018.cnblogs.com/blog/1436518/201909/1436518-20190904010349246-1707506398.jpg

Figure 16: Proxy模式

优势
Product的使用者和ProductImplementation都不知道所发生的事情,数据库操做在这二者都不知道的状况下被插入到应用程序中。 这正是 PROXY模式的优势。理论上,它能够在两个协做的对象都不知道的状况下被插入到它们之间。 所以,使用它能够跨越像数据库或者网络这样的障碍,而不会影响到任何一个参与者。

4.10.2 Stairway To Heaven模式

Stairway To Heaven模式

Stairway To Heaven模式是另外一个能够完成和Proxy模式同样的依赖关系倒置的模式。 咱们引入一个知道数据库的抽象类PersistentObject,它提供了read和write两个抽象方法, 同时提供了一组实现方法做为实现read和write所须要的工具。在PersistentProduct的read和write的实现中, 会使用这些工具把Product的全部数据字段从数据库中读出或者写入到数据库。 现使PersistentProduct同时继承Product的PersistentObject类,以下图所示。

https://img2018.cnblogs.com/blog/1436518/201909/1436518-20190904010947753-1171420794.jpg

Figure 17: StairwayToHaven模式

优点
Product的使用者并不须要知道PersistentObject,在须要数据库操做的少许代码中,则能够将类型向下转换(如dynamic cast), 将Product类转换成实际的PersistentObject类,调用其write和read方法。 这样,就能够将有关数据库的知识和应用程序的业务规则彻底分离开来。

4.11 Visitor设计模式系列

问题
在Modem对象的层次结构,基类中具备对于全部调制解调器来讲公共的通用方法,派生类表明着针对许多不一样调制解调器厂商和类型的驱动程序。 假设你有一个需求,要增长一个configureForUnix方法,调制解调器进行配置,使之能够工做于UNX操做系统中。 由于每一个不一样厂商的调制解调器在UNIX中都有本身独特的配置方法和行为特征,这样在每一个调制解调器派生类中,该函数的实现都不相同, 这样咱们将面临一种糟糕的场景,增长configureForUnix方法其实反映了一组问题:对于Windows该怎么办?对于MacOs该怎么办呢? 对于Linux又该怎么办呢?咱们难产要针对每一种新操做系统向Modem层次结构中增长一个新方法吗? 这种作法是丑陋的,咱们将永远没法封闭Modem接口,每当出现一种新操做系统时,咱们就必须更改该接口并从新部署全部的调制解调器软件。
Visitor模式系列
Visitor模式系列容许在不更改现有类层次的状况下向其中增长新方法。该系列中的模式包括:Visitor模式、 Acyclic Visitor模式、Decorator模式、Extension Object模式。

4.11.1 Visitor模式

Visitor模式使用了双重分发技术。之因此被称为双重分发是由于它涉及了两个多态分发,第一个分发是accept函数, 该分发辨别出所调用的accept方法所属对象的类型,第二个分发是viist方法,它辨别出要执行的特定函数。(以下图所示)

https://img2018.cnblogs.com/blog/1436518/201909/1436518-20190904011016334-950097281.jpg

Figure 18: Visitor模式

Visitor模式中的两次分发造成了个功能矩阵,在调制解调器的例子中,矩阵的一条轴是不一样类型的调制解调器, 另外一条轴是不一样类型的操做系统。该矩阵的每一个单元都被一项功能填充,该功能很好的解决把特定的调制解调器初始化为能够在特定的操做系统中使用的问题。

4.11.2 Acyclic Visitor模式

问题
在Visitor模式中,被访问层次结构的基类(Modem)依赖于访问者层次结构的基类(Modem Visitor)。 同时,访问者层次结构的基类中对于被访问层次结构中的每一个派生类都有一个对应函数。 这样, 就有造成了一个依赖环,把全部被访问的派生类(全部的调制解调器)绑定在一块儿,致使很难实现对访问者结构的增量编译, 而且也很难向被访问层次结构中增长新的派生类。
Acyclic Visitor模式

该变体把Visitor基类(modemVisitor)变成退化的,从而解除了依赖环,这个类中不存在任何方法, 使它再也不依赖于被访问层次结构的派生类(以下图所示)。

https://img2018.cnblogs.com/blog/1436518/201909/1436518-20190904011038855-58300359.jpg

Figure 19: AcyclicVisitor模式

对于被访问层次结构的每一个派生类,都有个对应的访问者接口,且访问者派生类派生自这些访问者接口。 这是一个从派生类到接口的180度旋转,被访问派生类中的accept函数把Visitor基类转型(cast)为适当的访问者接口。若是转型成功, 该方法就调用相应的visit函数。

优势
这种作法解除了环赖环,而且更易于增长被访问的派生类以及进行增量编译。
缺点
糟糕的是,它一样也使得解决方案更加复杂了。更糟糕的是,转型花费的时间依赖于被访问层次结构的宽度和深度,因此很难进行测定。 因为转型须要花费大量的执行时间,而且这些时间是不可预测的,因此Acycllic Visitor模式不适用于严格的实时系统。 该模式的复杂性可能一样会使它不适用于其余的系统,可是对于那些被访问的层次结构不稳定,而且增量编译比较重要的系统来讲, 该模式是一个不错的选择。
动态转型带来的稀疏特性
正像Visitor模式建立了一个功能矩阵(一个轴是被访问的类型,另外一个轴是要执行的功能)同样, Acyclic Visitor模式建立了一个稀疏矩阵。访问者类不须要针对每个被访问的派生类都实现visit函数。 例如,若是Ernie调制解调器不能够配置在UNIX中,那么UnixModemConfigurator就不会实现EnineVisitor接口。 所以,Acyclic Visitor模式容许咱们忽略某些派生类和功能的组合。有时,这多是一个有用的优势。

4.11.3 Decorator模式

问题
假设咱们有ー个具备不少使用者的应用程序,每一个使用者均可以坐在他的计算机前,要求系统使用该计算机的调制解调器呼叫另外一台计算机。 有些用户但愿听到拨号声,有些用户则但愿他们的调制解调器保持安静。一种简单的解决方案是在全部的Modem派生类中加入逻辑, 在拨号前询问使用者是否静音;另外一种解决方案,是将Modem接口变为一个类,将通用的逻辑放在基类中,而派生类只实现拨号动做。 前一种方案,须要在每个派生类中加入重复的代码,并须要新派生类的开发者必需要记着复制这段代码。然后一种方案虽然更好, 可是否大声拨号与调制解调器的内在功能没有任何关系,这违反了单一职责原则。
Decorator模式

Decorator模式经过建立一个名为LoudDialModem的全新类来解决这个问题。 LoudDialModem派生自Modem, 而且携有的一个Modem实例,它捕获对dial函数的调用并在委托前拨号动做前把音量设高。

https://img2018.cnblogs.com/blog/1436518/201909/1436518-20190904011054520-1177172395.jpg

Figure 20: Decorator模式

4.11.4 Extension Object模式

还有另一种方法能够在不更改类层次结构的状况下向其中增长功能,那就是使用Extension Object模式。 这个模式虽然比其余的模式复杂一些,可是它也更强大、更灵活一些。

Extension Object模式

层次结构中的每一个对象都持有一个特定扩展对象(Extension Object)的列表。 同时,每一个对象也提供一个经过名字查找扩展对象的方法,扩展对象提供了操做原始层次结构对象的方法。 举个例子,假设有一个材料单系统,咱们想让其中的每一个对象都有将本身数据导出为XML和CVS的能力, 这个需求和调制解调器对不一样操做系统支持的需求相似,此时,除了Acyclic Visitor模式外, 咱们也能够使用Extension Object模式(以下图所示):Part接口定义了添加扩展对象和获取扩展对象的方法, 同时,针对Assembly和PiecePart都实现了相应的XML和CVS导出能力的类,剩下只须要经过类构造方法或使用工厂模式, 将扩展对象装配到相应的数据对象中便可。

https://img2018.cnblogs.com/blog/1436518/201909/1436518-20190904011110975-1289268879.jpg

Figure 21: ExtensionObject模式

4.12 State模式

使用场景
有限状态机(FSM)是软件宝库中最有用的抽象之一, 它们提供了一个简单、优雅的方法去揭示和定义复杂系统的行为。 它们一样也提供了一个易于理解、易于修改的有效实现策略。在系统的各个层面, 从控制髙层逻辑的GUP到最低层的通信协议,都会使用它们。它们几乎适用于任何地方。 实现有限状态机的经常使用方法包括switch-case语句、使用转移表进行驱动以及State模式。
示例场景
现假设去实现一个十字门的状态机,当门“锁住”时,“投币”后可将十字门“解锁”; 在门“锁住”的状态下,若是有人尝试“经过”,十字门将“发出警报”; 在门“解锁”的状态下,人“经过”十字门后,门自动“锁住”; 在门“解锁”的状态下,若是继续“投币”,十字门将“播报感谢语音”。
State模式

以下图所示,Turnstyle类拥有关于事件的公有方法以及关于动做的受保护方法, 它持有一个指向TurnstyleState接口的引用,而TurnstyleState的两个派生类表明FSM的两个状态。 当Turnstyle的两个事件方法中的一个被调用时,它就把这个事件委托给TurnstyleState对象。 其中TurnstyleLockedState实现了LOCKED状态下的相应动做, TurnstyleUnlockedState的方法实现了UNLOCKED状态下的相应动做。 为了改变FSM的状态,就要把这两个派生类之一的实例赋给Turnstyle对象中的引用。

https://img2018.cnblogs.com/blog/1436518/201909/1436518-20190904011130690-2043061699.jpg

Figure 22: State模式

State模式与Strategy模式对比

这两个模式都有一个上下文类,都委托给一个具备几个派生类的多态基类。 不一样之处在于,在State模式中,派生类持有回指向上下文类的引用,全部状态设置方法都在上下文类中实现, 派生类的主要功能是使用这个引用选择并调用上下文类中的方法进行状态转移。 而在Strategy模式中,不存在这样的限制以及意图,Strategy的派生类没必要持有指向上下文类的引用, 而且也不须要去调用上下文类的方法。因此,全部的State模式实例一样也是Strategy模式实例, 可是并不是全部的Strtegy模式实例都是State模式实例。

https://img2018.cnblogs.com/blog/1436518/201909/1436518-20190904011147712-83805541.jpg

Figure 23: State模式与Strategy模式对比

优点
State模式完全地分离了状态机的逻辑和动做,动做是在Context类中实现的, 而逻辑则是分布在State类的派生类中,这就使得两者能够很是容易地独立变化、互不影响。 例如,只要使用State类的另一个派生类, 就能够很是容易地在一个不一样的状态逻辑中重用Context类的动做。 此外,咱们也能够在不影响State派生类逻辑的状况下建立Context子类来更改或者替换动做实现。 该方法的另一个好处就是它很是高效,它基本上和嵌套switch/case实现的效率彻底同样。 所以,该方法既具备表驱动方法的灵活性,又具备嵌套switch/case方法的效率。
缺点
这项技术的代价体如今两个方面。第一,State派生类的编写彻底是一项乏味的工做, 编写一个具备20个状态的状态机会令人精神麻木。第二,逻辑分散, 没法在一个地方就看到整个状态机逻辑,所以,就使得代码难以维护。 这会令人想起嵌套switch/case方法的晦涩性。

5 包的设计原则

5.1 粒度:包的内聚性原则

5.1.1 重用发布等价原则(Release Reuse Equivalency Principle)

定义
重用的粒度就是发布的粒度

RFP指出,一个包的重用粒度能够和发布粒度同样大,咱们所重用的任何东西都必须同时被发布和跟踪。 简单的编写一个类,而后声称它是可重用的作法是不现实的。只有在创建一个跟踪系统,为潜在的使用者提供所须要的变动通知、安全性以及支持后, 重用才有可能。

5.1.2 共同重用原则(Common Reuse Principle)

定义
一个包中的全部类应该是共同重用的。若是重用了包中的一个类,那么就要重用包中的全部类。

类不多会孤立的重用,通常来讲,可重用的类须要与做为该可重用抽象一部分的其余类协做。

CRP规定了这些类应该属于同一个包。在这样的一个包中,咱们会看到类之间有不少的互相依赖。一个简单的例子是容器类以及与它关联的迭代器类, 这些类彼此之间紧密耦合在一块儿,所以必须共同重用,因此它们应该在同一个包中。

所以,我想确信当我依赖于一个包时,我将依赖于那个包中的每个类。换句话说,我想确信我放入一个包中的全部类是不可分开的, 仅仅依赖于其中一部分的状况是不可能的。不然,我将要进行没必要要的从新验证和从新发行,而且会白费至关数量的努力。

5.1.3 共同封闭原则(Common Closure Principle)

定义
包中的全部类对于同一类性质的变化应该是共同封闭的。一个变化若对一个包产生影响,则将对该包中的全部类产生影响,

而对于其余的包不形成任何影响。

这是单一职责原则对于包的从新规定。正如SRP规定的一个类不该该包含多个引发变化的缘由那样,这条原则规定了一个包不该该包含多个引发变化的缘由。

CCP鼓励咱们把可能因为一样的缘由而更改的全部类共同汇集在同一个地方。若是两个类之间有很是紧密的绑定关系,无论是物理上的仍是概念上的, 那么它们老是会一同进行变化,于是它们应该属于同一个包中。这样作会减小软件的发布、从新验证、从新发行的工做量。 CCP经过把对于一些肯定的变化类型开放的类共同组织到同一个包中,从而加强了上述内容。于是,当需求中的一个变化到来时, 那个变化就会颇有可能被限制在最小数量的包中。

5.1.4 总结

过去,咱们对内聚性的认识要远比上面3个原则所蕴含的简单,咱们习惯于认为内聚性不过是指一个模块执行一项而且仅仅一项功能。 然而,这3个关于包内聚性的原则描述了有关内聚性的更加丰富的变化。在选择要共同组织到包中的类时,必需要考虑可重用性与可开发性之间的相副作用力。 在这些做用力和应用的须要之间进行平衡不是一件简单的工做。此外,这个平衡几乎老是动态的。 也就是说,今天看起来合适的划分到了明年也许就再也不合适了。 所以,当项目的重心从可开发性向可重用性转变时,包的组成极可能会变更并随时问而演化。

5.2 稳定性:包的耦合性原则

5.2.1 无环依赖原则

定义
在包的依赖关系图中不容许存在环

若是开发环境中存在有许多开发人员都在更改相同的源代码文件集合的状况,那么就会出现由于他人的更改致使你没法构建的状况。 当项目和开发团队的规模增加时,这种问题就会带来可怕的噩梦,每一个人都忙于一遍遍地更改他们的代码,试图使之可以相容于其余人所作的最近更改。

经过将开发环境划分红可发布的包,能够解决这个问题,这些包能够做为工做单元被一个开发人员或者一个开发团队修改,将一个包能够工做时, 就把它发布给其余开发人员使用。所以,全部的开发团队都不会受到其余开发团队的支配,对一个包做的理性没必要当即反应至其余开发团队中, 每一个开发团队独立决定什么时候采用上前所使用的包的新版本。此外,集成是以小规模增量的方式进行。

这是一个很是简单、合理的过程,并被普遍使用。不过,要使其可以工做,就必需要对包的依赖关系结构进行管理,包的依赖关系结构中不能有环。

5.3 启发:不能自顶向下设计包的结构

这意味着包结构不是设计系统时首先考虑的事情之一。事实上,包结构应该是随着系统增加、变化而逐步演化的。

事实上,包的依赖关系图和描绘应用程序的功能之间几乎没有关系,相反,它们是应用程序可构建性的映射图。 这就是为什么不在项目开始时设计它们的缘由。在项目开始时,没有软件可构建, 所以也无需构建映射图。 可是,随着实现和设计初期累积的类愈来愈多,对依赖关系进行管理,避免项目开发中出现晨后综合症的须要就不断增加。 此外,咱们也想尽量地保持更改的局部化,因此咱们开始关注SRP和CCP,并把可能会一同变化的类放在一块儿

若是在设计任何类以前试图去设计包的依赖关系结构,那么极可能会遭受惨败。咱们对于共同封闭尚未多少了解,也尚未觉察到任何可重用的元素, 从而几乎固然会建立产生依赖环的包。因此,包的依赖关系结构是和系统的逻辑设计一块儿增加和演化的。

5.4 稳定依赖原则(Stable Dependencies Principle)

定义
朝着稳定的方向进行依赖

对于任何包而言,若是指望它是可变的,就不该该让一个难以更改的包依赖于它!不然,可变的包一样也会难以更改。

5.4.1 稳定性

韦伯斯特认为,若是某物“不容易被移动”,就认为它是稳定的。稳定性和更改所须要的工做量有关。 硬币不是稳定的,由于推倒它所需的工做量是很是少的。可是,桌子是很是稳定的,由于推倒它要花费至关大的努力。

5.4.2 稳定性度量

  • (Ca)输入耦合度(Afferent Coupling):指处于该包的外部并依赖于该包内的类的类的数目
  • (Ce)输出耦合度(Efferent Coupling):指处于该包的内部并依赖于该包外的类的类的数目
  • 不稳定性I: \(I = C_e / (C_a + C_e)\)

SDP规定一个包的I度量值应该大于它所依赖的包的I度量值,也就是说,度量值应该顺着依赖的方向减小。

若是一个系统中全部的包都是最大程度稳定的,那么该系统就是不能改变的。这不是所但愿的情形。 事实上,咱们但愿所设计出来的包结构中,一些包是不稳定的而另一些是稳定的。 其中可改变的包位于顶部并依赖于底部稳定的包,把不稳定的包放在图的顶部是一个有用的约定, 由于任何向上的箭头都意味着违反了SDP。

5.5 稳定抽象原则(Stable Abstractions Principle)

定义
包的抽象程度应该和其稳定程度一致

该原则把包的稳定性和抽象性联系起来。它规定,一个稳定的包应该也是抽象的,这样它的稳定性就不会使其没法扩展。 另外一方面,它规定,一个不稳定的包应该是具体的,由于它的不稳定性使得其内部的具体代码易于更改。

SAP和SDP结合在一块儿造成了针对包的DIP原则。这样说是准确的,由于SDP规定依赖应该朝着稳定的方向进行,而SAP则规定稳定性意味着抽象性。 所以,依赖应该朝着抽象的方向进行。然而,DIP是一个处理类的原则。类没有灰度的概念(the shades of grey)。 一个类要么是抽象的,要么不是。SDP和SAP的结合是处理包的,而且容许一个包是部分抽象、部分稳定的。

5.5.1 抽象性度量

  • Nc:包中类的总数N
  • Na:包中抽象类的数目。请记住,一个抽象类是一个至少具备一个纯接口(pure interface)的类,而且它不能被实例化。
  • A是一个测量包抽象程度的度量标准。它的值就是包中抽象类的数目和所有类的数目的比值: \(A = N_a / N_c\)

5.6 主序列

如今,咱们来定义稳定性(I)和抽象性(A)之间的关系。

https://img2018.cnblogs.com/blog/1436518/201909/1436518-20190904011208299-1170595272.jpg

Figure 24: 稳定-抽象坐标-被排除区域

咱们能够建立一个以A为纵轴,I为横轴的坐标图。若是在坐标图中绘制出两种“好”的包类型,会发现那些最稳定、最抽象的包位于左上角(0,1)处。 那些最不稳定、最具体的包位于右下角(1,0)处。 并不是全部的包都会落在这两个位置,包的抽象性和稳定性是有程度的。例如,一个抽象类派生自另外一个抽象类的状况是很常见的。 派生类是具备依赖性的抽象体。所以,虽然它是最大限度抽的,可是它却不是最大程度稳定的,它的依赖性会下降它的稳定性。 由于不能强制全部的包都位于(0,1)或者(1,0),因此必需要假定在A/I图上有一个定义包的合理位置的点的轨迹。 咱们能够经过找出包不该该在的位置(也就是,被排除的区域)来推断该轨迹的含意。

痛苦地带(Zone of Pain)
考虑一个在(0,0)附近的包,这是一个高度稳定且具体的包,咱们不想要这种包,由于它是僵化的:没法对它进行扩展,由于它不是抽象的; 而且因为它的稳定性,也很难对它进行更改。所以,一般,咱们不指望看到设计良好的包位于(0,0)附近。 (0,0)周围的区域被排除在外,咱们称之为痛苦地带。
无用地带(Zone of Uselessness)
考虑一个在(1,1)附近的包,这不是一个好位置,由于该位置处的包具备最大的抽象性却没有依赖者。 这种包是无用的,所以,称这个区域为无用地带
主序列(Main Sequence)
显然,咱们想让可变的包都尽量地远离这两个被排除的区域。 那些距离这两个区域最远的轨迹点组成了链接和(1,0)和(0,1)的线。该线称为主序列。

5.6.1 到主序列的距离

距离D
\(D = |A + I - 1| / \sqrt{2}\)
规范化距离D`
\(D` = | A + I - 1|\)

Date: 2019-09-03 Tus

Author: C.Wong

Created: 2019-09-04 三 01:03

Validate

相关文章
相关标签/搜索