面向对象六大原则----里氏替换原则,依赖倒置原则

时间:2022-04-06 15:08:49

Java 中面向对象编程六大原则:

单一职责原则 英文名称是Single Responsibility Principle,简称SRP

开闭原则 英文全称是Open Close Principle,简称OCP

里氏替换原则 英文全称是Liskov Substitution Principle,简称LSP

依赖倒置原则  英文全称是Dependence Inversion Principle,简称DIP

接口隔离原则 英文全称是InterfaceSegregation Principles,简称ISP

迪米特原则 英文全称为Law of Demeter,简称LOD,也称为最少知识原则(Least Knowledge Principle)


构建扩展性更好的系统——里氏替换原则

前面两章讲了单一职责原则开闭原则。里氏替换原则英文全称是Liskov Substitution Principle,简称LSP。 由2008年图灵奖得主、美国第一位计算机科学女博士Barbara Liskov教授和卡内基·梅隆大学Jeannette Wing教授于1994年提出。其严格表述如下:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1代换o2时,程序P的行为没有变化,那么类型S是类型T的子类型。这个定义比较拗口且难以理解,因此我们一般使用它的另一个通俗版定义::所有引用基类的地方必须能透明地使用其子类的对象。

 里氏代换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。例如:我喜欢动物,那我一定喜欢狗,因为狗是动物的子类;但是我喜欢狗,不能据此断定我喜欢动物,因为我并不喜欢老鼠,虽然它也是动物。

继续在前面两章的ImageLoader为例:

面向对象六大原则----里氏替换原则,依赖倒置原则

上图MemoryCache、DiskCache、DoubleCache都实现了ImageCahe接口,ImageLoader类需要一个ImageCahe类,而上面的三种具体实现Cache类都可以替换ImageCache的工作,并且能够保证行为的正确性。所以ImageCache建立了获取缓存图片、保存缓存图片的接口规范,MemoryCache等根据接口规范实现了相应的功能,用户只需要在使用时指定具体的缓存对象就可以动态地替换ImageLoader中的缓存策略。这就使得ImageLoader的缓存系统具有了无限的可能性,也就是保证了可扩展性。

如果当ImageLoader中的setImageCache(ImageCache cache)中的cache对象不能够被子类所替换,那么用户就无法设置不同的缓存对象以及用户也无法自定义自己的缓存实现。里氏替换原则就为这类问题提供了指导原则,也就是建立抽象,通过抽象建立规范,具体的实现在运行时替换掉抽象,保证系统的高扩展性、灵活性。开闭原则和里氏替换原则往往是生死相依、不弃不离的,通过里氏替换来达到对扩展开放,对修改关闭的效果。所以,这两个原则都同时强调了一个OOP的重要特性——抽象,因此,在开发过程中运用抽象是走向代码优化的重要一步。

  在使用里氏代换原则时需要注意如下几个问题:

      (1)子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。根据里氏代换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义,如果一个方法只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法。

      (2)  我们在运用里氏代换原则时,尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。里氏代换原则是开闭原则的具体实现手段之一。


 让项目拥有变化的能力——依赖倒置原则

依赖倒置原则英文全称是Dependence Inversion Principle,简称DIP。抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。赖倒置原则指代了一种特定的解耦形式,使得高层次的模块不依赖于低层次的模块的实现细节的目的,依赖模块被颠倒了。这个概念有点不好理解,这到底是什么意思呢? 
依赖倒置原则的几个关键点:

  • 高层模块不应该依赖低层模块,两者都应该依赖其抽象;
  • 抽象不应该依赖细节;
  • 细节应该依赖抽象。

在Java语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节,其特点就是,可以直接被实例化,也就是可以加上一个关键字 new 产生一个对象。高层模块就是调用端,低层模块就是具体实现类。依赖倒置原则在 Java 语言中的表现就是:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。这又是一个将理论抽象化的实例,其实一句话就可以概括:面向接口编程,或者说是面向抽象编程,这里的抽象指的是接口或者抽象类。面向接口编程是面向对象精髓之一。

如果在类与类直接依赖于细节,那么它们之间就有直接的耦合,当具体实现需要变化时,意味着在这要同时修改依赖者的代码,并且限制了系统的可扩展性。在讲解单一原则时,开始ImageLoader代码如下:

public class ImageLoader {
// 内存缓存 ( 直接依赖于细节 )
MemoryCache mMemoryCache = new MemoryCache();
// 加载图片到ImageView中
public void displayImage(String url, ImageView imageView) {
Bitmap bmp = mMemoryCache.get(url);
if (bmp == null) {
downloadImage(url, imageView);
} else {
imageView.setImageBitmap(bmp);
}
}

public void setImageCache(MemoryCache cache) {
mCache = cache ;
}
// 代码省略
}

ImageLoader直接依赖于MemoryCache,这个MemoryCache是一个具体实现,而不是一个抽象类或者接口。这导致了ImageLoader直接依赖了具体细节,当MemoryCache不能满足ImageLoader而需要被其他缓存实现替换时,此时就必须修改ImageLoader的代码。而后面引入ImageCahe这个接口后,不管怎么修改MemoryCache类,都不用修改其依赖于ImageCahe类的ImageLoader类,最后的代码如下:

public interface ImageCache{
void put(String url,Bitmap bitmap);
Bitmap get(String url);
}

public class ImageLoader {
// 图片缓存
ImageCache mImageCache = null;

public void setImageCache(ImageCache cache){
mImageCache = cache;
}

public void displayImage(final String url, final ImageView imageView) {
if(mImageCache != null){
Bitmap bitmap = mImageCache.get(url);
if(bitmap != null){
imageView.setImageBitmap(bitmap);
return;
}
}
downLoadImage()
}
}

所以依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。为了确保该原则的应用,一个具体类应当只实现接口或抽象类中声明过的方法,而不要给出多余的方法,否则将无法调用到在子类中增加的新方法。


代码github地址:点击打开链接

更多精彩Android技术可以关注我们的微信公众号,扫一扫下方的二维码或搜索关注公共号: Android老鸟
                                                面向对象六大原则----里氏替换原则,依赖倒置原则