设计健壮的代码

翻译 · yiqing · 于 5年前 发布 · 4217 次阅读

翻译自强哥:witing-solid-code

设计健壮的代码(SOLID 设计原则在企业api框架中的显现)

Capital One, 我们开发了一个企业API框架(eAPI)来简化符合企业标准的api应用程序的开发。 此框架提供了一套企业级特征,比如消息管理,日志审计,企业安全,等。它也提供了一些用来快速开始开发api应用程序的骨架代码的启动包。 在此篇中,我会解释 由eAPI生成的 兼顾代码质量,可维护性,及可复用性而又拥有跨团队的一致性交付特征的骨架代码 如何遵从被称之为 SOLID的原理。

SOLID是个助记首字缩写 ,其代表了由 Robert C. Martin提出用于开发更简洁和更具维护性代码的五个 面向对象设计(OOD)原理:

  • 单一职责原理 (Single Responsibility Principle --- SRP)
  • 开闭原理 (Open/Closed Principle --- OCP)
  • 李氏替换原理 (Liskov Substitution Principle --- LSP)
  • 接口分离原理 (Interface Segregation Principle --- ISP)
  • 依赖反转原理 (Dependency Inversion Principle --- DIP)

单一职责原理(SRP)

SPR指出 类应该只有一个单一的责任 。 它即是UNIX原理 :"做一件事并把它做好" 。 假设一个类拥有多个职责,改变其中的一个职责可能影响该类满足其他职责的能力,进而导致该类以无法预料的方式被中断。

让我们来看下 由eAPI框架生成的 RESTful api应用骨架如何满足SRP 。下面的类图展示了主要的类/接口 和其在骨架程序中的关系。

如你所见,为了实现 用来暴露银行数据的RESTful APIs ,三个类/接口被创建 ,每个都假定有不同的职责。BankThingResource 类 负责解析接入的HTTP 请求,调用被请求的服务类,并返回响应 。BankThingService接口 负责提供 支持以不同方式跟BankThing数据交互 的服务。BankThingsDao 接口负责持久化BankTing 数据到持久化存储中 。

如果我们合并BankThingResource 和 BankThingService 功能到一个类中将发生什么事?这将创建一个在两个维度上对需求变更敏感的类 (比如,添加一个新的RESTful API 端点;修改数据查询的方法)。每种改变都迫使我们重建,重新测试,并重新部署依赖的应用程序。 即使变更跟应用程序无关(译注:应用程序只依赖BankThingService 并不关心底层查询方式)。

SRP是一个非常宽泛的原理,实际上,剩余的SOLID原理可或多或少的认为是衍生自SRP 。

开闭原理(OCP)

OCP 指出软件实体(类,模块,方法,等构建体) 应该允许在不修改原有代码(closed)的基础上自定义或者增强(open)。而OCP可能从 其字面不易理解,但它在实战中非常常见。

让我们用eAPI 骨架代码来解释这个原理 。BankThingServiceImpl 类使用实现BankThingsDao接口的HashMapBankThingsDao类。 BankThingsDao接口的意义何在 ,为什么不让BankThingServiceImpl 直接使用 HashMapBankThingsDao ? 这都因为OCP原理。接口的抽象 允许BankThingServiceImpl实现类 对BankThing对象使用不同的存储,而不修改BankThingServiceImpl的代码。
假设我们在BankThingServiceImpl直接引用【译者:依赖】HashMapBankThingsDao ,只要我们想使用不同的DAO类,我们将不得不修 改BankThingServiceImpl 。

除了使用接口,我们也可以使用 抽象基类 来满足OCP 。关键在于在引用中使用抽象来避免直接耦合于具体实现。

李氏替换原理(LSP)

