面向对象六大原则----开闭原则

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)


让程序更稳定、更灵活——开闭原则

开闭原则的英文全称是Open Close Principle,简称OCP,它是Java世界里最基础的设计原则,它指导我们如何建立一个稳定的、灵活的系统。开闭原则的定义是:软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是,对于修改是封闭的。软件开发过程中,最不会变化的就是变化本身。产品需要不断地升级、维护,没有一个产品从第一版本开发完就再没有变化了,如果因为变化、升级和维护等原因需要对软件原有代码进行修改时,这就可能会将错误引入原本已经经过测试的旧代码中,破坏原有系统。因此,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。那么如何确保原有软件模块的正确性,以及尽量少地影响原有模块,答案就是尽量遵守本章要讲述的开闭原则

继续上一章对ImageLoader进行分析,我们在第一轮重构之后的ImageLoader使用了单一原则,让代码职责单一、结构清晰,但是我们在使用中发现我们只实现了内存缓存图片,通过内存缓存解决了每次从网络加载图片的问题,但是,Android应用的内存很有限,且具有易失性,即当app重新启动之后,原来已经加载过的图片将会丢失,这样重启之后就又需要重新下载。这又会导致加载缓慢、耗费用户流量的问题。所以我们应该引入SD卡缓存,这样下载过的图片就会缓存到本地,即使重启应用也不需要重新下载了。 

根据上一章的单一原则,我们写一个DiskCache.java类,将图片缓存到SD卡中:

public class DiskCache {
    private String mCacheDirName = "imageCache";
    private String mCacheDirPath;
    private File mCachDir = null;

    public DiskCache(Context context){
        initDiskCacheDir(context);
    }

    //初始化SD卡缓存的目录位置
    public void initDiskCacheDir(Context context) {
        String cachePath;
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                || !Environment.isExternalStorageRemovable()) {
            //获取外部存储卡的位置,/sdcard/Android/data/xxxx(应用包名)/cache
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            //如果无法获取外部存储卡的位置,则使用应用自己的私用空间,/data/data/xxxx(应用包名)/cache
            cachePath = context.getCacheDir().getPath();
        }
        mCacheDirPath = cachePath+File.separator+mCacheDirName;
        mCachDir = new File(mCacheDirPath);
        if(!mCachDir.exists()){
            mCachDir.mkdirs();
        }
    }
    //将下载的图片流缓存到sd卡里
    public void put(String key, InputStream inputStream){
        if(!mCachDir.exists()){
            return;
        }
        //对传入的key值进行MD5处理
        String savedFilePath = mCacheDirPath+File.separator+hashKeyForDisk(key);
        try {
            FileOutputStream f = new FileOutputStream(new File(savedFilePath));
            BufferedInputStream in = new BufferedInputStream(inputStream);
            BufferedOutputStream out = new BufferedOutputStream(f);
            int n;
            byte[] buf = new byte[4096];
            while ((n = in.read(buf)) != -1) {
                out.write(buf,0,n);
            }
            in.close();
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //根据传入的key值返回缓存的文件流
    public InputStream get(String key){
        if(!mCachDir.exists()){
            return null;
        }
        String savedFilePath = mCacheDirPath+File.separator+hashKeyForDisk(key);
        File bitmapFile = new File(savedFilePath);
        if(bitmapFile.exists()){
            try {
                InputStream in = new FileInputStream(bitmapFile);
                return in;
            } catch (FileNotFoundException e) {
                e.printStackTrace();
                return null;
            }
        }
        return null;
    }

    /**
     * 将Key用MD5码编码
     * @param key
     * @return 返回MD5码后的编码
     */
    public String hashKeyForDisk(String key) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(key.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(key.hashCode());
        }
        return cacheKey;
    }

    private String bytesToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(0xFF & bytes[i]);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }
}

然后我们修改ImageLoader类加入我们的DiskCache:

public void setDiskCahe(Context context){
        mDiskCache = new DiskCache(context);
    }
再修改下载和显示逻辑,加入判断SD卡缓存里有没有缓存过要显示的图片,有缓存则直接取缓存显示,避免再下载

