软件设计~设计模式

为什么软件需要设计?—— 庖丁解牛

软件的核心特质在于变化,随着互联网的发展,这个特质更加突出。在互联网行业有一句名言“拥抱变化”,这些大多十天半月就需发版一次,提供一堆新特性太提高软件产品的竞争力;游戏公司则更夸张,周版本是常态,甚至于使用hotfix[4]这样的技术热更新软件,变化于无形之中……

变化就意味着软件的各个功能模块乃至于整个架构都会不停调整,这就导致随着软件的发展,调整的代价将会越来越大,乃至于摧毁软件的生命力……抽象一点说,就是软件的复杂度越来越高,需要考虑的问题越来越多,相互耦合在一起难以维护。

为了解决上述的问题,软件设计成了一个成功软件项目的重中之重,笔者认为一个好的软件必须像人类一样两条腿走路,否则就会摔跟头。软件的两条腿,一条是数据结构和算法,另一条是优秀的软件架构。

  • 数据结构和算法能够帮你用最有效率的方式解决抽象问题。例如:使用二分法查找有序数组比顺序查找快很多……
  • 软件架构能够帮助软件实现可测试、易扩展、松耦合、强健壮的特性,进而持续快速得演进系统……

数据结构和算法属于工程师的基本素养,这里就不多谈了。本文将主要针对软件设计谈谈笔者的看法,笔者认为一个好的软件设计者需要具备以下三种能力:

  1. 抽象能力。能够快速准确获取问题的抽象模型;具备问题垂直分层和水平分模块的能力;选定特定的逻辑切入点,提供扩展能力。
  2. 架构能力。能够对特定的功能点,能够在性能、可靠性和可维护性做trade off,选型合适的技术方案。
  3. 开发能力。遵循代码规范、妥善组织代码、开发单元测试、参加code review。

怎么去设计?—— 站在巨人的肩膀上

Design is the art of arranging code that needs to work today, and to be easy to change forever. – Sandi Metz

笔者认为软件设计问题,实际是解决复杂度问题。复杂的软件往往是由不同功能的模块相互协同提供服务的,大量功能揉杂在一起,使得大型软件难以维护和扩展。

在软件设计中,有一句圣经一般的话“Any problem in computer science can be solved by another layer of indirection.”。意即为,可以通过构建软件中间层,隔离不同逻辑层次的逻辑,进而解决软件设计问题。

分层有两种表现形式,一种是水平拆分,一种是垂直拆分。

  • 垂直拆分:通常都是面向逻辑流程进行拆分,典型案例就是TCP/IP[12]协议。
  • 水平拆分:典型的拆分方式有两种,面向功能拆分和面向服务拆分。
    • 面向服务拆分:典型案例就是Linux应用层服务拆分,Linux服务器可以提供诸如HTTP、DNS、FTP等不同的服务。
    • 面向功能拆分:典型案例就是HTTP服务的接口功能拆分,HTTP服务可以提供POST、GET、DEL等资源控制功能。

关于软件的设计,业界早已沉淀了大量的理念、方法和工具,其中《设计模式——可复用面向对象软件设计基础》[1]一书就是其中的佼佼者。

笔者整理了业界通用的一些设计原则,设计模式和业务抽象模型,在下面一一列出,其中会涉及到一些小的demo,笔者将使用Golang[2]进行实现。

设计理念——内功心法

总纲:开闭原则[11]。尊重存量代码,通过扩展代码来提供新增功能。

心法:合成复用原则[13]。组合优先于继承,降低类之间的耦合。

五个周天,最小化耦合粒度,提高代码复用性:

  1. 迪米特法则[10]:依赖方对被依赖方的了解要最小化,依赖接口>依赖函数>依赖结构体方法>依赖结构体属性。

  2. 依赖倒置原则[8]。高层模块依赖于子功能的抽象,不依赖于低层的实现细节。底层通过功能的抽象暴露高层所需行为。

  3. 接口隔离原则[9]:调用接口方依赖的接口粒度要最小,接口不提供调用方不使用的任何行为。

  4. 里氏替换原则[7]:派生类(子类)对象可以在程序中代替其基类(超类)对象。任意接口的实现都可以替换此依赖此接口其他的实现。

  5. 单一职责原则[6]:每个函数、类、接口都最好只负责一项职责。

最后,打通任督二脉。通过单元测试、继承测试,保证扩展后的软件的正确性。通过Table-Driver-Test[14]完成两种类型的测试:1. 测试case确定执行特定行为,2. 测试case确定返回特定结果。一个可测试的功能需要:

  1. 不依赖于全局变量,功能依赖的数据均依赖于参数传递。
  2. 不依赖于框架层的通信协议,通过mock或者依赖于特定的通信接口。
  3. 不依赖于随机变量。

综上所述,应该以最小的粒度切分接口,功能实现过程中要尽量依赖于接口,尽量组合不同接口提供功能。

设计模式——武功招数

每一个模式描述了一个在我们周围不断重复发生的问题以及该问题的解决方案的核心。—— Christopher Alexander[3]

设计模式则是通过大量上述的经验总结,抽象出的通用工具。统一的代码风格、提高代码复用性,就是设计模式能给项目带来的东西。

