1. 什么是面向对象编程和面向对象编程语言
面向对象编程是一种编程范式或编程风格。它以类或对象作为组织代码的基本单元,并将封装、抽象、继承和多态四大特性,作为代码设计和实现的基石。
面向对象编程语言是支持类或对象的语法机制,并且利用现成的语法机制,能够方便地实现面向对象编程四大特性的编程语言。
面向对象编程和面向对象编程语言之间的关系
一般来说,面向对象编程是由面向对象编程语言来实现的。但不用面向对象编程语言同样可以进行面向对象编程。
同样的,就算使用面向对象编程语言,写不来的代码也未必是面向对象编程风格的,有可能是面向过程编程。
2. 衡量一种语言是否是面向对象编程语言的方法
按照不严格的定义来讲,一般认为只要是以类或对象为组织代码基本单元的语言都称为面向对象编程语言。
而按照严格的定义来讲,面向对象编程语言不仅要满足以类或对象为组织代码的基本单元,而且还需要提供对面向对象四大特性(封装、抽象继承和多态)的支持。
3. 什么是面向对象分析 OOA 和面向对象设计 OOD
面向对象分析和面向对象设计都是针对对象或类来做需求分析和设计的。分析和设计完成后,最终的产出是类的设计。这个产出包括根据需求拆解出哪些类,每个类中包含哪些属性和方法,类与类之间的如何交互等。
简单来讲,面向对象分析就是要搞清楚要做什么,而面向对象设计就是要搞清楚要怎么做。
4. 理解面向对象四大特性
4.1 封装
封装是类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据。
封装对编程语言的要求
封装需要对类的访问进行控制的前提是编程语言提供了访问权限控制的语法机制。
封装存在的意义
- 提高代码的可读性和可维护性。如果没有封装,对类中属性的修改逻辑会散落在代码中的各个角落
- 提高类的易用性。每个类只提供有限的方式暴露其必要的操作,如果把类中所有的属性都暴露给了调用者,想要正确的使用属性,就必须对业务有较深的认识,对调用者来说也是一种负担
4.2 抽象
抽象主要是隐藏方法的具体实现,让调用者只关心方法提供了哪些功能,而不需要关心这些功能的具体实现。
作用
隐藏方法的具体实现。
抽象实现
在 java 世界,使用 interface 或 abstract 来实现抽象。
更广泛一点的抽象是类中的“函数”。通过函数包裹具体的实现逻辑,这本身就是一种抽象。用户在调用函数的时候,并不需要去了解其具体实现,只需要通过命名,注释和文档就可以直接使用了。
为什么有些地方抽象不被当作面向对象编程的一大特性
抽象这个概念是无处不在的,不仅编程会用到,架构设计等都会使用到抽象的概念。而且,只要提供了函数的语法机制,就可以实现抽象特性,它并没有什么特别的地方,所以,有些地方抽象并不被当作是面向对象编程的一大特性,因为它应用太普遍了,不只是面向对象编程中才有的概念。
抽象存在的意义
- 只关注实现的设计思路,帮助我们大脑过滤掉了许多非必要的信息
- 抽象思想在代码设计中起到了非常重要的指导作用。很多设计原则和设计模式如:基于接口而非实现、开闭原则、接口隔离原则等等都体现到了抽象的思想
- 抽象思想可以减少修改的可能性。在方法定义中,不要定义太多的实现细节。比如:通过 URL 获取图片,如果使用 getPicFromBaidu 的话,后面实现改成从 aliyun 获取图片,相应的方法定义需要改变,这样就会影响到对象的调用者
4.3 继承
继承表示的是类之间的一种 is a 关系。
继承存在的意义
继承最大的好处就是代码复用,避免代码重复写很多遍。同时,继承的设计符合现实人类的认知,很容易被人所理解。
继承带来的问题
- 继承层次过深,影响代码的可读性和可维护性
- 继承使得父类和子类耦合,针对父类的修改,也会影响到子类
继承的替代方案
一般认为,组合优于继承,使用松耦合的组合来替换强耦合的继承是一种主流的实现方式。
4.4 多态
多态指子类可以替代父类,在实现代码的运行中,调用子类的方法实现。
多态对编程语言的要求
- 父类对象可以引用子类对象
- 编程语言要支持继承或接口
- 子类可以实现父类中的方法
duck-typing 语法
duck-typing 语法是除继承或接口实现外,另外一种实现多态的方式,主要出现于动态语言中。duck-typing 关注的重点是该类能做什么,也就是当前类中的函数。如果我们定义一个函数,接收一个类型为“鸭子”的对象,并在函数内部调用了 “走”和“叫”方法。该函数可以接收任意对象,并调用接收对象的“走”和“叫”方法,如果接收对象中有没有这两个方法,则会报错。
class Logger:
def record(self):
print(“I write a log into file.”)
class DB:
def record(self):
print(“I insert data into db. ”)
def test(recorder):
recorder.record()
def demo():
logger = Logger()
db = DB()
test(logger)
test(db)
上面的代码中,Logger 和 DB 没有任何继承或实现同一接口的关系,但是都能调用 test 类,并执行对象的 record 方法。可以看出,这是一种实现多态更加灵活的方式。
即只要两个类中有相同的方法,就能实现多态,并不需要两个类之间有任何关系。
多态存在的意义
- 多态提高的代码的可扩展性。多态可以灵活地让新的逻辑可以通过继承父类或实现接口的方法加入到代码逻辑中来
- 多态提高的代码的复用性。同一段代码逻辑可以被多种具体的实现来调用
- 多态是很多设计原则、设计模式和编程技巧的代码实现基础
5. 什么是页面过程编程和面向过程编程语言
面向过程编程是一种编程范式或编程风格。它以过程(方法或者函数)作为组织代码的基本单元,以数据(成员变量或属性)与方法相分离为主要特点。面向过程编程是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作完成一项功能。
面向对象编程语言是一种编程语言。它不支持类和对象两大语法概念,更不支持面向对象的编程特性(比如:封装、继承和多态),仅支持面向过程编程。
什么情况下适合使用面向过程的编程方式
- 开发的程序本身就比较微小
- 一个数据处理相关的代码,以算法为主,数据为辅
6. 面向对象编程相比面向过程编程有哪些优势
- OOP 更能够应对大规模复杂程序的开发。对于大规模复杂程序的开发来说,整个程序处理流程错综复杂,并非只有一条主线。整个程序的处理流程是一个网状结构。如果我们再用流程化、线性的方式去翻译网状结构,就会非常吃力。
- OOP 代码更易复用、易扩展和易维护。主要由于面向对象可以利用封装、抽象、继承和多态四大特性非常方便的进行代码实现。
- OOP 更加人性化、更加高级、更加智能。编程语言的一个特点是让人跟机器打交道越来越容易。
7. 避免写面向过程风格的代码
- 避免滥用 getter、setter 方法
- 避免滥用全局变量和全局方法
- 避免定义数据和方法分离的类
大而全的 Constants 类所带来的问题
- 影响代码的可维护性。主要是类中数据多了之后,会影响查找的效率;同时,增加冲突的概率
- 增加代码的编译时间。Constants 类一般会被项目中很多地方引用到,每次对 Constants 类的修改,都会导致所有依赖它的类被重新编译
对大而全的 Constants 类的改造思路
- 将 Constants 类拆分成功能更加单一的多个类,比如:mysql 配置类、Redis 配置类
- 不专门定义 Constants 类,每个类自己维护对应的常量
后端 MVC 三层模型中的对应关系
在做前后端分离之后,三层结构在后端开发中,进行了调整。
- Model 层对应 Repository(Entity),负责数据读写
- View 层对应 Controller(VO,View Object),负责暴露接口给前端使用
- Controller 层对应 Service(BO,Bussiness Object),负责核心业务逻辑
8. 在面向对象编程中,为什么容易写出面向过程风格的代码
面向过程风格恰恰符合人先做什么、再做什么等的这种流程化思维方式。而面向对象编程思想中,不是一上来就按照执行流程来分解任务,而是先将任务分解为一个个的小模块,再设计类之间的交互,最后按照才按照流程将类组装起来,完成整个任务。这样的思考路径比较适合复杂的业务开发,但并不符合人类的思考习惯。
9. 基于贫血模型的开发模式
数据和操作被分开定义了。大部分的后端 MVC 开发模式会定义 Controller、Service、Repository 数据操作类和 VO(View Object)、BO(Bussiness Object)、entity 数据类,这样做,数据和对数据的操作就分离了。属于基于贫血模型的开发模式。
10. 抽象类和接口
10.1 为什么要使用抽象类
方便优雅地实现多态。如果子类都有某个方法,而父类没有的情况下,无法使用多态调用该方法。如果要可调用,就需要父类定义空方法,子类实现,这种方式并不优雅。而且,当扩展新的子类的时候,会忘了覆写这个子方法,而执行空的父类方法。
10.2 为什么需要使用接口
接口就是一组协议和契约。调用者只需要关注抽象的接口,不需要了解具体的实现。接口实现了约定和实现的分离,可以降低代码之间的耦合性,提高代码的可扩展性。
10.3 如何决定该使用抽象类或者接口
- 如果是一种 is-a 的关系,并且为了解决代码复用的问题,我们就用抽象类
- 如果表示的是一种 has-a 的关系,表示具有某一组行为特性,并且是为了解决抽象而非代码复用的问题,那我们就可以使用接口
10.4 基于接口而非实现编程原则
这条原则将接口和实现相分离,封装不稳定的实现,暴露稳定的接口。上游系统面向接口而非实现编程,当实现发生变化后,上游系统代码基本上不需要做改动,以此来降低耦合性,提高扩展性。
接口的设计原则
- 在定义接口的时候,不要暴露任何实现的细节。接口的定义只表明做什么,而不是怎么做
- 命名要足够通用,不能包含具体实现相关的字眼;同时,与特定实现有关的方法不要定义在接口中
11. 组合和继承
继承主要的三个作用:
- 表示 is-a 关系
- 支持多态特性
- 代码复用
11.1 替代继承的实现方式
接口替代继承
接口表示 has-a 的关系,也就是表明当前类具有哪些功能。我们可以通过定义一个或多个接口并由其对应的类来实现这些接口来替代继承。但这种替代过程中,对于同一个接口,需要实现该接口的每个类都实现一遍。如果这些实现都是一样的,就会导致代码重复的问题。如果想要解决代码重复的问题,可以将相同代码抽离到一个公共的处理类,然后,再通过组合的方式调用公共类中的方法。
替代继承的 is-a 关系
通过组合和接口的 has-a 来替代。
替代继承的多态特性
利用接口来实现。
替代继承的代码复用
通过组合和委托来实现。
总结
通过组合、接口和委托三个技术手段,完全可以替换掉继承。对于一些复杂的继承关系,推荐使用这种方式来进行替换继承的实现。
组合存在的问题
使用组合替代继承过程中,类和接口的增长增加了代码的复杂度和维护成本。
11.2 何时使用继承,何时使用组合
如果类之间的继承关系稳定,继承层次关系比较浅,继承关系不复杂的情况下,直接使用继承就可以了。反之,系统越不稳定,继承层次越深,继承关系越复杂,那就需要尽量使用组合来替代继承。
必须用组合的场景
两个没有 is-a 关系的类都用到了同一个方法,比如:字符串拼接,这种情况下,使用组合的方式,每个类各自通过依赖注入带有该方法的类,然后进行逻辑处理。
必须用继承的场景
一个函数的入参类型是一个具体的类,而不是接口,为了支持多态,只能采取继承来实现。以保证能使用该函数。还有一种情况是,对于一个外部类,我们想复用它的大部分方法,而少量方法需要改动,可以通过继承,覆写方法的方式来达到目的。
说明
此文是根据王争设计模式之美相关专栏内容整理而来,非原创。