LSP 指出子类必须是其基类的可替换品。比如,一个函数有个类型为 T 的参数,那么函数应该在 当为其提供一个T的子类型 S的参数时仍旧正常工作。一个经常违反LSP的原因是类间的不当继承 ,著名的 circle-ellipse problem(园与椭圆问题)就 阐明了它。另一个违反LSP原理的原因跟 类型检测代码有关 ,检测会在子类没被覆盖时失败(译注:类型检测只考虑了是当前类 ,没有 考虑是子类的情形) 。

接口分离原理(ISP)

ISP指出 多个客户端特定的接口 比一个通用目的的接口要好。此原理可理解为专门针对接口的SRP 。如果一个接口的方法可被分解到不同 的组,每个服务于不同类型的客户,我们称接口为“胖”接口 并违反了ISP 。根据SRP,它意味着相同的接口有多种职责。为了解决这种问题 ,“胖”接口应该替换为多个客户端特定的接口。

重新审视在eAPI骨架代码中的两个接口,我们可以得出他们是相互隔离的结论 。BankThingService 接口指定了 像BankThingResource 这样的主要关心以多种方式跟BankThing数据交互的客户端 使用的方法。而BankThingsDao接口包含 客户端想持久化数据到存储空间 的方法。

依赖倒置原理(DIP)

DIP 包含两句话,第一句指出:高级别的模块不应该依赖低级别的模块 ----- 两个都应该依赖抽象 。 第二句指出 抽象不应该依赖细节 ------ 细节应该依赖抽象 。

良构的面向对象(OO)系统 经常由分属于清晰定义的层 中的模块构成 。高层的模块 经常是策略制定者,它来决定系统应该长什么样子, 而低级别的层实现特定的策略。因此,让高层依赖底层模块是无意义的。如OCP所指,让底层模块直接依赖具体的高层模块也是有问题的。 应该创建抽象来解耦底层和高层模块。由于第一句话,这样的抽象应该属于高层模块。

现在让我们来检查eAPI骨架代码是否符合DIP 。在eAPI代码中 有两个层 。web层是上层 由BankThingResource构成 , service层是下层 由我们刚才提及的其他剩余类和接口构成 。如果我们要在两个层间画一个依赖线 ,我们会看到 高层依赖底层。比如,如果我们在 底层改变BankThingService 接口 ,上层如果不做相应改变 那么我们会断裂上层。另一方面,代码确实引入了接口并让细节依赖了抽象 而不是其他周围的方法 。因此 ,可以得出结论: eAPI骨架代码部分违反DIP原理 。

这是一个问题么?我不这样认为 。如果服务被很多客户端使用,保证接口在服务层是有意义的。或者更好的,把接口放在独立的包并让 服务和web层依赖此包。

总结

SOLID 是设计优秀OO代码的普适准线。有时会跟其他相似原理冲突,比如DRY(Don't Repeat Yourself -- 不要重复你自己 ), KISS(Keep It Simple Stupid -- 保证简单并傻瓜化)。也可能跟实际需求,编程语言等 相背 。 此时,违反SOLID可能比相较于其他 方面 获得的益处要好些。

如果你只是了解下 SOLID 可能看起来很令令人怯步的。如果你不能记住或者理解所有的这些原理,完全没关系 。只要记住 在你 的代码中避免紧耦合就行了 ,因为这就是SOLID的全部内容。在编程时保证这个,你将会立即开始写出更好的代码。


Posted Sep 10, 2015 by...

薛强

Qiang Xue Lead Software Engineer, Technology Fellows

本帖已被设为精华帖!
共收到 4 条回复 设计模式 设计原理 企业架构
forecho#15年前 0 个赞

这些原理有点难搞懂。

bashenjiyi#25年前 0 个赞

不好看懂

Rambone#35年前 0 个赞

SOLID SRP单一职责原理 OCP开闭原理 LSP里氏替换原理 ISP接口分离原理 DIP依赖倒置原理

gelove#45年前 0 个赞

可以写一些相应的代码看看吗

添加回复 (需要登录)
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册