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

定义:

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 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() {
        //计算可使用最大的内存
        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