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

时间:2022-01-15 14:41:03

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老鸟
                                                面向对象六大原则----开闭原则