设计原则之旅 (二):开闭原则

625 阅读13分钟
原文链接: www.jcodecraeer.com

定义:

Softeware entities like classes,modules and functions should be open for extension but closed for modifications.
(一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。)

问题由来:

在产品更新迭代阶段,随着需求的不断变化,原有的代码不能满足新的需求,需要对原有代码进行修改,在修改的过程中,可能会对原有代码引入新的错误,从而增加系统风险与维护成本。

解决方案:

遵循开闭原则:在实体类的设计阶段,对可能变化的需求行为提取抽象接口,实体类只依赖于抽象接口,具体行为方式的实现对外开放,由调用方去实现。当需求变化的时候,只需扩展新的功能,而不需要修改原有的代码,做到对 扩展开放对修改关闭

举例说明:

需求: 实现一个图片加载器,用于加载网络图片显示在 ImageView 中,并且具备缓存功能。


示例代码:

图片加载器:

/**
     * 图片加载器
     * Created by speedy on 2017/4/10.
     */
    public class ImageLoader {
     
        private static final String TAG = "ImageLoader";
     
        private ExecutorService mExecutorService;
     
        //图片缓存
        private LruCache<String,Bitmap> mImageCache;
     
        private static ImageLoader2 mImageLoader;
     
        public static synchronized ImageLoader2 getImageLoader(){
            if(mImageLoader == null){
                mImageLoader = new ImageLoader2();
            }
            return mImageLoader;
        }
     
        private ImageLoader2(){
            initThreadPool();
            initCache();
        }
     
     
        //初始化线程池
        private void initThreadPool() {
     
            //线程池,线程数目为 CPU 的数目
            mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
        }
     
        //初始化缓存
        private void initCache() {
     
            //计算可使用最大的内存
            final int maxMenory = (int) (Runtime.getRuntime().maxMemory()/1024);
     
            //取四分之一作为缓存
            final int cacheSize = maxMenory / 4;
     
            mImageCache = new LruCache<String,Bitmap>(cacheSize){
                @Override
                protected int sizeOf(String key, Bitmap value) {
                    return value.getRowBytes()* value.getHeight() / 1024;
                }
            };
        }
     
        //加入缓存
        public void put(String url,Bitmap bitmap){
            mImageCache.put(url,bitmap);
        }
     
        //获取缓存图片
        public Bitmap get(String url){
            return mImageCache.get(url);
        }
     
        //显示图片
        public void diaplayImage(@NonNull final String url,@NonNull final ImageView imageView){
            imageView.setTag(url);
            mExecutorService.submit(new Runnable() {
                @Override
                public void run() {
                    Bitmap bitmap = get(url);//获取缓存图片
                    if(bitmap == null){
                        bitmap = downloadImage(url);
                        if(bitmap == null){
                            Log.e(TAG, "图片下载失败: url = "+ url );
                            return;//图片下载失败,
                        }
                    }
     
                    if(url.equals(imageView.getTag())){
                        bindToImageView(bitmap,imageView);//显示在控件上
                    }
                    //缓存图片
                    put(url,bitmap);
                }
            });
        }
     
        //下载网络图片
        private 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;
        }
     
        //将 Bitmap 显示在 ImageView 中
        private void bindToImageView(final Bitmap bitmap,final ImageView imageView){
            imageView.post(new Runnable() {
                @Override
                public void run() {
                    imageView.setImageBitmap(bitmap);
                }
            });
        }
    }

测试 Activity

public class ImageTestActivity extends Activity {
     
        private ImageView imageView;    
        private Button button;    
        
        @Override
        protected void onCreate(Bundle savedInstanceState) {        
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_image_test);
     
            imageView = (ImageView) findViewById(R.id.imageView);
            button = (Button) findViewById(R.id.button);
     
            button.setOnClickListener(new View.OnClickListener() {           
                 @Override
                public void onClick(View v) {
                    String url = "http://avatar.csdn.net/6/B/9/1_easyer2012.jpg";
                    ImageLoader loader = ImageLoader.getImageLoader();
                    loader.diaplayImage(url,imageView);
                }
            });
        }
    }

布局文件

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center"
        >
     
        <ImageView        android:id="@+id/imageView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_launcher"
            />
     
        <Button        android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="50dp"
            android:text="开始加载网络图片"/></LinearLayout>

示例代码分析

ImageLoader 类的设计,初看起来,好像没什么太多问题,“ 完全 ”满足需求。 但是如果需求变化,就会发现,ImageLoader 类的设计的扩展性非常差,假如用户需要修改缓存策略,则 ImageLoader 类就需要修改源代码,或者,用户需要对加载后的图片做圆角处理,同样需要修改 ImageLoader 类。

问题分析

