设计原则之旅(二):开闭原则
定义:
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());