日拱一卒 | 设计模式之美 | 02 面向对象 理论篇
日拱一卒(2/100)
今天学习分享的是王争的《设计模式之美》之《面向对象》理论篇
封装、抽象、继承、多态
封装(Encapsulation)
封装可以提高代码可维护性;降低接口复杂度,提高类的易用性。
封装也叫作 信息隐藏 或 数据访问保护,类通过暴露有限的访问接口,授权外部仅能通过类提供的方式来访问内部信息或者数据。
例如有个钱包类:
代码语言:javascript复制public class 钱包{
private String id;
private long createTime;
private BigDecimal 余额;
private long 上次余额变更时间;
private ...
}
如果你全部都开放 get 和 set,那就有问题了,从业务角度来讲:
- id 和创建时间在创建时应该就已经赋值了,不可以改变。
- 余额从业务角度来讲只能增加或者减少,而不能重新设置。
应该这样做:
- 去掉余额和变更时间的 set 方法。
- 添加余额增加方法和减少方法。
- 并且在这俩个方法里同步更新变更时间。
这样也可以保证数据的一致性,同时也能确保业务代码不会散落在各处。
抽象(Abstraction)
抽象可以提高代码的扩展性、维护性;降低复杂度,减少细节负担。
隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的。
如:定义类方法时,不要在定义中暴露太多实现细节,比如 getAliyunPictureUrl () 改成 getPictureUrl () ,这样后期如果变更实现的话,也不需要去修改方法命名。
继承(Inheritance)
继承最大的一个好处就是代码复用(不止继承,组合关系也可以)
但过度继承、继承层次过深过复杂也会导致可读性维护性变差
继承用来表示类之间的 is-a 关系,分为单继承和多继承。
(多重继承增加了程序的复杂性和含糊性,例如容易导致菱形缺陷,Java 用 interface 更优雅的实现了多继承的功能)
一般建议多用组合少用继承
多态(Polymorphism)
多态特性能提高代码的可扩展性和复用性,同时也是很多设计模式、设计原则、编程技巧的代码实现基础。
多态是指子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。
例子:
代码语言:javascript复制//... 简化版代码 ...
public class Array{
protected Integer[] elements = new Integer[10];
protected int size = 0;
...
public add(Integer e){
// 添加
}
public size()...
public toString()...
private ensureCapacity()... // 扩容
}
public class SortedArray extends Array{
@Override
add(Integer e){
// 排序并添加
}
}
public class Example {
public static void test(Array array) {
array.add(4);
array.add(6);
array.add(2);
System.out.println(array.toString());
}
public static void main(String args[]) {
Array array = new SortedArray();
test(array); // 打印输出 246
}
}
以上是用继承实现了在 test 方法中用子类 SortedArray 替换父类 Array,并执行子类的 add 方法
使用接口类也能实现多态特性,例如 Iterator 迭代器,实现了这个接口的子类可以动态的调用不同的 next () 和 hasNext () 实现
还有 duck-typing ,这是一些动态语言特有的语法机制,如 Python、JavaScript 等。他们可以不需要继承也不需要接口,只要方法名相同就可以实现多态的特性。
这里直接贴王争老师的 Python 代码示例:
代码语言:javascript复制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)
哪些代码实际是面向过程?
滥用 getter、setter 方法
例如:
代码语言:javascript复制public class 购物车{
private List 商品列表;
private int 总数;
private double 总价;
}
如果此时都给定义了 get、set 方法暴露给外部使用, 外部是有可能直接修改 List 内部导致商品列表与其他字段数据不一致的。
总结:如果你把某个属性设置为 private ,但与此同时你又都给他提供了 public 的 get 和 set 方法,那跟直接把属性设置为 public 又有什么区别呢?
滥用全局变量和全局方法
对于这两种类的设计,我们尽量能做到职责单一,定义一些细化的小类,
比如 RedisConstants、FileUtils,而不是定义一个大而全的 Constants 类、Utils 类。
除此之外,如果能将这些类中的属性和方法,划分归并到其他业务类中,那是最好不过的了,能极大地提高类的内聚性和代码的可复用性。
实际上,只包含静态方法不包含任何属性的 Utils 类,是面向过程的编程风格。但是在实际开发中它能解决代码复用的问题,尽量避免滥用就可以了。
定义数据和方法分离的类
一般基于贫血开发模型的开发模式中,VO、BO、Entity 中只会定义数据,不会定义方法,所有操作这些数据的业务逻辑都定义在对应的 Controller 类、Service 类、Repository 类中。
这就是典型的面向过程的编程风格。
后面再详细解释贫血模型和充血模型。
面向对象编程与面向过程编程
我们人的逻辑一般是按流程往下走的,写代码也容易按照这种思路写成面向过程风格。
而面向对象编程正好相反,是一种自底向上的思考方式,将任务分解成一个个小的模块,设计类之间的交互,最后按照流程组装起来,适合复杂程序的开发。
如果开发微小程序或者数据处理相关的,以算法为主,数据为辅,那脚本式的面向过程编程风格就比较合适一些。
不管使用面向过程还是面向对象哪种风格来写代码,我们最终的目的还是写出易维护、易读、易复用、易扩展的高质量代码。
接口与抽象类?
抽象类是为了解决代码复用问题。
抽象类实际上就是类,只不过是一种特殊的类,这种类不能被实例化为对象,只能被子类继承。
接口是为了解决解耦问题,隔离接口和具体的实现,提高代码的扩展性。
相对于抽象类的 is-a 关系来说,接口表示一种 has-a 关系,表示具有某些功能。对于接口,有一个更加形象的叫法,那就是协议(contract)。
基于接口而非实现编程?
软件开发中唯一不变的就是变化
“基于接口而非实现编程” 这条原则的英文描述是:“Program to an interface, not an implementation”。
这里的 “接口” 非特指 Java 里的 interface 接口语法,可以理解为抽象类和接口,也可以称之为 “基于抽象而非实现编程”。
这条原则的设计初衷是,将接口和实现相分离,封装不稳定的实现,暴露稳定的接口。
上游系统面向接口而非实现编程,不依赖不稳定的实现细节,这样当实现发生变化的时候,上游系统的代码基本上不需要做改动,以此来降低代码间的耦合性,提高代码的扩展性。
例子:
代码语言:javascript复制public class 阿里图片存储{
public 创建桶()...
public 生成token()...
public 上传到阿里云()...
public 从阿里云下载()...
}
在编写代码的时候,要遵从 “基于接口而非实现编程” 的原则,具体来讲,我们需要做到下面这 3 点。
- 函数的命名不能暴露任何实现细节。比如例子里的 “阿里云”。
- 封装具体的实现细节。比如关于阿里云上传下载的流程不应该暴露给调用者。
- 为实现类定义抽象的接口。具体的实现类都依赖统一的接口定义,遵从一致的上传功能协议。使用者依赖接口,而不是具体的实现类来编程。
重构后的例子:
代码语言:javascript复制public interface 图片存储{
public 上传()...
public 下载()...
}
public class 阿里图片存储 implements 图片存储{
public 上传(){
创建桶();
生成token();
...
}
public 下载(){
生成token();
...
}
private 创建桶()...
// 这里注意不要把具体的实现搬到接口里,因为可能别的图片存储不需要生成token,所以他是属于阿里独有的实现。
private 生成token()...
}
public class 图像处理任务 {
public void process() {
图片存储 imageStore = new 阿里图片存储();
imagestore.上传(image, BUCKET_NAME);
}
}
多用组合少用继承?
为什么不推荐使用继承?
继承层次过深、过复杂,也会影响到代码的可维护性。
举个例子,定义个鸟的抽象类,然后在里面定义个 fly () 方法,没毛病吧?麻雀、鸽子、乌鸦都继承这个鸟类,也没毛病吧?这时突然来了个企鹅和鸵鸟?他们都不会飞,那可咋整?
重写 fly () 方法?显然不太 OK,违背了迪米特法则,暴露不该暴露的接口给外部,增加了类使用过程中被误用的概率。
把抽象类分为会飞的鸟和不会飞的鸟?那后面还得考虑鸟会不会叫、是否会下蛋等等。继承层次过深、继承关系过于复杂也会影响到代码的可读性和可维护性。
组合相比继承有哪些优势?
继承主要有三个作用:表示 is-a 关系,支持多态特性,代码复用。
而这三个作用都可以通过组合(composition)、接口、委托(delegation) 三个技术手段来达成。
除此之外,利用组合还能解决层次过深、过复杂的继承关系影响代码可维护性的问题。
这里直接贴王争老师的代码示例:
代码语言:javascript复制public interface Flyable {
void fly();
}
public class FlyAbility implements Flyable {
@Override
public void fly() {
//...
}
}
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(); // 委托
}
}
如何判断该用组合还是继承?
如果类之间的继承结构稳定(不会轻易改变),继承层次比较浅(比如,最多有两层继承关系),继承关系不复杂,我们就可以大胆地使用继承。
反之,系统越不稳定,继承层次很深,继承关系复杂,我们就尽量使用组合来替代继承。
除此之外,还有一些设计模式会固定使用继承或者组合。比如,装饰者模式(decorator pattern)、策略模式(strategy pattern)、组合模式(composite pattern)等都使用了组合关系,而模板模式(template pattern)使用了继承关系。
还有一些特殊的场景要求我们必须使用继承。如果你不能改变一个函数的入参类型,而入参又非接口,为了支持多态,只能采用继承来实现。
今天就到这,下一篇是实战篇。