public  void displayImage(final String url, final ImageView imageView) {
        imageView.setTag(url);
        //先从cache中取图片
        if(mMemImageCache.get(url)!=null){
            imageView.setImageBitmap(mMemImageCache.get(url));
            return;
        }
        if(mDiskCache != null){
            InputStream in = mDiskCache.get(url);
            if(in != null){
                Bitmap bitmap = BitmapFactory.decodeStream(in);
                imageView.setImageBitmap(bitmap);
                return;
            }
        }
        mExecutorService.submit(new Runnable() {
            @Override
            public  void run() {
                Bitmap bitmap = downloadImage(url);
                if (bitmap == null) {
                    return;
                }
                if (imageView.getTag().equals(url)) {
                    Message msg = mHandler.obtainMessage();
                    imageView.setTag(bitmap);
                    msg.obj = imageView;
                    mHandler.sendMessage(msg);
                }
                mMemImageCache.put(url, bitmap);
            }
        });
    }

    public  Bitmap downloadImage(String imageUrl) {
        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            HttpURLConnection conn = (HttpURLConnection)url.openConnection();
            conn.setDoInput(true); //允许输入流,即允许下载
            conn.setUseCaches(false); //不使用缓冲
            conn.setRequestMethod("GET"); //使用get请求
            InputStream is = conn.getInputStream();   //获取输入流,此时才真正建立链接
            if(mDiskCache!=null){  //缓存到硬盘上
                mDiskCache.put(imageUrl,is);
            }
            bitmap = BitmapFactory.decodeStream(mDiskCache.get(imageUrl));
            conn.disconnect();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return bitmap;
    }

通过上面的修改,现在我们只需在使用ImageLoader时,调用一下setDiskCahe(Context context)方法就能使用SD卡缓存图片,这是不是很方便,但是我们在添加一个DiskCache缓存时修改了许多ImageLoader代码,如果我们需要更多缓存方式或更多缓存策略,比如只使用内存缓存,或内存缓存和SD卡缓存同时使用等等需求,每次加新的缓存方法时都要修改原来的代码,这样很可能会引入Bug,而且会使原来的代码逻辑变得越来越复杂,按照上面这样的方法实现,用户也不能自定义缓存实现。

软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是对于修改是封闭的,这就是开放-关闭原则。也就是说,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。如何实现ImageLoader类需要的cache能扩展呢?我们可以定义一个ImageCache接口:

public interface ImageCache {
    void put(String key, Bitmap bitmap);
    Bitmap get(String key);
}
然后所有的具体Cache类实现这个接口,而我们的ImageLoader类只需操作这个ImageCache类就可以了.这就实现了用抽象类或接口来实现原功能的扩展。下面看一下UML图:


如上图,所有具体的Cache类实现了ImageCahe这个接口,而ImageLoader类只依赖于ImageCache类,这样就达到了可以不修改原有代码,而可以无限通过实现ImageCache这个接口来扩展ImageLoader类的缓存图片功能了。
于是ImageLoader类里增加设置ImageCache:

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

通过setImageCache(ImageCache cache)方法注入不同的缓存实现,这样不仅能够使ImageLoader更简单、健壮,也使得ImageLoader的可扩展性、灵活性更高。MemoryCache、DiskCache、DoubleCache缓存图片的具体实现完全不一样,但是,它们的一个特点是都实现了ImageCache接口。当用户需要自定义实现缓存策略时,只需要新建一个实现ImageCache接口的类,然后构造该类的对象,并且通setImageCache(ImageCache cache)注入到ImageLoader中,这样ImageLoader就实现了变化万千的缓存策略,而扩展这些缓存策略并不会导致ImageLoader类的修改。这就是开闭原则!

开闭原则指导我们,当软件需要变化时,应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。这里的“应该尽量”4个字说明OCP原则并不是说绝对不可以修改原始类的,当我们嗅到原来的代码“腐化气味”时,应该尽早地重构,以使得代码恢复到正常的“进化”轨道,而不是通过继承等方式添加新的实现,这会导致类型的膨胀以及历史遗留代码的冗余。当然我们在开发过程中也没有那么理想化的状况,完全地不用修改原来的代码,因此,在开发过程中需要自己结合具体情况进行考量,是通过修改旧代码还是通过继承使得软件系统更稳定、更灵活,在保证去除“代码腐化”的同时,也保证原有模块的正确性。

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

更多精彩Android技术可以关注我们的微信公众号,扫一扫下方的二维码或搜索关注公共号: Android老鸟

                                                





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章