软件设计,那些你不知道的事

代码质量和产出是衡量一个程序员是否优秀最直接的标准。如何提高代码质量和产出?这就要从软件重构和review入手。市面上有很多关于重构和review的书籍,但是看完之后,代码能力并不能立竿见影显著提升,只能帮助我们解决表面的bug和规范点,无法帮助我们发现更深层次的设计问题。

从设计角度来考虑review,识别代码坏味道可以有效减少技术债务。技术债务是指有意或无意的做出错误的或非最优的设计决策所引发的债务。债务越积越多,最后只能重新彻底重构项目才能解决问题,这也叫做技术破产。如何解决技术债务问题,就要从根源上明确引起技术债务的重要的原因——设计坏味和重构认识不足。

首先要明确软件设计原则

  • 抽象原则:通过精简和概括来简化实体:精简指的是删除不必要的细节,概括是找出并定义重要的通用特征。

  • 非循环依赖原则:包之间的关系不可形成循环。

  • 不自我重复原则:在详细设计中,设计实体和代码和重复可能表现为类型名重复和实现重复。

  • 封装原则:通过隐藏抽象的实现细节和隐藏变化等方法实现关注点分离和信息隐藏。

  • 信息隐藏原则:找出棘手或可能变化的设计决策,并创建合适的模块或类型来对其他模块或类型隐藏这些决策。

  • 保持简单原则:简洁是软件系统设计的重要目标,应避免引入不必要的复杂性。

  • 里氏替换原则:所有的子类型都必须至少提供超类型承诺的行为且对每个超类型的引用都可替换成子类型实例。

  • 层次接口原则:使用分类、概括、替换、排序等方法以层次方式组织对抽象。

  • 模块化原则:通过集中和分解等手法创建高内聚、低耦合的抽象。

  • 开闭原则:类型应对扩展开放,对修改关闭。具体是模块应该能够在不修改代码情况下支持新需求。

  • 单一职责原则:绝不应有多个导致类需要修改的原因,如修改一个成员可能影响类的其他不相关职责,导致类难以维护。

  • 变化封装原则:倡导一种信息隐藏方式,建议将可能发生变化的概念封装起来。很多设计模式都体现了这种设计原则,如策略模式、桥梁模式、观察者模式。

我们从设计的角度来看代码时,要遵循六要素:

了解完设计原则和六要素后,我们再来看设计坏味。

本文中每种坏味我们只选其中一例做具体说明。

抽象型坏味

抽象原则倡导通过精简和概括来简化实体:精简指的是删除不必要的细节,而概括指的是找出并定义通过的重要特征。交通标志是用于交流的抽象示例,而数字符号和编程语言是用于解决问题的抽象示例。

缺失抽象

使用一系列数据或者编码字符串,而不创建类或者接口时会产生这种坏味

  1. 概念
  • 应用抽象原则的一种实现手法是创建概念边界清晰,身份唯一的实体。由于没有创建抽象来表示实体,而是使用基本数据类型或编码字符串等原始数据来表示它,这违反了抽象原则,将这种坏味称为缺失抽象(Missing Abstraction)。不必要的抽象也违反了模块化原则。
  1. 潜在原因
  • 未做重复的设计分析
  • 未重构
  • 错误的将重点放在细微的性能改善上
  1. 示例
  • 在JDK1.0中方法printStackTrace()以字符串的方式将栈跟踪打印到标准错误流。
  • 在需要以编程方式访问栈跟踪元素的客户程序中,必须要编程代码来获取数据,如行号等,由于客户程度依赖这种字符串格式,JDK设计人员只能在后续版本中兼容这种格式了。

重构建议:从Jdk1.4起对JAVA的API进行了改进,StackTraceElement类就是原来设计中缺失的对象。

  1. 别名
  • 基本类型偏执:使用基本类型对日期、金额进行编码,而不创建类时,将引发这种坏味。
  • 数据泥团:在很多地方同事使用一系列数据项,而不创建类时,将引发这种坏味。
  1. 现实考虑
  • 避免过度设计:有时候,实体只是数据元素,没有任何相关联的行为。这种情况下使用类或者接口来表示它们可能导致过度设计。

封装型坏味

封装原则倡导通过隐藏抽象的实现细节和隐藏变化等手法实现关注的分离和信息隐藏。比如开车必须知道发动机原理吗?

