一、面向对象编程
1.什么是面向对象编程
- 面向对象编程是一种编程范式或编程风格。它以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计的基石。
- 面向对象编程语言是支持类或对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程四大特性的编程语言。
2.封装、继承、抽象、多态
封装
封装也叫做信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类提供的方式来访问内部信息或者数据。
抽象
隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的。
继承
继承是用来表示类之间的is-a关系。
### 多态
多态是指子类可以替换父类。
3.接口和抽象类的区别
抽象类
- 抽象类不允许被实例化,只能被继承。
- 抽象类可以包含属性和方法。方法即可以包含代码实现,也可以不包含代码实现。不包含代码实现的方法叫做抽象方法。
- 子类继承抽象类,必须实现抽象类中的所有抽象方法。
接口
- 接口不能包含属性(成员变量)
-
接口只能声明方法,方法不能包含代码实现。(jdk8的新特性,接口可以有默认实现)
- 类实现接口的时候,必须实现接口中声明的所有方法。
抽象类和接口能够解决什么编程问题
抽象类
抽象类不能被实例化,只能被继承。继承能解决代码复用的问题。所以,抽象类也是为代码复用而生的。多个子类可以继承抽象类中的属性和方法,避免在子类中,重复编写相同的代码。
接口
接口的作用在于解耦。接口是对行为的一种抽象,相当于一组协议或者契约。调用者只需要关注抽象的接口,不需要了解具体的实现。接口实现了约定和实现相分离,可以降低代码的耦合性,提高代码的可扩展性。
如何决定该用抽象类还是接口
如果我们要表示一种is-a的关系,并且是为了解决代码的复用的问题,我们就用抽象类;如果我们要表示一种has-a的关系,并且是为了解决抽象而非代码复用的问题,那我们就可以使用接口。
从类的继承层次上来看,抽象类是一种自下而上的设计思路,先有子类的代码重复,然后再抽象承上层的关系(抽象类)。而接口正好相反,它是一种自上而下的设计思路。我们编程的时候,一般都是先设计接口,再去考虑具体的实现。
4.基于接口而非实现编程
基于接口而非实现编程,这条原则能非常有效地提高代码质量,应用这条原则,可以将接口和实现相分离,封装不稳定的代码实现,暴露稳定的接口。上游系统面向接口而非实现编程,不依赖不稳定的实现细节,这样当实现发生变化的时候,上游系统的代码基本上不需要做改动,以此来降低耦合性,提高扩展性。
基于接口而非实现编程原则的另一个表述方式,是基于抽象而非实现编程。后者的表述方式其实更能体现这条原则的设计初衷。在软件开发中,最大的挑战之一就是需求的不断变化,这也是考验代码设计好坏的一个标准。越抽象、越顶层、越脱离具体某已实现的设计,越能提高代码的灵活性,越能对应未来的需求变化。好的代码设计不仅能应对当下的需求,而且在将来需求发生变化的时候,仍然能够在不破坏原有代码设计的情况下灵活应对。
1.函数的命名不能暴露任何实现细节。
2.封装具体的实现细节。
3.为实现类定义抽象的接口。
5.多用组合少用继承
继承层次过深、继承关系过于复杂会影响到代码的可读性和可维护性。
如果类之间的继承结构稳定,继承层次比较浅,继承关系不复杂,推荐是用继承。
反之,系统越不稳定,继承层次很深,继承关系复杂,尽量使用组合来替代继承。
二、设计原则
SOLID原则
1.单一职责原则
一个类或者模块只负责完成一个职责。
不要设计大而全的类,要设计粒度小、功能单一的类。一个类包含了两个或者两个以上业务不想干的功能,我们就说它职责不够单一,应该将它拆分成多个功能更加单一,粒度更细的类。
我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类。这就是所谓的持续重构。
- 类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就考虑对类进行拆分;
- 类依赖的其他类过多,或者依赖类的其他类过多,不符合高能聚、低耦合的设计思想,我们就考虑将类进行拆分。
- 私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为public方法,供更多的类使用,从而提高代码的复用性。
- 比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用到一些笼统的Manger、Context之类的词语来命名,这说明类的职责定义的可能不够清晰。
- 类中大量的方法都是集中操作类中的某几个属性,那么就可以考虑将这几个属性和对应的方法拆分出来。
2.开闭原则
对扩展开放、对修改关闭
添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码的方式来实现。
开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。
我们要时刻具备扩展意识、抽象意识、封装意识。在写代码的时候,我们要多花点时间思考一下,这段代码未来可能有哪些需求变更,如何设计代码结构,在不改动代码整体结构、做到最小代码改动的情况下,将新的代码灵活的插入到扩展点上。
3.里氏替换原则
子类对象能够替换程序中父类对象出现的任何地方,并且保证原来的逻辑行为不变及正确性不被破坏。
里氏替换原则是用来指导,继承关系中子类该如何设计的一个原则。理解里氏替换原则,最核心的就是理解“按照协议来设计”这几个字。父类定义了函数的约定,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的约定。这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。
4.接口隔离原则
理解“接口隔离原则”的重点是理解其中的“接口”二字。这里有三种不同的理解。如果把“接口”理解为一组接口集合,可以是某个微服务的接口,也可以是某个类库的接口等。如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口。
如果把“接口”理解为单个 API 接口或函数,部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数。
如果把“接口”理解为 OOP 中的接口,也可以理解为面向对象编程语言中的接口语法。那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。
5.依赖反转原则
-
控制反转
控制反转是一个比较笼统的设计思想,一般用来指导框架层面的设计。这里所说的“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制程序的执行。在使用框架之后,整个程序的执行流程通过框架来控制。流程的控制权有程序员反转给了框架。
-
依赖注入
依赖注入是一种具体的编程技巧。我们不通过new的方式来类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类来使用。
-
依赖注入框架
我们通过依赖注入框架提供的扩展点,简单配置一下所有需要的类及其类与类之间依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。
-
依赖反转原则
依赖反转原则也叫作依赖倒置原则。这条原则跟控制反转有点类似,主要用来指导框架层面的设计。高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不要依赖具体实现细节,具体实现细节依赖抽象。
KISS原则
Keep It Simple and Stupid.
KISS 原则是保持代码可读和可维护的重要手段。KISS 原则中的“简单”并不是以代码行数来考量的。代码行数越少并不代表代码越简单,我们还要考虑逻辑复杂度、实现难度、代码的可读性等。而且,本身就复杂的问题,用复杂的方法解决,并不违背 KISS 原则。除此之外,同样的代码,在某个业务场景下满足 KISS 原则,换一个应用场景可能就不满足了。
对于如何写出满足 KISS 原则的代码,我还总结了下面几条指导原则:
-
不要使用同事可能不懂的技术来实现代码;
- 不要重复造轮子,要善于使用已经有的工具类库;
- 不要过度优化。
YAGNI 原则
You Ain’t Gonna Need It.
不要去设计当前用不到的功能;不要去编写当前用不到的代码。实际上,这条原则的核心思想就是:不要做过度设计。
DRY原则
Don’t Repeat Yourself.
不要写重复的代码。
我们可以不写可复用的代码,但一定不能写重复的代码。
迪米特法则LOD
高内聚、松耦合
所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中,代码容易维护。
所谓松耦合是说,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动不会或者很少导致依赖类的代码改动。
迪米特法则
不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口(也就是定义中的“有限知识”)。
三、20条快速改善代码质量的编程规范
1.关于命名
-
命名的关键是能准确达意。对于不同作用域的命名,我们可以适当地选择不同的长度。
-
我们可以借助类的信息来简化属性、函数的命名,利用函数的信息来简化函数参数的命名。
-
命名要可读、可搜索。不要使用生僻的、不好读的英文单词来命名。命名要符合项目的统一规范,也不要用些反直觉的命名。
-
接口有两种命名方式:一种是在接口中带前缀“I”;另一种是在接口的实现类中带后缀“Impl”。对于抽象类的命名,也有两种方式,一种是带上前缀“Abstract”,一种是不带前缀。这两种命名方式都可以,关键是要在项目中统一。
2.关于注释
- 注释的内容主要包含这样三个方面:做什么、为什么、怎么做。对于一些复杂的类和接口,我们可能还需要写明“如何用”。
- 类和函数一定要写注释,而且要写得尽可能全面详细。
- 函数内部的注释要相对少一些,一般都是靠好的命名、提炼函数、解释性变量、总结性注释来提高代码可读性。
3.关于代码风格
- 函数、类多大才合适?函数的代码行数不要超过一屏幕的大小,比如 50 行。类的大小限制比较难确定。
- 一行代码多长最合适?最好不要超过 IDE 的显示宽度。当然,也不能太小,否则会导致很多稍微长点的语句被折成两行,也会影响到代码的整洁,不利于阅读。
- 善用空行分割单元块。对于比较长的函数,为了让逻辑更加清晰,可以使用空行来分割各个代码块。
- 四格缩进还是两格缩进?我个人比较推荐使用两格缩进,这样可以节省空间,尤其是在代码嵌套层次比较深的情况下。不管是用两格缩进还是四格缩进,一定不要用 tab 键缩进。
- 大括号是否要另起一行?将大括号放到跟上一条语句同一行,可以节省代码行数。但是将大括号另起新的一行的方式,左右括号可以垂直对齐,哪些代码属于哪一个代码块,更加一目了然。
- 类中成员怎么排列?在 Google Java 编程规范中,依赖类按照字母序从小到大排列。类中先写成员变量后写函数。成员变量之间或函数之间,先写静态成员变量或函数,后写普通变量或函数,并且按照作用域大小依次排列。
4.关于编程技巧
- 将复杂的逻辑提炼拆分成函数和类。
- 通过拆分成多个函数或将参数封装为对象的方式,来处理参数过多的情况。
- 函数中不要使用参数来做代码执行逻辑的控制。
- 函数设计要职责单一。
- 移除过深的嵌套层次,方法包括:去掉多余的 if 或 else 语句,使用 continue、break、return 关键字提前退出嵌套,调整执行顺序来减少嵌套,将部分嵌套逻辑抽象成函数。
- 用字面常量取代魔法数。
- 用解释性变量来解释复杂表达式,以此提高代码可读性。
5.统一编码规范
项目、团队,甚至公司,一定要制定统一的编码规范,并且通过 Code Review 督促执行,这对提高代码质量有立竿见影的效果。
四、设计模式
1.创建型
单例模式
工厂模式
建造者模式
2.结构型
代理模式
桥接模式
装饰着模式
适配器模式
3.行为型
观察者模式
模板模式
策略模式
职责链模式
迭代器模式
状态模式
(转载请注明作者和出处 代码笔记本lcwnote.com)