声明:本系列博客整理来源于《Android源码设计模式解析与实战》,仅作为个人学习总结记录,任何组织和个人不得转载进行商业活动!
里氏替换原则(Lisvok Substitution Principle,LSP)
LSP的第一种定义是:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S使类型T的子类型。不太好理解,里氏替换原则第二种定义:所有引用基类的地方必须能透明的使用其子类的对象。
面向对象的语言的三大特点是继承、封装、多态,里氏替换原则就是依赖于继承、多态这两大特性。里氏替换原则简单来说就是,所有引用基类的地方必须能透明的使用其子类的对象。通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。其实最终总结就两个字:抽象。
在Android中,Window与View的关系,如下图以下:
具体的代码实现:
/**
* 窗口类
*/
public class Window {
public void show(View child){
child.draw();
}
}
/**
* 建立视图抽象,测量视图的宽高为公用代码,绘制实现交给具体的子类
*/
public abstract class View {
public abstract void draw();
public void measure(int width,int height){
//测量视图大小
}
}
/**
* 按钮类的具体实现
*/
public class Button extends View {
@Override
public void draw() {
//绘制按钮
}
}
/**
* TextView的具体实现
*/
public class TextView extends View {
@Override
public void draw() {
//绘制文本
}
}
有上述示例可得出,在Android系统中,Window依赖于View,而View定义了一个视图抽象,measure()是各类共享的方法,子类通过重写View的draw方法实现具有各自特色的功能,在这里,这个功能就是绘制自身的内容。任何继承自View类的子类都可以设置draw方法,就是说的里氏替换。通过里氏替换,就可以自定义各式各样、千变万化的View,然后传递给Window,Window负责组织View,并且将View显示在屏幕上。
里氏替换原则的核心原理是抽象,抽象又依赖于继承这个特性,在OOP当中,继承的优缺点都相当明显。
优点有以下几点:
(1)代码重用,减少创建类的成本,每个子类都拥有父类的属性和方法;
(2)子类和父类基本相似,但又和父类有所区别;
(3)提高代码的可扩展性。
继承的缺点:
(1)继承是侵入性的,只要继承就必须拥有父类的属性和方法;
(2)可能造成子类代码冗余、灵活性降低,因为子类必须拥有父类的属性和方法。
事物总是具有两面性,如何权衡利与弊都是需要根据具体情况来作出选择并加以处理。里氏替换指导我们构建扩建性更好的软件系统,还是以ImageLoader为例。
如上图所示,很好的诠释了里氏替换原则,即MemoryCache、DiskCache、DoubleCache都可以替换ImageLoader的工作,并且能够保证行为的正确性。ImageLoader建立了获取缓存图片、保存缓存图片的接口规范,MemoryCache等根据接口规范实现了相应的功能,用户只需要在使用时指定具体的缓存对象就可以动态的替换ImageLoader中的缓存策略。这就使得ImageLoader的缓存系统具有了无限的可能性,也就是保证了可扩展性。
想象一种情况,当ImageLoader中的setImageCache(ImageCache cache)中的cache对象不能够被子类所替换,那么用户该如何设置不同的缓存对象,以及用户如何自定义自己的缓存实现,通过useDiskCache方法吗?显然不是的,里氏替换原则就为这类问题提供了指导原则,也就是建立抽象,通过抽象建立规范,具体的实现在运行时替换掉抽象,保证系统的扩展性、灵活性。开闭原则和里氏替换原则往往是生死相依、不弃不离的,通过里氏替换来达到对扩展开放,对修改关闭的效果。然而,这两个原则都同时强调了OOP的重要特性 --- 抽象。因此,在开发过程中运用抽象是走向代码优化的重要一步。