面向对象六大原则-里氏替换原则、依赖倒置原则、接口隔离原则

来源:互联网 发布:没有网络又可以玩游戏 编辑:程序博客网 时间:2024/06/08 16:06

这篇我们来学习里氏替换原则、依赖倒置原则、接口隔离原则,这篇是基于上篇的挤出来来进行讲解,如果没有学习上篇的,建议大家去看下实现原理,上篇地址开闭原则
接下来我们先来学习里氏替换原则:

里氏替换原则

1、里氏替换原则英文全称是 Liskov 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() {        //绘制文本    }}

上述的例子中可以看到,window依赖于view,而view定义了一个视图抽象,measure是各个子类共享的方法,子类通过覆写view的draw方法实现具有各自特色的功能,在这里,这个功能就是绘制自身的内容。任何继承自View类的子类都可以设置给show方法,就是所说的里氏替换。通过里氏替换,就可以自定义各式各样、千变万化的view,然后传递给window,window负责组织view,并将view显示到屏幕上。
里氏替换原则的核心原理是抽象,抽象又依赖于继承这个特性,它具有一下优点:

  1. 代码重用,减少创建类的成本,每个子类都拥有父类的方法和属性。
  2. 子类与父类基本相似,但又与父类有所区别;
  3. 提高代码的 。

继承的缺点:
1. 继承是侵入性的,只要继承就必须拥有父类的所有属性和方法;
2. 可能造成子类代码冗余、灵活性降低,因为子类必须拥有父类的属性和方法。


事物总是具有两面性,如何权衡利弊都是需要根据具体情况来做出选择并加以处理。里氏替换原则指导我们构建扩展性更好的软件系统。
开闭原则和里氏替换原则往往是生死相依、不弃不离的,通过里氏替换来达到对扩展开放,对修改关闭的效果。

依赖倒置原则

依赖倒置原则英文全称是Dependence Inversion Principle,缩写是DIP。
依赖倒置原则指代了一种特定的解耦形式,使得高层次的模块不依赖于低层次的模块的实现细节的目的,依赖模块被颠倒了。
依赖倒置原则有以下几个关键点:
1. 高层模块不应该依赖底层模块,两者都应该依赖其抽象;
2. 抽象不应该依赖细节;
3. 细节应该依赖抽象。


在java语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节,其特点就是,可以直接被实例化,也就是可以加上一个关键字new产生一个对象。高层模块就是调用端,底层模块就是具体实现类。
依赖倒置原则在java语言中表现就是:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。一句话概括:面向接口编程,或者说是面向抽象编程,这里的抽象指的是接口或者抽象类。
如果类与类直接依赖于细节,那么它们之间就有直接耦合,当具体实现需要变化时,意味着要同时修改依赖着的代码,这就限制了系统的可扩展性。大家可以回顾下ImageLoader相关的实现,如果ImageLoader直接依赖于MemoryCache,这个MemoryCache是一个具体实现,而不是一个抽象类或者接口。这导致了ImageLoader直接依赖了具体细节,当MemoryCache不能满足ImageLoader而需要被其他缓存实现替换时,此时就必须修改ImageLoader的代码,例如:

public class ImageLoader {    //图片缓存,直接依赖于细节    MemoryCache memoryCache = new MemoryCache();    //线程池,线程数量为CPU的数量    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());    public void setImageCache(MemoryCache cache){        memoryCache = cache;    }    public void displayImage(final String url, final ImageView imageView){        //判断使用哪种缓存        Bitmap bitmap = memoryCache.get(url);        if(bitmap!=null){            imageView.setImageBitmap(bitmap);            return;        }        //没有缓存,则提交给线程下载        imageView.setTag(url);        mExecutorService.submit(new Runnable() {            @Override            public void run() {                Bitmap bitmap = downloadImage(url);                if(bitmap==null){                    return;                }                if(imageView.getTag().equals(url)){                    imageView.setImageBitmap(bitmap);                }                memoryCache.put(url,bitmap);            }        });    }}

随着产品升级,用户发现MemoryCache已经不能满足需求,另外用户自定义还必须继承MemoryCache,在命名上的限制,用户体验也不好,并且在ImageLoader中也违反了开闭原则,灵活性也比较差。所有做了以下修改:

//ImageCache缓存抽象public interface ImageCache {    public void put(String url, Bitmap bitmap);    public Bitmap get(String url);}
public class ImageLoader {    //图片缓存,依赖于抽象,并且有一个默认的实现    ImageCache mImageCache = new MemoryCache();    //线程池,线程数量为CPU的数量    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());    //设置缓存,依赖于抽象        public void setImageCache(ImageCache cache){        mImageCache = cache;    }    public void displayImage(final String url, final ImageView imageView){        //判断使用哪种缓存        Bitmap bitmap = mImageCache.get(url);        if(bitmap!=null){            imageView.setImageBitmap(bitmap);            return;        }        //没有缓存,则提交给线程下载        imageView.setTag(url);        mExecutorService.submit(new Runnable() {            @Override            public void run() {                Bitmap bitmap = downloadImage(url);                if(bitmap==null){                    return;                }                if(imageView.getTag().equals(url)){                    imageView.setImageBitmap(bitmap);                }                mImageCache.put(url,bitmap);            }        });    }    public Bitmap downloadImage(String imageUrl){        Bitmap bitmap = null;        try {            URL url = new URL(imageUrl);            final HttpURLConnection conn = (HttpURLConnection) url.openConnection();            bitmap = BitmapFactory.decodeStream(conn.getInputStream());            conn.disconnect();        }catch (Exception e){            e.printStackTrace();        }        return bitmap;    }}

这里我们建立了ImageCache抽象,并且让ImageLoader依赖于抽象而不是具体细节。当需求发生变化时,只需要实现ImageCache类或者继承其他已有的ImageCache子类完成相应的缓存功能,然后将具体的实现注入到ImageLoader即可实现缓存功能的替换,这就保证了缓存系统的高可扩展性,有了拥抱变化的能力,这就是依赖倒置原则。

接口隔离原则

接口隔离原则英文全称是Interface Segregation Principles,缩写是ISP。
ISP的定义是:客户端不应该依赖它不需要的接口。另一种定义是:类间的依赖关系应该建立在最小的接口上。接口隔离原则将非常庞大、臃肿的接口拆分成更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。接口隔离原则的目的是系统解开耦合,从而容易重构、更改和重新部署。
接下来我们还是用最熟悉的代码来看:

 public void put(String url, Bitmap bmp){        FileOutputStream fileOutputStream = null;        try {            fileOutputStream = new FileOutputStream(cacheDir+url);            bmp.compress(Bitmap.CompressFormat.PNG,100,fileOutputStream);        }catch (Exception e){            e.printStackTrace();        }finally {            if(fileOutputStream!=null){                try {                    fileOutputStream.close();                } catch (IOException e) {                    e.printStackTrace();                }            }        }    }

我们可以看到这段代码的可读性非常差,各种try……catch嵌套都是些简单的代码,但是会严重影响代码的可读性,并且多层级的大括号很容易将代码写到错误的层级中。
我们可能知道Java中有一个Closeable接口,该接口标识了一个可关闭的对象,它只有一个close方法。

//java接口package java.lang;public interface AutoCloseable {    void close() throws Exception;}package java.io;import java.io.IOException;public interface Closeable extends AutoCloseable {    void close() throws IOException;}

既然它是实现了Closeable 接口,为了代码的可读性,我们还是建立一个方法来同意关闭这些对象。

public class CloseUtils {    private CloseUtils(){    }    /**     * 关闭Closeable对象     */    public static void closeQuietly(Closeable closeable){        if (null!= closeable){            try {                closeable.close();            } catch (IOException e) {                e.printStackTrace();            }        }    }}
//修改后的方法 public void put(String url, Bitmap bmp){        FileOutputStream fileOutputStream = null;        try {            fileOutputStream = new FileOutputStream(cacheDir+url);            bmp.compress(Bitmap.CompressFormat.PNG,100,fileOutputStream);        }catch (Exception e){            e.printStackTrace();        }finally {            CloseUtils.closeQuietly(fileOutputStream);        }    }

大家可以看到代码简洁了很多,CloseUtils.closeQuietly()这个方法可以运用到各类可关闭的对象中,保证了代码的重用性。CloseUtils的closeQuietly方法的基本原理就是依赖于Closeable抽象而不是具体实现,这就符合依赖倒置原则,并且建立在最小化依赖原则的基础上,它只需要知道这个对象是可关闭的,其他一概不关心,也就是接口隔离原则。其实前面提到的ImageLoader中的ImageCache就是接口隔离原则的运用。

这里就先给大家介绍到这里,第一遍看不懂的就多读几次,带着思考去分析下,可以学到不少精髓,欢迎大家提建议。
下一篇将为大家介绍迪米特原则

0 0