代码可读性是衡量代码质量的重要标准,可读性也是可维护性、可扩展性的保证,因为代码是连接程序员和机器的中间桥梁,要对双边友好。
随着项目在不断演进过程中,代码不停地在堆砌,如果没有人为代码的质量负责,代码总是会往越来越混乱的方向演进。当混乱到一定程度之后,量变引起质变,项目的维护成本已经高过重新开发一套新代码的成本,想要再去重构,已经没有人能做到了。
工作中常见的坏代码(“坏味道”):
-
代码重复
实现逻辑相同、执行流程相同 -
方法过长
方法中的语句不在同一个抽象层级
逻辑难以理解,需要大量的注释
面向过程编程而非面向对象(如逻辑扁平化、面条化、过多if-else) -
过大的类
类做了太多的事情
包含过多的实例变量和方法
类的命名不足以描述所做的事情 -
逻辑分散
发散式变化:某个类经常因为不同的原因在不同的方向上发生变化
散弹式修改:发生某种变化时,需要在多个类中做修改 -
严重的情结依恋
某个类的方法过多的使用其他类的成员 -
数据泥团/基本类型偏执
两个类、方法签名中包含相同的字段或参数
应该使用业务类来代替基本类型,比如表示数值与币种的Money类、起始值与结束值的Range类 -
不合理的继承体系
继承打破了封装性,子类依赖其父类中特定功能的实现细节
子类必须跟着其父类的更新而演变,除非父类是专门为了扩展而设计,并且有很好的文档说明 -
过多的条件判断
-
过长的参数列
-
临时变量过多
-
令人迷惑的暂时字段
某个实例变量仅为某种特定情况而设置
将实例变量与相应的方法提取到新的类中
- 纯数据类
仅包含字段和访问(读写)这些字段的方法
此类被称为数据容器,应保持最小可变性
-
不恰当的命名
命名无法准确描述做的事情
命名不符合约定俗称的惯例 -
过多的注释或者过时的注释
坏代码的问题
-
难以复用
系统关联性过多,导致很难分离可重用部分 -
难于变化
一处变化导致其他很多部分的修改,不利于系统稳定 -
难于理解
命名杂乱,结构混乱,难于阅读和理解 -
难以测试
分支、依赖较多,难以覆盖全面
坏代码的缘由
- 编码之前缺乏有效的设计
- 成本上的考虑,在原功能堆砌式编程
- 缺乏有效代码质量监督机制
什么是好代码
代码质量的评价有很强的主观性,描述代码质量的词汇也有很多,比如可读性、可维护性、灵活、优雅、简洁。这些词汇是从不同的维度去评价代码质量的。其中,可维护性、可读性、可扩展性又是提到最多的、最重要的三个评价标准。
要写出高质量代码,我们就需要掌握一些更加细化、更加能落地的编程方法论,这就包含面向对象设计的原则、设计模式、编码规范、重构技巧等。
面向对象设计的原则(SOLID)
单一职责原则(Single Responsibility Principle, SRP):一个类应该有且只有一个引起它变化的原因。简单地说:接口职责应该单一,不要承担过多的职责。工作中我们需要迭代调整代码以应对需求变更,如果职责越多,职责间存在耦合越大,意味着可能潜在的变化点越多,变化的概率和风险越大,后续实现改变的需求难度越大。因此,如果在设计过程中发现一个类承担的职责太多,最直接有效的解决方式就是按职责 "拆分"。
开放封闭原则(Open Closed Principle,OCP):一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭。允许通过扩展的方式增加系统行为,禁止通过直接修改源码的方式增加系统行为。简单地说:就是当别人要修改软件功能的时候,他不能修改我们原有代码,只能新增代码实现软件功能修改的目的。例如代码中存在if-else语句,且随着功能拓展不得不追加,这是拓展性糟糕的表象,应该做好功能抽象、逻辑解耦、分层设计,这里推荐使用简单工厂+策略模式。
里氏替换原则(Liskov Substitution Principle,LSP):所有引用基类的地方必须能透明地使用其子类的对象。简单地说:所有父类能出现的地方,子类就可以出现,并且替换了也不会出现任何错误。这就要求子类的所有相同方法,都必须遵循父类的约定,否则当父类替换为子类时就会出错。这里父类的约定,不仅仅指的是语法层面上的约定,还包括实现上的约定。
接口分离原则(Interface Segregation Principle,ISP):类间的依赖关系应该建立在最小的接口上。简单地说:接口的内容一定要尽可能地小,能有多小就多小。这样做也是为了更好地隔离变化,降低内部改动导致的风险。在软件设计中,建议不要将一个大而全的接口扔给使用者,而是将每个使用者关注的接口进行隔离,分别提供不同的接口服务。
依赖倒置原则(Dependency Inversion Principle,DIP):高层模块不应该依赖底层模块,两者都应该依赖其抽象。抽象不应该依赖细节,即接口或抽象类不依赖于实现类,细节应该依赖抽象,即实现类不应该依赖于接口或抽象类。简单地说,就是说我们应该面向接口编程,通过抽象成接口,使各个子类的实现彼此独立,实现类之间的松耦合,而此时接口时应该具备通用的业务特性,能够满足对外开放使用。
迪米特法则:一个对象应该对其他对象保持最少的了解,以降低代码间的耦合。
合成复用原则:尽量使用合成/聚合的方式,而不是使用继承。
总而言之:
依赖倒置原则告诉我们要面向接口编程;
当面向接口编程之后,接口隔离原则和单一职责原则又告诉我们设计接口的时候要精简单一,要注意职责的划分;
当我们职责划分清楚后,里氏替换原则告诉我们在使用继承时,要注意遵守父类的约定;
依赖倒置、接口隔离、单一职责、里氏替换的最终目标都是为了实现开闭原则;
开闭原则是总纲,告诉我们要对扩展开放,对修改关闭。
设计模式
设计模式:软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案。设计模式可以分为三大类:创建型、结构型、行为型。
- 创建型
创建型模式,就是创建对象的模式,抽象了实例化的过程。它帮助一个系统独立于如何创建、组合和表示它的那些对象。关注的是对象的创建,创建型模式将创建对象的过程进行了抽象,也可以理解为将创建对象的过程进行了封装,作为客户程序仅仅需要去使用对象,而不再关心创建对象过程中的逻辑。包括:
(常用)
工厂模式(Factory Pattern)
抽象工厂模式(Abstract Factory Pattern)
单例模式(Singleton Pattern)
建造者模式(Builder Pattern)
(不常用)
原型模式(Prototype Pattern)
- 结构型
结构型模式是为解决怎样组装现有的类,设计他们的交互方式,从而达到实现一定的功能的目的。结构型模式包容了对很多问题的解决。例如:扩展性(外观、组成、代理、装饰)、封装性(适配器,桥接)。包括:
(常用)
代理模式(Proxy Pattern)
适配器模式(Adapter Pattern)
桥接模式(Bridge Pattern)
装饰器模式(Decorator Pattern)
过滤器模式(Filter、Criteria Pattern)
(不常用)
组合模式(Composite Pattern)
外观模式(Facade Pattern)
享元模式(Flyweight Pattern)
- 行为型
主要解决的是类或对象之间的交互行为的耦合,是为了让代码逻辑更灵活,有利于实现高聚合、低耦合。包括:
(常用)
责任链模式(Chain of Responsibility Pattern)
策略模式(Strategy Pattern)
模板模式(Template Pattern)
迭代器模式(Iterator Pattern)
状态模式(State Pattern)
观察者模式(Observer Pattern)
(不常用)
命令模式(Command Pattern)
解释器模式(Interpreter Pattern)
中介者模式(Mediator Pattern)
备忘录模式(Memento Pattern)
空对象模式(Null Object Pattern)
访问者模式(Visitor Pattern)
各个模式之间的关系图(参见《设计模式》第8页):
设计模式差异汇总:
1、单例模式和工厂模式:
在实际开发中,一般会把工厂类写成单例模式;
2、策略模式和工厂模式:
1)策略模式属于行为模式,工厂模式属于创建型模式;
2)工厂模式在于封装对象的创建,策略模式在于接收工厂创建的对象,从而实现不同的行为。
3、策略模式和委派模式:
1)策略模式是委派模式的一种内部实现形式,策略模式关注的结果是能否相互贴换。
2)委派模式不是GOF23种设计模式,更多关注分发,调度的过程。
4、策略模式和模板模式:
1)策略模式和模板模式都是行为模式;
2)策略模式与模板模式都有封装算法,策略模式重点是不同的算法之间可以相互贴换,模板模式重点是定义一套流程。
3)策略模式可以改变算法流程,可以替代代码中的if...else...分支;模板模式不能改变算法的流程。
5、装饰器模式和静态代理:
1)装饰器模式强调给对象动态添加方法,而代理更注重控制对 对象的访问。
2)代理模式和装饰器模式都持有对方的引用,但逻辑处理重心不一样。
6、装饰器模式和适配器模式:
1)装饰者模式和适配器模式都是属于包装器模式;
2)装饰者模式可以实现被装饰者与相同的接口或者继承被装饰者作为它的子类,而适配器和被适配者可以实现不同的接口。