Edison's Studio.

敏捷软件开发:设计

Word count: 2kReading time: 6 min
2021/05/09
loading

本篇为阅读《敏捷软件开发:原则,模式与实践》后的读后感。主要阅读的是第二部分:敏捷设计。
这本书的第一部分,关于敏捷开发中的测试驱动开发与重构,可以阅读其他专门的书籍。我阅读的是《重构,改善既有代码设计》。
本书第三部分的案例,可自由选择是否阅读以加深理解。我自己更多的是结合自己遇到的问题,从书中描述找到问题的解决方案。

什么是敏捷设计

软件越来越难维护

软件在第一版本发布的时候,是软件设计最干净清晰的时候。随着复杂的需求与软件持续迭代,软件代码往往会在原来的设计上做各种hack以满足新的需求。

这系列的破坏设计的操作,会在后续的需求增加的过程中逐渐变得难以维护,而后的需求迭代速率将越来越慢。

使用敏捷设计

所以我们推荐使用TDD + 重构的形式,来持续调整代码设计,每一次做微小的重构,尽管对单次迭代耗时长,但是把一个大的重构的时间分散在每次迭代中,将有利于代码持续维护。

此外,在后续的内容中,将使用经典的软件设计原则,来从一开始就让软件设计趋于敏捷,或者说,让软件设计能更好的拥抱变化

尽可能保持代码干净

由于在实际开发项目中,文档是软件开发的附属物,代码才是软件开发的本身。如果代码设计的好,将有效的加快开发过程,同时方便新任开发者快速接手,阅读,理解与进一步设计。

软件设计原则

软件设计原则篇章,重点讲述了经典的六大设计原则中的其中五项SOLID

单一职责原则

就一个类而言,应该只有一个引起他变化的原因

将一个类中复杂的耦合分离开来,是该原则的实践。这要求,每一次做类设计,都要避免将一堆复杂的逻辑放在一个类中,否则这个类就失去了单一职责的原则。

而在具体情况中,对于几乎不会发生变化的模块,我们不需要为了解耦而解耦,而是将他们放在一个类即可。

单一职责原则,简单也复杂。难点在于,我们很难在一开始就知道,哪些职责(引起变化的原因 )是需要解耦的。所以,我们仍然需要通过持续重构来不断优化类设计,使其符合单一职责原则。

开发-封闭原则

对扩展开放,对更改封闭

对扩展开放,是指,对一个类新增能力是允许的,而且可以非常容易做到的。对更改封闭,是指,对一个类做能力新增的时候,只需要添加代码,而不需要对旧的代码做修改。

开放-封闭原则是一种非常理想的原则。我们很难在实际业务中,完全不修改旧代码的逻辑,而新增新的代码,除非代码架构从一开始就是与业务不耦合的架构。我们新增业务逻辑的时候,是完全有可能修改旧的逻辑使其具备支持新的业务逻辑的能力。甚至,修改旧的业务逻辑本身就是这次新增的一部分。而我们在编写原来的业务逻辑的时候,并不一定能考虑到其不能支持新增的需求。

所以,事实上,要实现该原则,要做的就是,重构。我们需要先将旧的代码逻辑重构成符合开放-封闭原则的代码,并跑通测试。然后,在对更改封闭的前提下编写类扩展代码。

书中提到预测,我认为,预测是一种潜意识的行为,当对业务熟悉,对团队熟悉,对设计方法熟悉,自然会在设计过程中做出更准群的预测。可以说,有办法做到预测,且确实准确预测,则执行该原则会变得简单。而即使没有预测,通过重构,我们依然可以解决问题。

里氏替换

子类型必须能够替换掉基类型

里氏替换的应用场景是这样子的:假设我们定一个一个base类和一个sub类,其中,base类定义了一个method,sub类复写了该method。同时我们定义了一个function用于接受base类的输入,并执行base类的method。这个时候,我们要求子类可以作为function的输入,即是说,子类复写的method必须是与base类的method在输入和输出上是一致的,仅内部计算逻辑不一致。

当我们在sub类复写了method,造成sub类不能替换base类的时候,我们可能会在function对输入做处理,让其可以接受base类和sub类。我们更可能为sub类专门写一个测试验证其能通过。其实这两者都违反了前述的开放-封闭原则。即我们对function的修改没有对sub类封闭。

另外需要说的是:

  • 里氏原则是面向行为的。一个行为限制了某种输入,才有原则的应用场景。
  • 基于契约设计。显然,我们预先约定了base类和sub类符合里氏原则,才有办法开发接受base类的符合原则的function
  • 继承本身是复杂的设计,golang甚至舍弃了继承,但我们在这里不谈关于继承的问题。当我们在使用继承的时候,要时刻注意是否符合里氏替换原则。

接口隔离原则

不应该让client依赖于使用不到的方法

一个client需要来源于其他类的某一个特殊的功能,这个时候,如果使用继承,则会将另一个类的所有接口都携带进来,这个时候对client本身是不利的。

解决方案:

  • 可以使用适配器(Adapter)来实现隔离,不过这会造成性能问题
  • 使用多重继承

依赖倒置原则

高层模块不应该依赖于低层模块,两者应该都依赖于抽象
抽象不应该依赖于细节,细节应该依赖于抽象

一个经典的例子是,我们在使用框架进行web服务开发的时候,通常只需要编写处理request与返回response的业务逻辑,而request与response的具体执行,都由框架直接执行。从分层的角度,框架是底层,我们实现的web业务是高层。显然,web服务没有调用底层的http处理,而是底层的处理调用了高层的web业务。

高层模块通常都会依赖底层模块。依赖倒置的目的是为了应对底层模块经常变动的情况。基于开发-封闭原则,如果底层模块变动造成了依赖他的高层模块需要调整,则应该拒绝高层对底层的依赖,而是让两者依赖于不变动的抽象。

至于细节依赖抽象,则比较好理解。通常,细节是比较容易因为需求增加或调整而发生变更的,抽象则不然。所以细节需要依赖抽象,而抽象不应该依赖细节,即不应该因为细节变更而造成抽象的变更。

总结

敏捷设计的基础是,在测试驱动开发的前提下,持续重构。同时使用软件设计基本原则,保障代码不至于陷入“坏味道”而难以重构。

CATALOG
  1. 1. 什么是敏捷设计
    1. 1.1. 软件越来越难维护
    2. 1.2. 使用敏捷设计
    3. 1.3. 尽可能保持代码干净
  2. 2. 软件设计原则
    1. 2.1. 单一职责原则
    2. 2.2. 开发-封闭原则
    3. 2.3. 里氏替换
    4. 2.4. 接口隔离原则
    5. 2.5. 依赖倒置原则
  3. 3. 总结