设计模式涉及了下面一些概念:

  • 对象,是一组数据和与其相关的操作的集合,是代码执行阶段的逻辑内存实体。
  • signature(型构),特定函数签名的对象方法。
  • interface(接口),对象所定义的所有操作型构的集合。
  • 对象的类型(type):类型只与接口相关,即对象提供的可以操作数据结构的方法集合。
  • 对象的类(class),数据结构与操作数据结构的接口集合。
  • 类继承和接口继承(子类型化):
    • 类继承:根据一个对象的实现定义另一个对象的实现。
      • 抽象类,所有操作延迟到子类实现。也就是纯接口。抽象类技术。
      • 混入类,和抽象类一样无法实例化,但是存在功能实现。MixIn技术。
    • 接口继承:一个对象什么时候可以用来替代另一个对象。

按照功能对设计模式的分类:

  • 创建型:如何创建对象?
    • 1.简单工厂模式(Simple Factory)。
    • 2.工厂方法模式(Factory Method)。
    • 3.抽象工厂模式(Abstract Factory)。
    • 4.创建者模式(Builder)。
    • 5.原型模式(Prototype)。
    • 6.单例模式(Singleton)。
  • 结构型:处理类和对象的组合。
    • 7.外观模式(Facade)。
    • 8.适配器模式(Adapter)。
    • 9.代理模式(Proxy)。
    • 10.装饰模式(Decorator)。
    • 11.桥模式(Bridge)。
    • 12.组合模式(Composite)。
    • 13.享元模式(Flyweight)。
  • 行为型:对类或对象怎么交互和怎么分配职责进行描述。
    • 14.模板方法模式(Template Method)。
    • 15.观察者模式(Observer)。
    • 16.状态模式(State)。
    • 17.策略模式(Strategy)。
    • 18.职责链模式(Chain of Responsibility)。
    • 19.命令模式(Command)。
    • 20.访问者模式(Visitor)。
    • 21.调停者模式(Mediator)。
    • 22.备忘录模式(Memento)。
    • 23.迭代器模式(Iterator)。
    • 24.解释器模式(Interpreter)。

如前文所述,设计模式是在特定的业务框架下,如何抽象某个功能点以实现更好的复用。集合上面提到的概念,也即在固定的业务框架下,如何选取实现特定接口的对象,从而实现代码的高可复用。多数语言根据接口族而不是面向实现来进行连接的能力,被称为多态。多态是代码复用的关键点。

面向接口编程最关键点在于,子类仅仅添加或者重定义操作,没有隐藏父类的操作。所有子类都能相应接口的请求,从而子类的类型都是抽象类的子类型。

多态分为如下几种形式:

  1. 类继承和对象组合是两种最常用的代码复用技术。
  • 类继承:白箱复用(white-box reuse),白箱指的是,子类对于父类实现的内部细节是可见的。
  • 组合复用:黑箱复用(black-box reuse),对内部细节不可见。
  1. 委托(delegation)方式。使组合具有与继承同样的复用能力。相当于子类型将数据交给组合对象使用。
type Receivor interface{
    DoIt()
}

type ReceivorDelegation struct{
    receivor Receivor
}

func (d * ReceivorDelegation) DoIt(){
    d.DoIt()
}
  1. 继承和参数化类型(parameterized type)的比较。
  • 类属(generic),允许定义一个类型时并不指定该类型所用到的其他所有类型。

总结一句:通过多态可以实现代码的复用,功能的有效扩展。

设计架构——融会贯通

参考文献

[1] 设计模式——可复用,面向对象软件的基础:https://item.jd.com/12623588.html

[2] Golang:https://golang.org/

[3] Christopher Alexander:https://en.wikipedia.org/wiki/Christopher_Alexander

[4] Hotfix:https://en.wikipedia.org/wiki/Hotfix

[5] 设计模式:https://zh.wikipedia.org/wiki/设计模式_(计算机)

[6] 单一职责原则(Single Responsibility Priceple):https://zh.wikipedia.org/wiki/单一功能原则

[7] 里氏替换原则(Liskov Substitution Principle):https://zh.wikipedia.org/wiki/里氏替换原则

[8] 依赖倒置原则(Dependency Inversion Principle):https://zh.wikipedia.org/zh-hans/依赖反转原则

[9] 接口隔离原则(Interface Segregation Principle):https://zh.wikipedia.org/wiki/接口隔离原则

[10] 迪米特法则(Law of Demeter):https://baike.baidu.com/item/迪米特法则/2107000

[11] 开放封闭原则(Open Close Principle):https://zh.wikipedia.org/wiki/开闭原则

[12] TCP/IP:https://zh.wikipedia.org/zh-hans/TCP/IP协议族

[13] 合成复用原则(Composite Reuse Principle,CRP):http://c.biancheng.net/view/1333.html

[14] Table-Driver-Test:https://www.codementor.io/@cyantarek15/how-table-driven-tests-makes-writing-unit-tests-exciting-and-fun-in-go-15g1wzdf7g

posted @ 2022-04-30 15:24  code_wk  阅读(98)  评论(0)    收藏  举报