不充分的封装

对于抽象的一个或多个成员,声明的访问权限超过了实际需求时,将导致这种坏味。例如,将字段声明为公有的类就存在「不充分封装」坏味。

  1. 概念
  • 封装的原则是将接口和实现分离,以便能独立修改。这种关注点分离,让客户程序只依赖抽象的接口,而对它们隐藏具体实现。修改实现不影响客户程序。对抽象的内部隐藏的不充分称为不充分的封装(Deficient Encapsulation)。
  1. 潜在原因
  • 为方便测试
  • 在面向对象中采用过程思维
  • 快速交付
  1. 示例
  • 来看看java.lang.System,in、out、err都被声明成final,但可以通过java.lang.System的setIn、setOut、setErr分别赋值。任何代码都能很方便的使用它们,比如System.out.println();
  • PrintStream是java 1.0就有的,只支持8位的ASCII值,Java1.1出的PrintWriter支持Unicode,然而就是因为应用程序都能直接使用PrintStream来访问PrintStream的方法,根本不能摒弃PrintStream类。

  1. 重构建议:Java 1.6引入了java.io.Console类,他提供了用于访问基于字符的控制台的方法。reader()、writer()来获取Console相关的Writer和Reader对象。

  2. 别名

  • 可隐藏的公有属性、方法
  • 未封装的类
  • 包含未参数化方法的类
  1. 现实考虑
  • 嵌套或匿名类中过于宽松的访问性
  • 性能考虑:比如前面说的java.lang.System

模块化坏味

模块化原则倡导利用集中和分解等手法创建高内聚、低耦合的抽象。

拆散的模块化

应集中放在一个抽象中的数据和方法分散在多个抽象中时,将导致这种坏味。表现为类被用作数据容器没有任何方法、类的方法更多的被其他类的成员调用。

  1. 概念
    一种重要的模块化实现手法是「将相关的数据和方法集中在一起」。如果抽象中只包含数据成员,而操作这些数据成员的方法位于其他抽象中,它就违反了这种实现手法,存在「拆散的模块化」坏味。称为拆散的模块化(Broken Modularization)。

  2. 潜在原因

  • 以过程思维使用面向对象语言
  • 不熟悉既有设计
  1. 重构建议
    对于包含大量数据类的过程型设计,可采用重构手法“将过程型设计转换为对象”。

  2. 别名

  • 被动地存储数据的类
  • 数据类
  • 数据记录
  • 记录类
  • 数据容器
  • 错位的操作
  • 依恋情结
  • 错位的控制
  1. 现实考虑
  • 自动生成的代码
  • 数据传输对象
  • 层次型坏味
  • 层次结构原则倡导采用分类、归并、替换和排序等手法以层次方式组织抽象。比如地球上的870万种生物。

缺失的层次结构

代码片段使用条件逻辑来显式管理行为变化,而原本可以创建一个层次目录,并使用它来封装这些变化,会产生这种坏味

  1. 概念
  • 基于类型码的switch语句(或串接的if-else语句)是最著名的设计坏味之一。
  • 使用类型码来处理行为变化表明没有进行有意义的分类,导致设计中缺少相应的层次结构。称为缺失层次结构(Missing Hierarchy)。
  1. 潜在原因
  • 错误的采用过于简单的设计
  • 过程型设计思维
  • 忽视了继承也是一种设计手法
  1. 示例
  • 串接的if else语句显示的检查类型AbstractButton,JToolBar和JTextCompont并在各种条件下调用方法getMargin(),这种造成的情况是将来可能在代码中的其他地方也会出现。

  1. 重构建议
  • 如果条件检查中的多个实现调用方法相同,可引入相关的接口来抽象共同的协议。
  • 如果代码中包含可转换为类的条件语句,可采用重构手法“提取层次结构”来创建一个类层次结构,其中每个类都表示条件检查中的一种情形。
  1. 别名
  • 标记类
  • 继承缺位
  • 紧缩的类型层次结构
  • 内嵌功能
  1. 现实考虑
  • 与外部交互

——————————————————分割线——————————————————

我是小微,专注微服务技术分享,致力挖掘更多“高、精、全”的微服务知识分享给大家。

我的微信:weiweiweiblack (备注:博客园 )

微信公号:黑少微服务,“分享技术,热爱生活”,欢迎关注