要想写出好代码,我们首先要知道,什么样的代码是好代码,代码的批判标准有哪些?
1、可维护性(maintainability):易维护代码:在不破坏原有代码设计、不引入新的 bug 的情况下,能够快速地修改或者添加代码;代码不易维护:修改或者添加代码需要冒着极大的引入新 bug 的风险,并且需要花费很长的时间才能完成。
2、可读性(readability):软件设计大师 Martin Fowler 曾经说过:“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.”翻译成中文就是:“任何傻瓜都会编写计算机能理解的代码。好的程序员能够编写人能够理解的代码。”
可读性的标准:是否符合编码规范、命名是否达意、注释是否详尽、函数是否长短合适、模块划分是否清晰、是否符合高内聚低耦合等等。
3、可扩展性(extensibility):表示我们的代码应对未来需求变化的能力;代码的可扩展性表示,我们在不修改或少量修改原有代码的情况下,通过扩展的方式添加新的功能代码。
4、简洁性(simplicity):尽量保持代码简单;思从深而行从简。
5、可复用性(reusability):尽量减少重复代码的编写,复用已有的代码。
6、测试可性(testability):代码可测试性的好坏,能从侧面上非常准确地反应代码质量的好坏,如果代码的可测试性差,比较难写单元测试,那基本上就能说明代码设计得有问题。
然后怎么就是怎么去写出好代码了,今天我们就从运用设计原则,来如何去编写好代码,那么下面我们就来聊聊都有哪些设计原理。
定义:一个类或者模块只负责完成一个职责(或者功能)。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。
好处:使用单一原则指导代码编写,就好比我们的代码就是积木,我们的代码越单一化,就相当于我们的积木是最小的一个正方形,那么在去使用的时候,哪里都可以拿来复用,代码块越复制,就相当于一个不可拆分的大积木,只能出现在特定的位置,复用性差。
我们的代码怎么才算单一呢?可以有以下几点进行指导:
类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分。
类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分。
私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性。
比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰。
类中大量的方法都是集中操作类中的某几个属性。
定义:软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”;如:添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
好处:使用开闭原则指导代码编写,可以减少bug的引入,毕竟是在源代码上改动最小的情况下,去引入新功能,可以很好的因为不熟悉老代码,而对以前的代码就行错误的修改。并且许多设计模式的产生就是满足开闭原则,如策略模式、责任链模式等。
开闭原则并不是指完全不修改代码,怎么修改代码才算满足开闭原则呢?
- 只要它没有破坏原有的代码的正常运行,没有破坏原有的单元测试,我们就可以说,这是一个合格的代码改动。
尽量让修改操作更集中、更少、更上层,尽量让最核心、最复杂的那部分逻辑代码满足开闭原则。
私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性。
定义:子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。
好处:里氏替换原则,主要就是指导我们,子类中的方法,如何去重写父类中的方法,也就是说我们子类去重写父类方法时,子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。这里的行为约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。
什么情况下违反了里氏替换原则呢?
子类违背父类声明要实现的功能:例如:父类中提供的 sortOrdersByAmount() 订单排序函数,是按照金额从小到大来给订单排序的,而子类重写这个 sortOrdersByAmount() 订单排序函数之后,是按照创建日期来给订单排序的。
子类违背父类对输入、输出、异常的约定:例如:在父类中,某个函数约定:运行出错的时候返回 null;获取数据为空的时候返回空集合(empty collection)。而子类重载函数之后,实现变了,运行出错返回异常(exception),获取不到数据返回 null。
子类违背父类注释中所罗列的任何特殊说明:例如:父类中定义的 withdraw() 提现函数的注释是这么写的:“用户的提现金额不得超过账户余额……”,而子类重写 withdraw() 函数之后,针对 VIP 账号实现了透支提现的功能,也就是提现金额可以大于账户余额。
定义:客户端不应该被强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者或者使用者。
好处:使用此原则,可以指导我们的接口设计的更加单一合理,其中对于接口的理解可以是单一的一个接口类,也可以是对外部的一组接口的调用等,接口隔离原则有点类似于我们的单一指责。
接口隔离原则和单一职责的区别?
单一职责原则针对的是模块、类、接口的设计。接口隔离原则相对于单一职责原则,一方面更侧重于接口的设计,另一方面它的思考角度也是不同的。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。
定义:高层模块不要依赖低层模块。高层模块和低层模块应该通过抽象来互相依赖。除此之外,抽象不要依赖具体实现细节,具体实现细节依赖抽象。所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。
主要用在框架层面的设计指导思想,最典型的就是spring的IOC容器。平时开发中,很少用来指导开发,这里就不多说。
定义:尽量保持简单;代码足够简单,也就意味着很容易读懂,bug 比较难隐藏。即便出现 bug,修复起来也比较简单。
好处:就像前面说的那样思从深而行从简,什么是好代码,让别人看的简单明了的代码就是好代码,别人一看就能看出你的代码是用来干什么的就是好代码,所以我们在写代码的时候,一定是写的越简单越好(感觉和单一职责也有一定的联系,所以说万物都是相同的)。
如何保持我们的代码简单呢?
不要使用同事可能不懂的技术来实现代码。
不要重复造轮子,要善于使用已经有的工具类库。
不要过度优化。不要过度使用一些奇技淫巧(比如,位运算代替算术运算、复杂的条件语句代替 if-else、使用一些过于底层的函数等)来优化代码,牺牲代码的可读性。
定义:文全称是:You Ain’t Gonna Need It。直译就是:你不会需要它,也就是不要去设计当前用不到的功能;不要去编写当前用不到的代码。实际上,这条原则的核心思想就是:不要做过度设计。
好处:也就是说,当前项目中,没有用到的东西,我们不要提前去编写他,等到真正用到的时候再去写他就好了,也就是在我们写代码的时候,不要去想一些我们可能根本就不会用到东西,去浪费我们的脑细胞了,毕竟脑细胞有限,想多了容易秃头。当然,这并不是说我们就不需要考虑代码的扩展性。我们还是要预留好扩展点,等到需要的时候就可以快速编写代码。
定义:不要写重复的代码。
好处:怎么保证我们的代码不重复呢,就要求我们的代码复用性好,怎么保证代码复用性好呢,我们的方法足够单一(又回到单一原则,说明很重要)。有时候,在写代码的过程中,出现重复代码后,我要就要想想是不是可以抽出一个单独的方法,进行通用,方法多了,是不是就要想想,是不是可以把方法抽出一个通用的类呢?
什么样的代码才算重复代码呢?
实现逻辑重复:尽管代码的实现逻辑是相同的,但语义不同,我们判定它并不违反 DRY 原则。对于包含重复代码的问题,我们可以通过抽象成更细粒度函数的方式来解决。
能语义重复:在项目中,统一一种实现思路。相同的功能,不要出现多种实现。
代码执行重复:调用链路中,出现同一个逻辑被多次验证。(如判空逻辑)