OOP三大特性之封装:写出高内聚的clean code
Posted JavaEdge.
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OOP三大特性之封装:写出高内聚的clean code相关的知识,希望对你有一定的参考价值。
结构化编程有效解决了很多问题,但随代码膨胀,结构化编程的局限也越发凸显。但它提供的解决方案中,各模块依赖关系太强,不能有效将变化隔离。
于是,OOP诞生,提供更好的程序组织方案。
习惯结构化编程的程序员,认为OO就是数据+函数。
这种理解不算全错,但理解程度太低。结构化编程思维就如用显微镜看世界,这只能看到局部。
而想要用好OOP,则需有更宏观的视角。
我们都知道 OOP三大特点:封装、继承和多态。
虽然背的都挺熟练,面试时都知道,但实际上,我看很多程序员的代码风格,还真没理解这三大特点。
面试时说的头头是道,代码却写得稀里糊涂,这就是大多数Java 程序员。本文我们就好好理解封装。
理解封装
OO是解决更大规模应用开发的一种尝试,它提升了程序员管理程序的尺度。
封装,是OO的根基。它把紧密相关的信息放在一起,形成一个单元。
若该单元稳定,即可将该单元和其它单元继续组合,构成更大单元。
同理,继续构建更大单元,一层层封装变大。
OO的初心
OO这个词由Alan Kay创造,2003年图灵奖获得者。其最初构想,对象就是个细胞。将细胞组织起来,组成身体各器官,再组织起来,就构成人体。
而当你去观察人时,就不用再去考虑每个细胞如何。
所以,OO提供了一个更宏观思维。
但这一切的前提:每个对象要构建好,即封装要做好。就像每个细胞都有细胞壁将其与外界隔离,形成一个完整个体。
Kay强调对象之间只能通过消息通信。按如今程序设计语言通常做法,发消息就是方法调用,对象之间就是靠方法调用通信。
但这方法调用并非简单地把对象内部的数据通过方法暴露。Kay的构想甚至想把数据去掉。
因为封装的重点在于对象提供了哪些行为,而非有哪些数据。
即便我们把对象理解成数据+函数,数据、函数也不是对等的:
- 函数是接口
接口是稳定的 - 数据是内部的实现
实现是易变的,应该隐藏
那么看看很多人的日常习惯:编写一个类,写出它的一堆字段,然后生成一堆getter、setter,将这些字段的访问暴露出去。
这种做法是错误的,在于把数据当成设计核心,这一堆getter、setter,就等于暴露实现细节。
正确做法是:
- 设计一个类,先考虑对象应提供哪些行为
- 然后,根据这些行为提供对应方法
- 最后考虑实现这些方法要有哪些字段
所以连接二者的是方法,其命名就是个大学问了,应体现你的意图,而非具体怎么做的。所以,getXXX和setXXX绝不是个好命名。
比如,设计:用户修改密码。
一些人上手就来:
但推荐写法是表达你的意图:
两段代码只是修改密码的方法名不同,但更重要的差异是:
- 一个在说做什么
- 一个在说怎么做
将意图与实现分离,优秀设计须考虑的问题。
实际项目中,有时确实需要暴露一些数据。
所以,当确实需暴露时,再写getter也不迟,你一定要问自己为何要加getter?
关于setter:
- 大概率是你用错名字,应该用一个表达意图的名字
- setter通常意味着修改,这是不推荐的
可变的对象会带来很多的问题,后续再深入讨论。所以,设计中更好的做法是设计不变类。
Lombok很好,少写很多代码,但必须限制它的使用,像Data和Setter都不该用。Java Bean本来也不是应该用在所有情况下的技术,导致很多人误用。
减少暴露接口
之所以需要封装,就是要构建一个内聚单元。所以,要减少该单元对外的暴露:
- 减少内部实现细节的暴露
- 减少对外暴露的接口
OOP语言都支持public、private,日常开发经常会轻率地给一个方法加public,不经意间暴露了一些本是内部实现的部分。
比如,一个服务要停下来时,你可能要把一些任务都停下来:
别人可能这样调用时:
突然某天,你发现停止轮询任务必须在停止定时器任务之前,你就不得不要求别人改代码。而这一切就是因为我们很草率地给那两个方法加上public,让别人有机会看到这俩方法。
设计角度,必须谨慎自省:这个方法有必要暴露吗?
其实可仅暴露一个方法:
外部的调用代码也会简化:
尽可能减少接口暴露,该原则适于类的设计、系统设计。
很多人都特别随意在系统里添加接口,让一个看似不复杂的系统,随便就有成百上千个接口。
后续,当你想改造系统,去掉一些接口时,很可能造成线上事故,因为你根本不知道哪个团队在何时用到了它。
所以,软件设计中,暴露接口要谨慎!
可总结出一个原则:最小化接口暴露,即每增加一个接口,都要找到一个充分的理由!
有了封装,对象就成了一个个可组合单元,也形成了一个个可以复用的单元。面向对象编程的思考方式就是组合这些单元,完成不同的功能。
同结构化编程相比,这种思考问题的方式站在了一个更宏观视角。
总结
封装的重点在于对象提供了哪些行为,而不是有哪些数据。封装,除了要减少内部实现细节的暴露,还要减少对外接口的暴露。每暴露一个公共API就增加一份职责,所以在每次暴露API时就要问自己,这个职责是自己必要的,还是有可能会增加不必要的负担。
一个原则是最小化接口暴露。
注意区分:
- OO和 Java 语言
- 传输数据和业务对象
Java语言特点就是一切皆对象,Java中对象的概念跟OO中对象的概念不同:
- 前者是语言特性
- 后者是一种编程范式
在具体编码中,哪些属于对象,哪些不属于对象,应该是程序员掌控。
比如:
- DDD中的领域实体,就是对象,需仔细设计其行为接口
- 一些POJO,可看成数据载体,可直接加getter、setter的(没有这些默认getter、setter,很多第三方数据转化都很不方便,比如JSON,SQL)。使用时,不归结为对象即可
基于行为进行封装,不要暴露实现细节,最小化接口暴露。
Demeter 不是一个人,而是一个项目,项目主页 http://www.ccs.neu.edu/research/demeter/。最早提到迪米特法则的论文出版于 1989 年,Assuring good style for object-oriented programs。还有一本书,1996 年出版,Adaptive Object-Oriented Software: The Demeter Method with Propagation Patterns。没有看过。
Demeter 是希腊神话中的大地和丰收女神,也叫做德墨忒尔。
迪米特法则简单的说,分为两个部分:不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。其实如果用另一个名字“最小知识原则”可能更容易理解一些,这个也算是程序员的“黑话”吧。
虽然接触面向对象编程已经很久了,也知道封装、继承和多态,不过写程序的时候,似乎还是习惯“一个对象一张表”的方式,也没有太多的考虑过封装的问题。整个类里面都是 getter、setter 的事情也做过,有点像是用“面向对象的语言写面向过程的代码”。
参考
- https://www2.ccs.neu.edu/research/demeter/
以上是关于OOP三大特性之封装:写出高内聚的clean code的主要内容,如果未能解决你的问题,请参考以下文章