面向对象

1. 面向对象概述

现在主流的编程范式或编程风格有三种,分别是面向过程、面向对象和函数式编程。面向对象又是这其中最主流的,现在大部分编程语言都是面向对象编程语言。大部分项目也是基于面向对象风格开发的。面向对象拥有丰富的特性(封装、抽象、继承、多态),可以实现很多复杂的设计思路,是很多设计原则、设计模式编码实现的基础。

2. 面向对象四大特性

“封装”,封装也叫作信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类的方法来访问内部信息或者数据。它需要编程语言提供全新访问控制机制的语法来支持,例如Java中的private、protected、public关键字。封装特性存在的意义,一方面是保护数据不被随意修改,提高代码可维护性;另一方面是仅仅暴露有限的必要接口,提高类的易用性。

“抽象”,如果说封装主要是如何隐藏信息、保护数据,那抽象就是如何隐藏方法的具体实现,让使用者只需要关心方法提供了哪些功能,不用了解方法功能的具体实现细节。抽象可以通过接口或者抽象类来实现。抽象存在的意义,一方面是修改实现不需要改变定义;另一方面,它也是处理复杂系统的有效手段,能有效地过滤掉不必要关注的信息。

“继承”,继承用来表示类之间的is-a关系,分为两种模式:单继承和多继承。单继承表示一个子类只继承一个父类,多继承表示一个子类可以继承多个父类。继承主要是用来解决代码复用的问题。

“多态”,多态是指子类可以替换父类,在实际代码运行过程中,调用子类的方法实现。比如继承、接口类、ducktyping。多态可以提高代码的扩展性和复用性,是很多设计模式、设计原则、编程技巧的代码实现基础。

3. 面向对象 VS 面向过程

面向对象编程相比面向过程编程的优势主要有三个。

对于大规模复杂程序的开发,程序的处理流程并非单一的一条主线,而是错综复杂的网状结构。面向对象编程比起面向过程编程,更能应对这种复杂类型的程序开发。
面向对象有更加丰富的特性(封装、抽象、继承、多态)。利用这些特性编写出来的代码更容易扩展、复用、维护。
面向对象编程语言比起面向过程编程语言,更加人性化、高级、智能。

4. 面向对象分析、设计与编程

面向对象分析(OOA)、面向对象设计(OOD)、面向对象编程(OOP),是面向对象开发的三个主要环节。简单点讲,面向对象分析就是要搞清楚做什么,面向对象设计就是要搞清楚怎么做,面向对象编程就是将分析和设计的结果翻译成代码的过程。

面向对象编程基本的判断标准就是让代码尽量地满足“松耦合”、“高内聚”、单一职责、对扩展开放对修改关闭,尽量做到代码可复用、易读、易扩展、易维护。

5. 接口 VS 抽象类

抽象类不允许被实例化,只能被继承。它可以包含属性和方法。方法可以包括代码实现,也可以是抽象方法。子类继承抽象类,必须实现抽象类中的所有抽象方法。接口不能包含属性(Java可以定义静态常量),只能声明方法,方法不能包含代码实现(java8后可以有默认实现)。类实现接口的时候,必须实现接口中声明的所有方法。

什么时候用抽象类?什么时候用接口?

如果要表示一种 is-a 的关系,并且为了解决代码复用问题,我们就用抽象类,但是抽象类不能有太多层,不然阅读和维护起来会出现很多问题;如果要表示一种 has-a 关系,并且是为了解决抽象而非代码复用问题,那我们就用接口。

6. 基于接口而非实现编程

应用这条原则,可以将接口和现实相分离,封装不稳定的实现,暴露稳定的接口。上游系统面向接口编程,不依赖不稳定的实现细节,实现代码发生变化的时候,上游系统的代码基本上不需要做改动,降低耦合性,提高扩展性。

越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。

7. 多用组合少用继承

为什么不推荐使用继承?

继承是面向对象的四大特性之一,用来表示类之间的 is-a关系,可以解决代码复用的问题。虽然继承有很多好处,但是继承的层次过深、过复杂,也会影响到代码的可维护性。在这种情况下,应尽量少用继承,甚至不用。

组合相比继承有哪些优势?

继承主要有三个作用:表示 is-a关系、支持多态性、代码复用。而这三个作用都可以通过组合、接口、委托三个技术手段来达成。除此之外,利用组合还能解决层次过深、过复杂的继承关系影响代码可维护性的问题。

如何判断该用组合还是继承?

在实际开发中,要根据情况,来选择该用继承还是组合。如果类之间的继承结构稳定,层次比较浅,关系不复杂,还有一些设计模式、特殊的应用场景,会固定使用继承或者组合。

设计模式

1. SOLID原则:SRP单一职责原则

一个类只负责完成一个职责或者功能。单一职责原则通过避免设计大而全的类,避免不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少代码耦合,实现代码的高内聚、松耦合。但是,拆分的过细,实际上会适得其反,反倒降低内聚性,也会影响代码的可维护性。

