面向过程
什么是面向过程编程
相较于面向对象编程以类为组织代码的基本单元,面向过程编程则是以过程(或方法)作为组织代码的基本单元。它最主要的特点就是数据和方法相分离。相较于面向对象编程语言,面向过程编程语言最大的特点就是不支持丰富的面向对象编程特性,比如继承、多态、封装。
面向对象
什么是面向对象编程和面向对象编程语言
-
面向对象编程(OOP,Object Oriented Programming)
是一种编程范式或编程风格。它以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性作为代码设计和实现的基石。 -
面向对象编程语言(OOPL,Object Oriented Programming Language)
是支持类或对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程四大特性的编程语言
如何判定一个编程语言是否是面向对象编程语言
只要某种编程语言支持类或对象的语法概念,并且以此作为组织代码的基本单元,那就可以被粗略地认为它就是面向对象编程语言了。至于是否有现成的语法机制,完全地支持了面向对象编程的四大特性、是否对四大特性有所取舍或优化,可以不作为判定的标准。
面向对象编程和面向对象编程语言之间的关系
面向对象编程一般使用面向对象编程语言来进行,但是不用面向对象编程语言,照样可以进行面向对象编程。反过来讲,即使我们使用面向对象编程语言,写出来的代码也不一定是面向对象编程风格的,也有可能是面向过程编程风格的。
什么是面向对象分析和面向对象设计
面向对象分析(OOA,Object Oriented Analysis)就是要搞清楚做什么,面向对象设计(OOD,Object Oriented Design)就是要搞清楚怎么做。这两个阶段最终的产出是类的设计,包括程序被拆解为哪些类,每个类有哪些属性方法、类与类之间如何交互等等。
面向对象软件开发要经历的三个阶段:OOA就是要搞清楚做什么;OOD就是要搞清楚怎么做;OOP就是将分析和设计的结果翻译成代码的过程
面向过程 VS 面向对象
面向对象编程相比面向过程编程的优势
- 对于大规模复杂程序的开发,程序的处理流程并非单一的一条主线,而是错综复杂的网状结构。面向对象编程比起面向过程编程,更能应对这种复杂类型的程序开发
- 面向对象编程相比面向过程编程,具有更加丰富的特性。利用这些特性编写出来的代码,更加易扩展、易复用、易维护。
- 从编程语言跟机器打交道的演化规律中,面向对象编程语言比起面向过程编程语言,更加人性化、更加高级、更加智能。
哪些代码设计看似面向对象,实际是面向过程的
- 滥用getter、setter方法
它违反了面向对象编程的封装特性,相当于将面向对象编程风格退化成了面向过程编程风格。
在设计实现类的时候,除非真的需要,否则尽量不要给属性定义setter方法。getter方法虽然相对安全些,但如果返回的是集合容器,也需要防范集合内部数据被修改的风险 - 滥用全局变量和全局方法
全局变量(单例类对象、静态成员变量、常量)通常被设计在Constants类中;全局方法通常设计在Utils类中。全局方法将方法和数据分离,破坏了封装特性,也是典型的面向过程风格。
面对这两种设计,我们尽量能做到职责单一,定义一些细化的小类,比如RedisConstants,FileUtils,不是定义一个大而全的Constants类、Utils类。除此之外,如果能将这些类中的属性和方法,划分归并到其他业务类中,能极大地提高类的内聚性和代码的可复用性 - 定义数据和方法分离的类
传统的MVC叫作基于贫血模型的开发模式,这就是典型的数据与方法分离的类。
采用DDD基于充血模型的开发模式,将数据和对应的业务逻辑封装到同一个类中。
抽象类 VS 接口
特性
-
抽象类
抽象类不允许被实例化,只能被继承。
抽象类可以包含属性和方法,方法既可以包含代码实现,也可以不包含。不包含代码实现的叫抽象方法
子类继承抽象类,必须实现抽象类中的所有抽象方法 -
接口
接口不能包含属性
接口只能声明方法,方法不能包含代码实现
类实现接口的时候,必须实现接口中声明的所有方法
抽象类实际上就是类,只不过是一种特殊的类,这种类不能被实例化为对象,只能被子类继承,表示一种 is-a 的关系。接口表示一种 has-a 的关系,表示具有某些功能。对于接口,有一个更加形象的叫法,那就是协议(contract)
意义
-
抽象类
抽象类不能被实例化,只能被继承。继承能解决代码复用的问题,所以抽象类也是为代码复用而生的。
抽象类是一种特殊的类,所以抽象类能解决的问题,使用普通类也能解决。比如抽象方法可以使用普通类的空方法代替,但普通空方法带来的问题是:影响代码的可读性,如果不了解背后的设计思想,必须通过查看子类才能明白其设计意图;在几百行的代码中可能会漏掉某个空方法的重写导致故障;普通类可以被实例化,这样会导致被误用的风险,虽然可以通过私有构造函数来解决,但不够优雅。 -
接口
接口是对行为的一种抽象,相当于一组协议或契约,所以接口更侧重于解耦。
接口实现了约定和实现相分离,可以降低代码间的耦合性,能极大提高代码的灵活性、扩展性。
应用场景
-
抽象类
如果要表示一种 is-a 的关系,并且是为了解决代码复用问题 -
接口
如果要表示一种 has-a 的关系,并且是为了解决抽象而非代码复用问题
基于接口而非实现编程
“Program to an interface, not an implementation”
这条原则最早出现在 1994 年 GoF 的《设计模式》一书中,它先于很多编程语言,是一条比较抽象、泛化的设计思想。
此处的“接口”二字从本质上讲,就是一组“协议”或“约定”,是功能提供者提供给使用者的一个“功能列表”。落实到具体的编码中,可以理解为编程语言中的接口或抽象类。
“基于接口而非实现编程”这条原则的另一个表述是“基于抽象而非实现编程”。在软件开发中,最大的挑战之一就是需求的不断变化。越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。好的代码设计,不仅能应对当下的需求,而且在将来需求发生变化的时候,仍然能够在不破坏原有代码设计的情况下灵活应对。抽象就是提高代码扩展性、灵活性、可维护性最有效的手段之一。
public class AliyunImageStore {
//...省略属性、构造函数等...
public void createBucketIfNotExisting(String bucketName) {
// ...创建bucket代码逻辑...
// ...失败会抛出异常..
}
public String generateAccessToken() {
// ...根据accesskey/secrectkey等生成access token
}
public String uploadToAliyun(Image image, String bucketName, String accessToken) {
//...上传图片到阿里云...
//...返回图片存储在阿里云上的地址(url)...
}
public Image downloadFromAliyun(String url, String accessToken) {
//...从阿里云下载图片...
}
}
遵从“基于接口而非实现编程”的原则,需要做到以下三点:
- 函数的命名不能暴露任何实现细节
比如:uploadToAliyun()就不符合要求,应该改为更加抽象的命名方式,upload() - 封装具体的实现细节
比如:跟阿里云相关的特殊上传流程不应该暴露给调用者,createBucketIfNotExisting 或 generateAccessToken。我们应该对上传流程进行封装,对外提供一个包括所有上传细节的方法,给调用者使用 - 为实现类定义抽象的接口
具体的实现类都依赖统一的接口定义,遵从一致的上传功能协议。使用者依赖接口,而不是具体的实现类来编程
在软件开发的时候,一定要有抽象意识、封装意识、接口意识。在定义接口的时候,不要暴露任何实现细节。接口的定义只表明做什么,而不是怎么做。
是否需要为每个类定义接口
做任何事情都要讲一个“度”,过度使用这条原则,非得给每个类都定义接口,也会导致不必要的开发负担。
这条原则的设计初衷是,将接口和实现分离,封装不稳定的实现,暴露稳定的接口。如果在业务场景中,某个功能只有一种实现方式,未来也不可能被其他实现方式替换,那我们就没有必要为其设计接口,也没有必要基于接口编程,直接使用实现类就可以了。
组合优于继承
-
为什么不推荐使用继承
继承用来表示类之间的 is-a 关系,可以解决代码复用问题。但继承层次过深,过复杂,也会影响到代码的可维护性。这种情况下,尽量少用甚至不用继承 -
组合相比继承有哪些优势
继承主要有三个作用:表示 is-a 关系;支持多态特性;代码复用。这三个作用都可以通过组合、接口、委托三个技术手段来达成,除此之外,利用组合能解决层次过深、过复杂的继承关系影响代码可维护性的问题
public interface Flyable {
void fly();
}
public class FlyAbility implements Flyable {
@Override
public void fly() { //... }
}
//省略Tweetable/TweetAbility/EggLayable/EggLayAbility
public class Ostrich implements Tweetable, EggLayable {//鸵鸟
private TweetAbility tweetAbility = new TweetAbility(); //组合
private EggLayAbility eggLayAbility = new EggLayAbility(); //组合
//... 省略其他属性和方法...
@Override
public void tweet() {
tweetAbility.tweet(); // 委托
}
@Override
public void layEgg() {
eggLayAbility.layEgg(); // 委托
}
}
-
如何判断该用组合还是继承
如果类之间的继承结构稳定、层次比较浅、关系不复杂,就可以大胆地使用继承。反之就尽量使用组合来替代继承。
基于MVC贫血模式 VS 基于DDD充血模式
什么是基于贫血模型的传统开发模式
在MVC三层架构中,UserEntity 和 UserRepository 组成了数据访问层;UserBO 和 UserService 组成了业务逻辑层;UserVO 和 UserController 组成接口层。UserBO 是一个纯粹的数据结构,只包含数据,不包含业务逻辑。业务逻辑集中在 UserService 中。
像 UserBO 这样只包含数据,不包含业务逻辑的类,就叫作贫血模型(Anemic Domain Model)。这种贫血模型将数据与操作分离,破坏了面向对象的封装特性,是一种典型的面向过程的编程风格。
什么是基于充血模型的DDD开发模式
充血模型(Rich Domain Model)与之相反,数据和对应的业务逻辑被封装到同一个类中。因此充血模型满足面向对象的封装特性,是典型的面向对象编程风格。
DDD(领域驱动设计)主要是用来指导如何解耦业务系统,划分业务模块,定义业务领域模型及其交互。
基于贫血模式的传统开发中,Service 层包含 Service 类和 BO 类,基于充血模型的 DDD 开发模式中,Service 层包含 Service 类和 Domain 类两部分。Domain 就相当于 BO。不过 Domain 与 BO 的区别在于它是基于充血模型开发的,既包含数据,也包含业务逻辑。总结一下就是,贫血模型重 Service 轻 BO;充血模型轻 Service 重 Domain。
为什么基于贫血模式的传统开发受欢迎
- 大部分情况下,系统业务可能都比较简单,都是基于 SQL 的 CRUD 操作,贫血模式足以应付。
- 充血模型的设计要比贫血模型更加有难度。因为充血模型从一开始就要设计好针对数据要暴露哪些操作,定义哪些业务逻辑。而不是想贫血模型那样只需要定义数据,之后有什么功能直接在 Service 层定义什么操作
- 思维固话,转型有成本
什么项目应该考虑使用充血模型的DDD开发模式
对于业务不复杂的系统来说,基于贫血模型的传统开发模式简单够用。
对于业务复杂的系统开发来说,基于充血模型的DDD开发模式,因为前期需要在设计上投入更多时间和精力,来提高代码的复用性和可维护性,所以相比于贫血模型的开发模式,更加有优势。
Controller 层和 Repository 层有必要也进行充血领域建模吗?
Controller 层主要负责接口的暴露,Repository 层主要负责与数据库打交道,这两层包含的业务逻辑并不多,即便是设计成充血模型,类也非常单薄,看起来也很奇怪,所以没必要进行充血领域建模。
尽管这样的设计是一种面向过程的编程风格,但只要控制好面向过程编程风格的副作用,一样可以开发出优秀的软件。
- Repository 的 Entity 被设计成贫血模型,违反面向对象编程的封装特性,有被任意代码修改数据的风险,但 Entity 生命周期是有限的。一般来说,传递到 Service 层之后就会转化为 BO 或 Domain,Entity 的生命周期到此就结束了,所以并不会被到处任意修改
- Controller 层的 VO实际上是一种 DTO,它主要是作为接口的数据传输承载体,将数据发送给其他系统。从功能上来讲,它理应不包含业务逻辑、只包含数据。所以将它设计成贫血模型是比较合理的。
网友评论