评价代码质量的标准
- 为什么要学习设计模式
- 如何评价代码的好坏
- 什么是设计原则, 常用的设计原则有哪些
- 面向对象, 设计原则, 设计模式, 编程规范, 重构有何关系
- 什么是面向对象编程和面向对象编程语言
- 如何判定一门语言是否是面向对象编程语言
- 面向对象编程和面向对象编程之间的关系
- 什么是面向对象分析和面向对象设计
- 面向对象的四大特性是什么, 它们的作用分别是什么
- 什么是面向过程编程和面向过程编程语言
- 面向对象编程相比面向过程有哪些优势
- 有在开发过程中, 有哪些操作是在进行面向过程编程
- 为什么在面向对象编程时, 容易写出面向过程的代码
- 面向过程编程就真的没有用了吗
- 抽象类和接口有什么区别? 分别是为了解决什么问题
- 如何用抽象类和普通类模拟接口
- 如何决定该用抽象类还是接口
- 如何理解”基于接口而非实现编程”中”接口”二字
- 如何定义一个好的接口
- 是否需要为每个类定义接口
- 为什么不推荐继承
- 什么是基于贫血模型的开发模式
- 什么是基于充血模型的开发模式
- 什么是重放攻击
- 如何面向对象分析和设计
- 如何判断类的职责是否足够单一
- 判断类的职责是否单一的几个技巧
- 类的职责是否设计得越单一越好
- 如何理解”对扩展开放, 修改关闭”
- 如何理解”里氏替换”
- “里氏替换”的目的
- 如何理解”接口隔离原则”
- “接口隔离原则”的三种应用
- 什么是”控制反转”
- 什么是”依赖注入”
- 什么是”依赖注入框架”
- 怎么理解”依赖反转原则”
- 什么是
KISS
原则和YANGI
原则 - 如何提高代码复用性
- 什么是”高内聚”和”松耦合”
- “高内聚”和”松耦合”之间的关系
- 什么是”迪米特法则”
- 如何实践”高内聚”和”松耦合”
- 为什么要重构, 重构的对象是什么, 什么时候进行重构
- 什么是单元测试, 为什么要编写单元测试
- 什么是代码的可测试性, 常见的测试性不友好的代码
- “解耦”为什么非常重要
为什么要学习设计模式
我认为学习设计模式其实就是学习一些专业名词, 当别人谈起什么模式的时候, 你知道它是个什么东西. 设计模式根本就不是高深的东西, 完全是故弄玄虚. 自己写代码的时候, 自然而然就会写出这样的代码.
如何评价代码的好坏
- 代码是否符合代码规范, 例如命名是否规范, 函数是否过长等等
- 代码的逻辑是否清晰, 别人能很轻易的看懂
- 代码写得是不是正确, 考虑的场景是否全面
- 代码的复用性(通用性)怎么样
- 我个人认为还有一点很重要, 可测试性怎么样, 这个对单元测试, 代码重构以及项目的质量非常重要
什么是设计原则, 常用的设计原则有哪些
设计原则是指导我代码设计的一些经验总结.
常用的设计原则:
SOLID
原则: 单一职责原则SOLID
原则: 开闭原则SOLID
原则: 里氏替换原则SOLID
原则: 接口隔离原则SOLID
原则: 依赖装置原则DRY
原则,KISS
原则,YAGNI
原则,LOD
法则
设计模式
设计模式是针对软件开发中经常遇到的一些设计问题, 总结出来的一套解决方案或者设计思路.
编程规范
编程规范主要解决的是代码的可读性问题.
代码重构
在软件开发过程中, 只要软件在不停地迭代, 就没有一劳永逸的设计. 随着需求的变化, 代码的不停堆砌, 原有的设计必定会存在这样那样的问题. 针对这些问题, 我们就需要进行代码重构. 重构是软件开发过程中非常重要的一个环节. 持续重构是保持代码质量不下降的有效手段, 能有效比买呢代码腐化到无可救药的地步.
五者之间的关系
面向对象编程因为具有丰富的特性(封装, 抽象, 继承, 多态), 可以实现很多复杂的设计思路, 是很多设计原则, 设计模式等编码实现的基础.
设计原则是指导我们代码设计的一些经验总结.
设计模式是针对软件开发过程中遇到的一些问题, 总结出来的一套解决方案或者设计思路. 应用设计模式的主要目的是提高代码的可扩展性. 从抽象程度上来讲, 设计原则比设计时更加抽象. 设计模式更加具体,更加可执行.
编程规范主要解决的是代码的可读性问题.
重构作为保持代码质量不下降的有效手段, 利用的就是面向对象, 设计原则, 设计模式, 编程规范这些理论.
理论一: 当讨论面向对象的时候, 我们到底在谈论什么?
什么是面向对象编程和面向对象编程语言?
面向对象编程是一种编程范式或编程风格. 它以类或者对象作为组织代码的基本单元, 并将封装, 继承, 多态三个特性, 作为代码设计和实现的基石.
面向对象编程语言是支持类或者对象的语法机制, 并有现成的语法机制, 能方便地实现面向对象三大特性的编程语言.
如何判定一个编程语言是否是面向对象编程语言
如果严格按照定义, 需要有现成的语法支持类, 对象, 三大特性才能叫作面向对象编程语言. 如果放宽要求的话, 只要某种编程语言支持类, 对象语法机制, 那基本上就可以说这种编程语言是面向对象编程语言了, 并不一定非得要求具有所有的三大特性.
面向对象编程和面向对象编程语言之间有何关系
面向对象编程一般使用面向对象变成语言来进行, 但是, 不用面向对象编程语言, 我们照样可以进行面向对象编程. 反过来说, 即便我们使用面向对象编程语言, 写出来的代码也不一定是面向对象编程风格的, 也有可能是面向过程编程风格的.
什么是面向对象分析和面向对象设计
简单点讲, 面向对象分析就是要搞清楚做做什么, 面向对象设计就是要搞清楚怎么做. 两个阶段最终的产出就是类的设计, 包括程序被拆解为哪些类, 每个类有哪些属性方法, 类与类之间如何交互等等.
封装, 抽象, 继承, 多态分别可以解决哪些编程问题
封装
封装也叫做信息隐藏或者数据访问保护. 类通过暴露有限的访问接口, 授权外部仅能通过类提供的方式(或者叫函数)来访问信息或者数据.
如果我们对类中属性的访问不做限制, 那任何代码都可以访问, 修改类中的属性, 虽然这样看起来很灵活, 但从另一方面来说, 过度灵活也意味着不可控, 属性可以随意被各种奇葩的方式修改.
除此之外, 类仅仅通过有限的方法暴露必要的操作, 也能提供类的易用性. 如果我们把类属性都暴露给类的调用者, 调用者想要正确地操作这些属性, 就势必要对业务细节有足够的了解. 而这对于调用者来说也是一种负担. 相反, 如果我们将属性分装起来, 暴露少许的几个必要的方法给调用者, 调用者就不需要了解太多背后的细节业务, 用错的概率就减少很多.
抽象
封装主要讲的是如何隐藏信息, 保护数据, 而抽象讲的是如何隐藏方法的具体实现, 让调用者只需要关心方法提供了哪些功能, 并不需要知道这些功能是如何实现的.
继承
继承最大的好处就是代码复用. 我们也可以通过其他方式来解决这个代码复用的问题, 比如利用组合.
多态
多态是指, 子类可以替换父类或接口, 在实际运行过程中, 调用子类实现的方法. 除了利用”继承加方法重写”这种实现方式外, 我们还有其他两种比较常见的实现方式, 一个是利用接口类语法, 另一个是利用duck-typing
语法.
duck-typing
是编程语言中动态类型语言中的一种设计风格, 一个对象的特性不是由父类决定的, 而是通过对象的方法决定. go
语言中结构体有方法, 就自动实现了哪些接口, 这种机制就是duck-typing
.
多态特性能提高代码的可扩展性和复用性, 是很多设计原则, 设计模式的基础.
理论三: 面向对象相比面向过程有哪些优势? 面向过程真的过时了吗?
什么是面向过程编程与面向过程编程语言
面向过程编程也是一种编程范式或编程风格. 它以函数作为组织代码的基本单元, 以数据与方法相分离为主要的特点. 面向过程风格是一种流程化的编程风格, 通过拼接一组顺序执行的方法来操作数据完成一项功能.
面向过程编程语言最大的特点时不支持类和对象两个语法概念, 不直接支持面向对象编程的特性(比如封装, 继承, 多态).
面向对象编程相比面向过程编程有哪些优势
- OOP更能够应对大规模复杂程序的开发: 面向对象编程是以类为思考对象. 在面向对象编程的时候, 我们并不是一上来就去思考, 如何将复杂的流程拆解为一个一个方法, 而是先思考如何给业务建模, 如何将需求翻译为类, 如何给类之间建立交互关系, 而完成这些工作完全不需要思考错综复杂的处理流程. 当我们有了类的设计之后, 然后再像搭积木一样, 按照处理流程, 将类组织起来形成整个程序. 这种开发模式, 思考问题的方式, 能让我们在应对复杂程序开发的时候, 思路更加清晰. 除此之外, 免洗那个对象编程还提供了一种更加清晰的, 更加模块化的代码组织思路.
- OOP风格更易复用, 易扩展, 易维护: 继承可以提高复用性, 多态可以让程序更扩展, 同时还能提高复用性, 封装让程序容易维护.
理论四: 哪些代码设计看似时面向对象, 实际是面向过程?
哪些代码设计看似时面向对象, 实际是面向过程?
滥用getter
, setter
方法
面向对象封装的定义是: 通过访问权限控制, 隐藏内部数据, 外部仅能通过类提供的游戏爱你接口访问, 修改内部数据. 所以, 暴露不应该暴露的setter
方法, 明显违反了面向对象的封装特性. 数据没有访问权限控制, 任何代码都可以随意修改它, 代码就退化成面向过程编程风格的了.
滥用全局变量和全局方法
在面向对象编程中, 常见的全局变量有单例类对象, 静态成员, 常量等, 常见的方法有静态方法. 全局变量可以被很多对象访问, 没有办法进行权限访问控制, 而静态方法也没有和数据绑定, 可以操作很多静态变量.
Constants
类和Utils
类是最常见的全局变量和全局方法, 它们其实是几乎是无法避免的. Constants
类其实并不会带来太问题, 做好分类或者把常量定义在要使用的类中. Utils
类是用来防止以写通用的静态方法, 这些方法要操作很多个类中的数据, 但这些类又不好抽象出一个父类, 这时可以把这些方法放在一起, 作为一个Utils
类. 当然, 也可以可以把这些类中的数据抽象出一个父类, 把静态方法写为成员方法也行.
定义数据和方法相分离的类
在编写WEB项目时, 通常会定义定义BO(Business)
, Entity
类, 这些类中通常只会定义数据, 不会定义操作它们的方法, 所有操作这些数据的业务逻辑都定义在对应的Controller
类, Service
类, Repository
类中, 这样数据和方法相分离了. 实际上, 这种开发模式叫做基于贫血模型的开发模式.
在面向对象编程中, 为什么容易写出面向过程风格代码?
因为人做一件事情时, 通常是先干什么, 再干什么, 这就是面向过程的思考方式, 自然而然, 也就很容写出面向过程的代码. 而面向对象编程, 是一种自底向上的思考方式, 先把任务分解成子任务, 在组合子任务, 这种模式适合复杂的事情, 人思考简单的事情时, 不会特意思考去分解任务.
面向过程编程就真的没有用了吗?
在编写简单的功能时, 使用面向过程编程更方便, 也更快捷. 面向对象编程中编写方法时, 其实就是在使用面向过程编程.
理论五: 接口VS
抽象类的区别? 如何用普通的类模拟抽象类和接口
在面向对象编程中, 抽象类和接口是两个经常被用到的语法概念, 是面向对象四大特性, 以及很多设计模式, 设计思想, 设计原则编程思想的基础. 比如, 我们可以使用借口来实现面向对象的抽象特性, 多态特性和基于接口而非实现的设计原则, 使用抽象类来实现面向对象继承特性和模版设计模式等等.
抽象类和接口区别在哪里?
从语法特性上来说, 接口不能拥有成员属性, 所有属性都是静态属性. 在java8之前, 接口不能拥有方法体, 8和8之后支持了静态方法和默认方法, 可以有方法体. 继承要求必须实现抽象方法, 而接口要求实现所有方法. 同时, 一个类只能继承一个类, 但是可以实现多个接口. 继承关系是一种is-a
关系, 接口表示一种has-a
关系, 表示具有某些功能.
抽象类和接口能解决什么编程问题?
抽象类会被继承, 可以用来解决代码复用问题, 相比继承普通类, 可以实现自己不实现方法, 但要求子类必须实现的功能, 这种情况在设计父类时经常遇到.
抽象类更多是为了代码复用, 而接口更强调解耦, 隔离接口和具体实现, 提高代码的扩展性. 接口的实现对调用者透明, 不让调用者知道具体实现, 减少调用者使用方法的难度, 也让实现者更自由. 接口中没有成员属性, 只有方法, 因此定义了实现类能有怎样的行为, 是对行为的一种抽象.
如何模拟抽象类和接口两个语法概念
接口中没有成员变量, 只有方法声明, 没有方法实现, 实现接口的类必须实现接口中的所有方法.. 只要满足这样几点, 从设计的角度上来说, 我们就可以把它称为接口. 也就是抽象类没有成员方法, 所有方法都是抽象方法. 这时从语法特性上来说, 这个抽象类就相当于一个接口.
如果没有抽象类, 如何实现接口呢? 首先, 普通类没有成员属性, 所有的方法直接抛出异常来告诉实现类必须实现这些方法, 最后将构造器设置为protected
, 来防止被包外的类初始化.
如何决定该用抽象类还是接口
实际上, 判断的标准很简单. 如果我们要表示一种is-a
的关系, 并且是为了解决代码复用问题, 我们就用抽象类; 如果我们要表示一种has-a
关系, 并且是为了解决抽象而非代码复用的问题, 那我们就可以使用接口.
从类的继承层次上来看, 抽象类是一种自下而上的设计思路, 先有子类的代码重复, 然后再抽象成上层父类. 而接口正好相反, 它是一种自上而下的设计思路.
我们在编程的时候, 一般都是先设计接口, 再考虑具体的实现, 发现有子类代码重复, 然后抽象成父类或抽象类.
理论六: 为什么基于接口而非实现编程? 有必要为每个类都定义接口吗?
如何解读原则中的”接口”二字
“接口”就是一组”协议”或者”约定”, 是功能提供者提供给使用者的一个功能列表. 从编程语言上来说, 接口可以理解为接口类或者抽象类.
这条原则能非常有效地提高代码质量, 之所以这么说, 那是因为, 应用这条原则, 可以将接口和实现相分离, 封装不稳定的实现, 暴露稳定的接口. 上游系统面向接口而非实现编程, 不依赖不稳定的实现细节, 这样当实现发生变化的时候, 上游系统的代码基本不需要做改动, 以此来降低耦合性, 提高扩展性.
如何设计一个好的接口
在设计接口的时候, 我们要多思考一下, 这样的接口设计是否足够通用, 时是否能做到在替换具体的接口实现的时候, 不需要任何接口定义的改动.
是否需要为每个类定义接口
如果上游系统非常稳定, 实现的代码只可能有一种, 这时就没有必要去设计接口, 因为这时接口的实现只有这一种, 并不会体现接口带来的扩展性. 如果上游系统可以依赖多种实现, 这时才需要设计接口.
理论七: 为何说要多用组合少用继承? 如何决定该用组合还是继承
为什么不推荐使用继承
当一些类需要增加一些特性, 但是另外一些类又不需要增加这些特性时, 如果使用继承, 就需要新增加一个有这些特性的父类. 如果这样的场景很多, 就会有很多这样的父类, 会导致类的继承层次越来深, 继承关系会越来越复杂. 同时, 如果一个类既需要A特性, 也需要B特性, 在不支持多继承的语言中, 会有重复代码.
实战一(上): 业务开发常用的基于贫血模型的MVC架构违背OOP吗?
什么是基于贫血模型的开发模式
将Entity
, BO
, VO
与Repository
, Business
, Controller
相互分离. Entity
, BO
, VO
是纯粹的数据结构, 只包含数据, 不包含任何业务逻辑. 它们的操作都放在Repository
, Business
, Controller
中, 这样的模型就是贫血模型. 这种贫血模型将数据与操作分离, 破坏了面向对象的封装特性, 是一种典型的面向过程的编程风格.
什么是基于充血模型的开发模式
在贫血模型中, 数据和业务逻辑被分割到不同的类中. 充血模型正好相反, 数据和对应的业务逻辑被封装到同一个类中. 因此, 这种充血模型满足面向对象的封装特性, 是典型的面向对象编程风格.
在基于贫血的传统开发模式中, Service
层包含Service
和BO
类两个部分, BO
是贫血模型, 只包含数据, 不包含具体的业务逻辑. 业务逻辑集中在Service
类中. 在基于充血模型的开发模式中, Service
层包含Service
类中Domain
类两部分. Domain
就相当于贫血模型中的BO
. 不过, Domain
与BO
的区别在于它是基于充血模型开发的, 既包含数据, 也包含业务逻辑. 而Service
类变得非常单薄. 总结一下就是, 基于贫血模型的传统的开发模式, 重Service
轻BO
; 基于充血模型的开发模式, 轻Service
重Domain
.
实战一(下): 如何利用充血模型开发一个虚拟钱包系统
将不用依赖别的
Domain
的业务逻辑放在Domain
中.使
Service
类负责与Repository
交流.
实战二(上): 如何对接口鉴权这样一个功能开发做面向对象分析
重放攻击
拿着完全一样的信息, 发送请求. 这个信息可以不从通信过程中获得. 如果使用了https
, 别人是无法获得这些信息的. 重放攻击无法避免, 服务器只能通过信息来判断, 如果信息完全相同, 服务器是无法知道发送方还是不是原来那个. 但是重放攻击可以减轻, 减少信息的有效时间, 可以防止别人拿这个信息一直用.
实战二(下): 如何利用面向对象设计和编程开发接口鉴权功能
面向对象分析的产出是详细的需求描述. 面向对象设计的产出是类.
如何面向对象分析和设计
根据需求描述, 找到其中涉及的功能点, 一个一个罗列出来, 识别其中的动词, 作为候选方法. 然后在把功能点中涉及的名词, 作为候选属性. 然后把操作相同的属性的功能点, 看是否能归位一个类.
理论一: 对于单一职责原则, 如何判定某个类的职责是否足够”单一”?
如何理解单一职责原则
一个类只负责完成一个职责或者功能. 不要设计大而全的类, 要设计粒度小, 功能单一的类. 单一职责原则是为了实现代码高内聚, 低耦合, 提高代码的复用性, 可读性, 可维护性.
如何判断类的职责是否足够单一
不同的应用场景, 不同阶段的需求背景下, 对同一个类的职责是否单一的判定, 可能都是不一样的. 在某种应用场景或者当下的需求背景下, 一个类的设计可以已经满足单一职责原则了, 但如果换个应用场景或者在未来的某个需求背景下, 可能就不满足了, 需要继续拆分成粒度更细的类.
评价一个类的职责是否足够单一, 我们并没有一个非常明确的, 可以量化的标准, 可以说, 这是件非常主观, 仁者见仁智者见智的事情. 实际上, 在真正的软件开发中, 我们也没有必要过于未雨绸缪, 过度设计. 所以, 我们可以先写一个粗粒度的类, 满足业务需求. 随着业务的发展, 如果粗粒度的类越来越庞大, 代码越来越多, 这个时候, 我们就可以将这个粗粒度的类, 拆分成几个粒度更细的类. 这就是所谓的持续重构.
判断类的职责是否足够单一的几个技巧
- 类中的代码行数, 函数或属性过多, 会影响代码的可读性和可维护性, 我们就需要考虑对类进行拆分.
- 类依赖的其他类过多, 不符合高内聚, 低耦合的设计思想, 我们就需要考虑对类进行拆分.
- 比较难给类起一个合适的名字, 很难用一个业务名词概括, 或者只能用一些笼统的
Manager
,Context
之类的词语来命名, 这就说明类的职责定义得可能不够清晰. - 类中大量的方法都是集中操作类中的某几个属性, 那就可以考虑将这几个属性和对应的方法拆出来.
类的职责是否设计得越单一越好
比如说一个类有编码和解码两个方法, 你可以说这是两个功能, 给它分别放入到编码类和解码类中. 但是编码和解码通常是有联系的, 用一种方式编码, 就只能用特定的方式解码, 如果分开就有可能出现编码和解码不匹配的情况. 这个就是不满足高内聚带来的后果.
理论二: 如何做到”对扩展开放, 修改关闭”? 扩展和修改各指什么?
如何理解”对扩展开放, 修改关闭”
具体一点来说就是, 添加一个新的功能应该是, 在已有代码基础上扩展代码(新增模块, 类, 方法等), 而非修改已有代码(修改模块, 类, 方法等). 开闭原则并不是完全杜绝修改, 而是以最小的修改代码的代价来完成新功能的开发. 同时, 同样的代码改动, 在粗代码粒度下, 可能被认定为”修改”; 在细代码粒度下, 可能又被认定为”扩展”.
理论三: 里氏替换跟多态有何区别? 哪些代码违背了里氏替换?
如何理解”里氏替换”
子类对象能替换程序中父类对象出现的任何地方, 并且保证原来程序的正确性不被破坏. 从定义描述和代码实现上看, 多态和里氏替换有点类似, 但它们关注的角度是不一样的. 多态面向对象语言的一种语法, 我们可以使用它来解决问题, 是一种代码实现的思路. 里氏替换是一种设计原则, 是用来指导继承关系中子类该如何设计, 子类的设计要保证在替换父类的时候, 不改变原有程序的逻辑以及破坏原有程序的正确性.
“里氏替换”的目的
里氏替换是为了要求子类遵守父类方法的功能描述.
理论四: 接口隔离原则有哪三种应用? 原则中的”接口”该如何理解?
如何理解”接口隔离原则”?
客户端不应该强迫依赖它不需要的接口. 接口隔离原则可以看作实现单一职责的一种方式, 通过调用者如何使用接口来间接地判定. 如果调用者只使用部分”接口”或”接口”的部分功能, 那”接口”的设计就不够职责单一.
接口隔离原则的三种应用
- 把”接口”认为是模块或类提供的一组
api
. - 把”接口”认为是语言中类实现的接口.
- 把”接口”认为是一个方法声明.
理论五: 控制反转, 依赖反转, 依赖注入, 这三者有何区别和联系?
什么是”控制反转”
框架提供了一个扩展的代码骨架, 用来组装对象, 管理整个执行流程. 程序员利用框架进行开发的时候, 只需要往预留的扩展点上, 添加跟自己业务相关的代码, 就可以利用框架来驱动整个程序流程的执行.
这里的”控制”指的是对程序执行流程的控制, 而”反转”指的是在没有使用框架之前, 程序员自己控制整个程序的执行. 在使用框架之后, 整个程序的执行流程可以通过框架来控制. 流程的控制权从程序员”反转”到了框架.
实际上, 实现控制反转的方法很多, 除了刚才例子中所示的类似于模版设计模式的方法之外, 还有依赖注入等方法. 所以, 控制反转并不是一种具体的实现技巧, 而是一个比较笼统的设计思想, 一般用来指导框架层面的设计.
什么是”依赖注入”
依赖注入和控制反转恰恰相反, 它是一种具体的编码技巧.不通过new()
的方式在类内部创建依赖类的对象, 而是将依赖的类对象在外部创建好之后, 通过构造函数, 函数参数等方式传递(或注入)给类使用.
什么是”依赖注入框架”
在采用依赖注入实现的类中, 虽然我们不需要用类似hard code
的方式, 在类内部通过new
来创建依赖对象, 但是, 这个创建对象, 组装(或注入)对象的工作仅仅是被移动到更上层代码而已, 还是需要我们程序员自己来实现.
在实际的软件开发中, 一些项目可能会涉及几十, 上百, 甚至几百个类, 类对象的创建和依赖注入会非常复杂. 如果这部分工作都是靠程序员自己写代码来完成, 容易出错且开发成本也比较高. 而对象创建和依赖注入的工作, 本身跟具体的业务无关, 我们完全可以抽象成框架来自动完成.
这个框架就是”依赖注入框架”. 我们只要通过依赖注入框架提供的扩展点, 简单配置一下所有需要创建的类对象, 类和类之间的依赖关系, 就可以实现有框架来自动创建对象, 管理对象的生命周期, 依赖注入等原本需要程序员来做的事情.
怎么理解”依赖反转原则”
高层模块不要依赖低层模块. 高层模块和低层模块应该通过抽象来相互依赖. 除此之外, 抽象不要依赖具体实现细节, 具体实现细节依赖抽象.
“依赖注入”和”基于接口而非实现编程”的区别和联系
依赖注入是一种具体的编程技巧, 关注的是对象创建和类之间的关系, 提高了代码的扩展性, 我们可以灵活地替换依赖的类.
基于接口而非实现编程是一种设计原则, 关注抽象和实现, 上下游调用的稳定性, 目的是降低耦合性, 提高扩展性.
它们都是基于开闭原则思路, 提高代码扩展性.
理论六: 我为何说KISS
, YANGI
原则看似简单, 却经常被用错?
什么是KISS
和YANGI
原则
KISS
原则做事情尽量保持简单, YANGI
原则你不需要它, 遇到现在不需要做的事情就不要做, 留下扩展点就行.
理论七: 重复的代码就一定违背DRY
吗? 如何提高代码的复用性?
DRY
不用重复你自己. 重复的代码通常违背DRY
原则, 可以先用一个, 如何以后有特殊处理, 在抽出来就行.
如何提高代码复用性
- 减少代码耦合
- 满足单一职责原则
- 业务代码和业务代码逻辑分离
- 继承, 多态, 抽象, 封装
- 应用模版等设计模式
理论八: 如何利用迪米特法则实现”高内聚, 松耦合”?
什么是”高内聚, 松耦合”
所谓”高内聚”, 就是指相近的功能应该放到同一个类中, 不相近的功能不要放到同一个类中. 相近的功能放在一起, 容易管理和维护. 单一职责原则就是实现”高内聚”非常有效的手段.
所谓”松耦合”, 在代码中, 类与类之间的依赖关系简单清晰, 同时, 即使两个类之间有依赖关系, 一个类的改动不会或者很少导致依赖类的代码改动. 依赖注入, 接口隔离, 基于接口而非实现编程, 以及迪米特法则, 都是为了实现代码的松耦合.
“高内聚”和”松耦合”之间的关系
“高内聚”是用来指导类本身的设计, “松耦合”是用来指导类于类之间依赖的关系, 这两者并非完全独立不相干. 高内聚有助于松耦合, 松耦合又需要高内聚的支持.
什么是”迪米特法则”
每个模块只应该了解那些与它关系密切的模块的有限知识. 具体一点说, 不该有直接依赖关系的类之间, 不要有依赖; 有依赖关系的类之间, 尽量只依赖必要的接口.
如何实践”高内聚”和”松耦合”
- 当两个类之间不应该有直接联系时, 使用一个中间类来联系两个类
- 当一个类要求”高耦合”, 但别的类又要求”松耦合”时, 可以通过实现接口来实现
理论一: 什么情况下要重构? 到底重构些什么? 又该如何重构?
重构的目的: 为什么要重构?
重构是一种对软件内部结构的改善, 目的是在不改变软件的可见行为的情况下, 使其更易理解, 修改成本更低.
重构是为了保证代码的质量. 防止代码质量差导致出现很多问题, 同时重构还可以提升代码质量, 让最初代码的质量变得越来越好.
重构的对象: 到底重构什么?
根据重构的规模, 我们可以笼统地分为大规模高层次重构(“大型重构”)和小规模低层次的重构(“小型重构”).
大型重构指的是对顶层代码设计的重构, 包括: 系统, 模块, 代码结构, 类与类之间的关系等的重构, 重构的手段有: 分层, 模块化, 抽象可复用组件等等. 这类重构的工具就是设计思想, 原则和模式.
小型重构指的是对代码细节的重构, 主要是针对类, 函数, 变量等代码级别的重构, 比如规范命名, 规范注释, 消除超大类和函数, 提取重复代码等等. 小型重构更多利用的是编程规范.
重构的时机: 什么时候重构?
提倡的重构策略是持续重构. 平时没有事情的时候, 可以看看项目中有哪些写得不够好的, 可以优化的代码, 主动去重构一下. 或者, 在修改, 添加某个功能的时候, 也可以顺手把不符合代码规范, 不好的设计重构一下. 总之, 就像把单元测试, 代码检视作为开发的一部分, 如果能把持续重构作为开发的一部分, 成为一种开发习惯, 对项目, 对自己都会很有好处.
理论二: 为了保证重构不出错, 有哪些非常能落地的技术手段?
如何保证重构不出错, 熟悉掌握各种设计思想, 原则, 模式, 还需要对所重构的业务和代码有足够的了解. 除了这些个人能力因素之外, 最可落地执行, 最有效的保证重构不出错的手段是单元测试. 当重构完成之后, 如果新的代码仍能通过单元测试, 那就说明代码原有逻辑的正确性未被破坏, 原有的外部可见行为未变. 当然, 这需要之前的单元测试存在且有效. 还有一个手段是代码检视.
什么是单元测试?
单元测试有研发工程师自己编写, 用来测试自己写的代码的正确性. 我们通常将它跟集成测试放到一块来对比. 单元测试相对集成测试来说, 测试的粒度更小一些. 集成测试的对象是整个系统或者某个功能模块, 比如测试用户注册, 登录功能是否正常, 是一种端到端的测试. 而单元测试的测试对象是类或者函数, 用来测试一个类和函数是否都按照预期的逻辑执行. 这是代码层级的测试.
为什么要写单元测试?
- 单元测试相当于一次”自我代码检视”, 能有效地帮你发现代码中的
bug
和代码设计上的不合理 - 单元测试可以通过
桩
很轻松写出某些集成测试不好测试的场景, 是对集成测试的有力补充 - 单元测试是重构的基础, 没有单元测试, 重构无法保证正确性
理论三: 什么是代码的可测试性? 如何写出可测试性好的代码?
什么是代码的可测试性
粗略地讲, 所谓代码的可测试性, 就是针对代码编写单元测试的难易程度. 对于一段代码, 如果很难为其编写单元测试, 或者单元测试写起来很费劲, 需要依靠单元测试框架中很高级的特性, 那往往就意味着代码设计得不够合理, 代码的可测试性不好.
编写可测试性代码的最有效的手段是依赖注入, 通过依赖注入, 在写单元测试时, 可以通过mock
的方法解决外部依赖.
常见的测试性不友好的代码
- 代码中包含未决逻辑行为
- 滥用可变全局变量
- 滥用静态方法
- 使用复杂的继承关系
- 高度耦合的代码
理论四: 如何通过封装, 抽象, 模块化, 中间层等解耦代码?
“解耦”为什么非常重要?
不管阅读代码还是修改代码, “高内聚, 松耦合”的特性能让我们聚焦在某一模块或类中, 不需要了解太多其他模块或类的代码, 让我们的焦点不至于过于发散, 降低阅读和修改代码的难度. 而且, 因为依赖关系简单, 耦合小, 修改代码不至于牵一发而动全身, 代码改动比较集中, 引入bug
的风险也就减少了很多. 同时, “高内聚, 松耦合”的代码可测试性也更加好, 容易mock
或者很少需要mock
外部依赖的模块或者类.