不同的场景、不同阶段的需求、不同业务,对同一个类的职责是否单一,可能有不同的判定结果,比如,出现下面这些情况就有可能说明这个类设计的不满足单一职责原则:

类中的代码行数、函数或者属性过多;
类依赖的其他类过多或者依赖类的其他类过多;
私有方法过多;
比较难给类起一个合适的名字;
类中大量的方法都是几种操作类中的某几个属性。

2. SOLID原则:OCP开闭原则

如何理解 “ 对扩展开放、修改关闭 ”?

添加一个新功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。有亮点需要注意,第一是,开闭原则并不是完全杜绝修改,而是以最小的修改代码的代价完成新功能的开发。第二点是,同样的代码改动,在粗代码粒度下,可能被认定为 ‘修改“;在细粒度下,节能又被认定为 “扩展”。

如何做到 “对扩展开放、修改关闭”?

要时刻具备扩展意识、抽象意识、封装意识。写代码的时候多思考代码未来有可能变更的地方,如何设计代码结构,事先留好扩展点,好方便未来需求变更的时候,在不改变代码整体结构、做到最小代码改动,将新代码灵活的插入到扩展点上。

3. SOLID原则:LSP里氏替换原则

子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。

里氏替换原则是用来指导继承关系中子类该如何设计的一个原则。理解里氏替换原则,最核心的就是理解 “ design by contract,按照协议来设计 ”这几个子。父类定义了函数的约定,那子类可以改变函数的内部实现逻辑,但不能改变函数的原有 “约定”。这里的“约定”包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。

还要理解里氏替换原则跟多态的区别。虽然从定义描述和代码实现上来看他们有点类似,但是他们关注的角度是不一样的。多态是面向对象编程的一大特性,也是面向对象编程的一种语法,是一种代码实现的思路。而里氏替换原则,是用来知道继承关系中该如何设计,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑及不破坏原有程序的正确性。

4. SOLID原则:ISP接口隔离原则

接口隔离原则的描述是:客户端不应该强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者。理解“接口隔离原则”的重点是理解其中的 “接口”二字。这里有三种不同的理解。

如果把“接口”理解为一组接口集合,可以是某个微服务接口,也可以是某个类库的接口等。如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口。

如果把“接口”理解为单个API接口或函数,部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的哪个细粒度函数。

如果把“接口”理解为OOP中的接口,也可以理解为面向对象编程语言中的接口语法。那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。

5. SOLID原则:DIP依赖倒置原则

控制反转:实际上,控制反转是一个比较笼统的设计思想,不是一种具体的实现方法,一般用来指导框架层面的设计。控制-指的是对程序执行流程的控制,而反转-指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,程序的执行流程通过框架来控制。

依赖注入:依赖注入和控制反转恰恰相反,它是一种具体的编码技巧。不通过new的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类来使用。

依赖注入框架:通过依赖注入框架提供的扩展点,简单配置需要的类及其类与类之间的依赖关系,可以由框架自动创建对象、管理对象的生命周期、依赖注入等原来本需要程序员来做的事情。

依赖反转原则:依赖反转原则也叫做依赖倒置原则。跟控制反转有点类似,用来指导框架层面的设计。高层模块不依赖底层模块,他们共同依赖同一个抽象。抽象不需要依赖具体实现细节,具体实现细节依赖抽象。

6. KISS、YAGNI原则

KISS原则的中文描述是:尽量保持简单。KISS原则是保持代码可读和可维护的重要手段。KISS原则中的“简单”并不是以代码行数来考量的。代码行数越少并不代表代码越简单,我们还要考虑逻辑复杂度、实现难度、代码可读性等。

对于如何写出满足KISS原则的代码,大概有几条知道原则:

不要使用同时可能不懂的技术来实现代码;
不要重复造轮子,善于使用已经有的工具类库;
不要过度优化。
YAGIN原则翻译过来就是:你不会需要它。在软件开发中的时候,它的意思是:不要去设计当前用不到的功能;不要去编写当前用不到的代码。大概就是不要过度的设计。

YAGIN原则跟KISS原则并非一回事。KISS原则讲的是“如何做”的问题(尽量保持简单),而YAGIN原则说的是“要不要做”的问题(当前不需要的就不要做)。

7. DRY原则

DRY原则中文描述是:不要重复自己,也就是不要写重复的代码。

大概有三种俯冲的情况:实现逻辑重复、功能语义重复、代码执行重复。

8. LOD原则

如何理解“高内聚、松耦合”?

高内聚、松耦合是一个非常重要的设计思想,能够有效提高代码的可读性和可维护性,缩小功能改动呆滞的代码改动范围。“高内聚”用来指导类本身的设计,“松耦合”用来指导类与类之间依赖关系的设计。所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。所谓松耦合,就是在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动也不会或者很少导致依赖类的代码改动。

如何理解“迪米特法则”

迪米特法则描述为:不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。