ImageLoader 类扩展性差的主要原因是,ImageLoader类的设计违背的两大设计原则:

  • 违背 单一职责原则
    ImageLoader 类既负责了图片的加载功能,又负责图片缓存功能的具体实现。

  • 违背 开闭原则
    ImageLoader 类的图片缓存功能功能是属于变化的行为,不应该将具体实现写在ImageLoader 类, 应该提取抽象行为,ImageLoader 类只依赖抽象接口,具体实现交给外界实现,便于扩展,做到对扩展开放,对修改关闭。


优化后代码:

说明:ImageLoader 类的设计存在许多需要待优化的地方,本文讲解重点是开闭原则,所以,只是以缓存的优化作为一个切入点作为例子讲解。

1 , 提取图片缓存接口

public interface ImageCache {    //加入缓存
        public void put(String url,Bitmap bitmap);    //获取缓存图片
        public Bitmap get(String url);
     
    }

2 , 实现具体缓存方式

/**
     * 内存缓存图片
     * Created by Speedy on 2017/4/10.
     */
    public class MenoryCache implements ImageCache {
     
        private LruCache<String,Bitmap> mImageCache;
     
        public MenoryCache(){
     
            //计算可使用最大的内存
            final int maxMenory = (int) (Runtime.getRuntime().maxMemory()/1024);
     
            //取四分之一作为缓存
            final int cacheSize = maxMenory / 4;
     
            mImageCache = new LruCache<String,Bitmap>(cacheSize){
                @Override
                protected int sizeOf(String key, Bitmap value) {
                    return value.getRowBytes()* value.getHeight() / 1024;
                }
            };
        }
     
        //加入缓存
        public void put(String url,Bitmap bitmap){
            mImageCache.put(url,bitmap);
        }
     
        //获取缓存图片
        public Bitmap get(String url){
            return mImageCache.get(url);
        }
    }

3 , ImageLoader 与 ImageCache  建立依赖关系

/**
     * 图片加载器
     * Created by Speedy on 2017/4/10.
     */
    public class ImageLoader {
     
        private static final String TAG = "ImageLoader";
     
        private ExecutorService mExecutorService;
     
        //缓存接口
        private ImageCache mImageCache;
     
        private static ImageLoader mImageLoader;
     
        public static synchronized ImageLoader getImageLoader(){
            if(mImageLoader == null){
                mImageLoader = new ImageLoader();
            }
            return mImageLoader;
        }
     
        private ImageLoader(){
            initThreadPool();
            initCache();
        }
     
     
        //初始化线程池
        private void initThreadPool() {
     
            //线程池,线程数目为 CPU 的数目
            mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
        }
     
        //初始化缓存
        private void initCache() {
            mImageCache = new MenoryCache();
        }
     
        //提供扩展
        public void setImageCache(ImageCache imageCache) {
            this.mImageCache = imageCache;
        }
     
        //显示图片
        public void diaplayImage(@NonNull final String url,@NonNull final ImageView imageView){
            imageView.setTag(url);
            mExecutorService.submit(new Runnable() {
                @Override
                public void run() {
                    Bitmap bitmap = mImageCache.get(url);//获取缓存图片
                    if(bitmap == null){
                        bitmap = downloadImage(url);
                        if(bitmap == null){
                            Log.e(TAG, "图片下载失败: url = "+ url );
                            return;//图片下载失败,
                        }
                    }
     
                    if(url.equals(imageView.getTag())){
                        bindToImageView(bitmap,imageView);//显示在控件上
                    }
                    //缓存图片
                    mImageCache.put(url,bitmap);
                }
            });
        }
     
        //下载网络图片
        private 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;
        }
     
        //将 Bitmap 显示在 ImageView 中
        private void bindToImageView(final Bitmap bitmap,final ImageView imageView){
            imageView.post(new Runnable() {
                @Override
                public void run() {
                    imageView.setImageBitmap(bitmap);
                }
            });
        }
    }

4,后期如果客户缓存策略有变,只需要实现 ImageCache 接口扩展功能,而 ImageLoader 这无须做任何修改,从而做到了对扩展开放,对修改关闭,及满足了开闭原则。例如:加入后期客户需要实现 SD 卡缓存功能 。

/**
     * SD 卡缓存
     * Created by Speedy on 2017/4/10.
     */
    public class DiskCache implements ImageCache {
     
        @Override
        public void put(String url, Bitmap bitmap) {
            //讲图片保持只SD 卡中
        }
     
        @Override
        public Bitmap get(String url) {
            //从SD卡中获取缓存图片
            return null;
        }
    }

5,动态修改缓存策略

  ImageLoader loader = ImageLoader.getImageLoader();
      loader.setImageCache(new DiskCache());

欢迎关注微信公众号:“ Android 之旅 ”,一起学习 、交流 。

邀请二维码.png