读书笔记——面向对象的六大原则
来源:互联网 发布:电子设计软件有哪些 编辑:程序博客网 时间:2024/06/16 16:31
本文的大部分内容是基于《Android源码设计模式解析与实战》这本书。
1、单一职责原则
“就一个类而言,应该仅有一个引起它变化的原因。” 简单说来,两个完全不一样的功能就不应该放在一个类中。一个类中,应该是一组相关性很高的函数、数据的封装。工程师可以不断的审视自己的代码,根据具体业务、功能对类进行相对的拆分,这是程序员优化代码的第一步。
小明编写一个ImageLoader图片加载类,却把图片加载,图片缓存等功能都写在一起。那么这个ImageLoader的耦合性就非常高了,随着功能的增加,ImageLoader类会变得越来越庞大,代码也会变得越来越复杂,从而导致这个图片加载类变得脆弱。
当小明学习了单一职责原则后,对ImageLoader做了拆分:ImageLoader类只用来加载图片,并添加了一个ImageCache类用来处理图片缓存。这样ImageLoader的代码量变小了,职责也清晰了;当与缓存相关的逻辑要修改的时候,不修改ImageLoader类,二图片加载逻辑需要修改时,也不会影响到缓存处理逻辑。
2、开闭原则
“软件中的对象(类、模块、函数)应该对于扩展是开放的,但是对于修改是封闭的。”也就是说,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码。
小明发现了他的ImageLoader类出现了一个问题:由于他只做了内存缓存,当程序重新启动后,原来加载过的图片丢失了,又要重新从网络加载。这时候小明就对他的图片缓存逻辑进行修改 ——缓存优先使用内存缓存,如果内存缓存没有图片再使用SD卡缓存,如果SD卡中也没有图片最后才从网络获取。于是小明新增了一个DiskCache类用于SD卡缓存,新增一个DoubleCache类实现双缓存。相应的对ImageLoader类展示图片的逻辑进行了修改。
但是,以后有新的缓存策略的话,还是要修改原来的代码,这样很可能引入bug,而且会使原来的代码逻辑变得越来越复杂。当小明学习了开闭原则后,将ImageLoader进行了一次重构,具体代码如下:
public class ImageLoader { //图片缓存 ImageCache mImageCache = new MemoryCache(); //线程池,线程数量为CPU的数量 ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); //注入缓存实现 public void setImageCache(ImageCache cache) { mImageCache = cache; } public void displayImage(String imageUrl, ImageView imageView) { Bitmap bitmap = mImageCache.get(imageUrl); if (bitmap != null) { imageView.setImageBitmap(bitmap); return; } //图片没缓存,提交到线程池下载 submitLoadRequest(imageUrl, imageView); } private void submitLoadRequest(final String url, final ImageView view) { view.setTag(url); mExecutorService.submit(new Runnable() { @Override public void run() { Bitmap bitmap = downloadImage(url); if (bitmap == null) { return; } if (view.getTag().equals(url)) { view.setImageBitmap(bitmap); } 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; }}
public interface ImageCache { Bitmap get(String url); void put( String url,Bitmap map);}
public class MemoryCache implements ImageCache { private LruCache<String,Bitmap> mMemoryCache; public MemoryCache(){ //计算最大可用内存 final int maxMemory=(int)(Runtime.getRuntime().maxMemory()/1024); //取1/4 final int cacheSize=maxMemory/4; mMemoryCache=new LruCache<String, Bitmap>(cacheSize){ //必须重写此方法,来测量Bitmap的大小 @Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getRowBytes()*bitmap.getHeight()/1024; } }; } @Override public Bitmap get(String url) { return mMemoryCache.get(url); } @Override public void put(String url,Bitmap map) { mMemoryCache.put(url,map); }}
public class DiskCache implements ImageCache { @Override public Bitmap get(String url) { return null;//从本地文件中获取该图片 } @Override public void put(String url, Bitmap map) { //Bitmap写入文件中 }}
public class DoubleCache implements ImageCache { ImageCache mMemoryCache=new MemoryCache(); ImageCache mDiskCache=new DiskCache(); //先从内存缓存中获取图片,如果没有,再从SD卡中获取 @Override public Bitmap get(String url) { Bitmap bitmap=mMemoryCache.get(url); if(bitmap==null){ bitmap=mDiskCache.get(url); } return bitmap; } @Override public void put(String url, Bitmap map) { mMemoryCache.put(url,map); mDiskCache.put(url,map); }}
细心的读者可能注意到了,ImageLoader类中添加了一个SetImageCache(ImageCache cache)函数,使用者可以通过该函数设置缓存,也就是通常说的依赖注入。下面看下简单的使用:
ImageLoader imageLoader=new ImageLoader(); imageLoader.setImageCache(new MemoryCache()); imageLoader.setImageCache(new DiskCache()); imageLoader.setImageCache(new DoubleCache()); //使用自定义的图片缓存 imageLoader.setImageCache(new ImageCache() { @Override public Bitmap get(String url) { return null; } @Override public void put(String url, Bitmap map) { } });
开闭原则指导我们,当软件需要变化的时,应该尽量通过扩展的方式来实现变化,而不是通过修改原来的代码。
遵循开闭原则的重要手段是通过抽象。这里的“应该尽量”说明开闭原则并不是说绝对不能去修改原来的代码。当我们嗅到原来代码“腐朽的味道”时,应该尽早的重构。
3、里氏替换原则
所有引用基类的地方都能透明地使用其子类对象。通俗的讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何的错误或异常。
里氏替换原则的核心原理就是抽象,抽象又依赖于继承这个特性,在OOP当中,继承的优缺点都相当的明显。优点如下:
1. 代码重用,减少创建类的成本,每个子类都有父类的方法和属性。
2. 子类与父类基本相似,又与父类有所不同
3. 提高代码的可扩展性
继承的缺点:
1. 继承是入侵性的,只要继承就必须拥有父类的所有属性和方法
2. 可能造成代码冗余,灵活性降低,因为子类必须要有父类的方法和属性
在开闭原则的代码中很好的反应了里氏替换原则,即MemoryCache,DiskCache,DoubleCache都可以替换ImageCache工作,并且能保证行为的正确性。
开闭原则和里氏替换原则往往是生死相依的,通过里氏替换原则来达到对扩展开放,对修改关闭的效果。然而这两个原则都强调了OOP的重要特性——抽象,因此,在开发中运用抽象是走向代码优化的重要一步。
4、依赖倒置原则
模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。
依赖倒置原则有以下几个关键点:
- 高层模块不应该依赖低层模块,两者都应该依赖其抽象;
- 抽象不应该依赖细节;
- 细节应该依赖抽象。
在Java中,抽象就是指接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节。高层模块就是调用端,低层模块就是具体实现类。
如果类与类之间直接依赖于细节,那么它们之间有直接的耦合,当具体实现需要改变的时候也要同时修改依赖者的代码,这限制了系统的扩展性。
假如在ImageLoader中的图片缓存直接依赖于MemoryCache,这个MemoryCache是一个具体实现,不是一个接口或抽象类。这导致了ImageLoader直接依赖了具体的细节,当MemoryCache不能满足ImageLoader而需要被其他缓存实现替换时,此时就必须修改ImageLoader的代码,也违背了开闭原则了。例如:
//内存缓存,直接依赖细节 MemoryCache mMemoryCache=new MemoryCache(); public void displayImage(String url,ImageView img){ Bitmap bitmap=mMemoryCache.get(url); if(bitmap==null){ downloadImage(url,img); }else{ img.setImageBitmap(bitmap); } } public void setImageCache(MemoryCache cache){ mMemoryCache=cache; }}
随着产品的升级,用户发现MemoryCache已经不能满足需求了,用户需要小明的ImageLoader可以将图片同事缓存在SD卡和内存中,或者让用户可以自定义缓存。此时,我们的MemoryCache这个类名不仅不能表达内存缓存和SD卡缓存的意义,也不能满足功能。另外,用户需要自定义缓存策略时还必须继承MemoryCache,而用户的缓存实现可不一定需要内存缓存。所以,需要重构!
ImageCache抽象类:
public interface ImageCache { Bitmap get(String url); void put( String url,Bitmap map);}
内存缓存类:
public class MemoryCache implements ImageCache { private LruCache<String,Bitmap> mMemoryCache; public MemoryCache(){ //计算最大可用内存 final int maxMemory=(int)(Runtime.getRuntime().maxMemory()/1024); //取1/4 final int cacheSize=maxMemory/4; mMemoryCache=new LruCache<String, Bitmap>(cacheSize){ //必须重写此方法,来测量Bitmap的大小 @Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getRowBytes()*bitmap.getHeight()/1024; } }; } @Override public Bitmap get(String url) { return mMemoryCache.get(url); } @Override public void put(String url,Bitmap map) { mMemoryCache.put(url,map); }}
SD卡缓存类:
public class DiskCache implements ImageCache { @Override public Bitmap get(String url) { return null;//从本地文件中获取该图片 } @Override public void put(String url, Bitmap map) { //Bitmap写入文件中 }}
内存缓存+SD卡缓存
public class DoubleCache implements ImageCache { ImageCache mMemoryCache=new MemoryCache(); ImageCache mDiskCache=new DiskCache(); //先从内存缓存中获取图片,如果没有,再从SD卡中获取 @Override public Bitmap get(String url) { Bitmap bitmap=mMemoryCache.get(url); if(bitmap==null){ bitmap=mDiskCache.get(url); } return bitmap; } @Override public void put(String url, Bitmap map) { mMemoryCache.put(url,map); mDiskCache.put(url,map); }}
ImageLoader类:
public class ImageLoader{ //图片缓存类,依赖于抽象,并且有一个默认实现 ImageCache mCache=new MemoryCache(); public void displayImage(String url,ImageView img){ Bitmap bitmap=mMemoryCache.get(url); if(bitmap==null){ downloadImage(url,img); }else{ img.setImageBitmap(bitmap); } } public void setImageCache(MemoryCache cache){ mMemoryCache=cache; }}
5、接口隔离原则
类间的依赖关系应该建立在最小的接口上。接口隔离原则将非常庞大的、臃肿的借口拆分为更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法
在Java6以及之前的JDK版本中,有一个非常讨厌的问题,那就是使用了OutputStream或者其他可关闭的对象后,我们必须保证他们最终被关闭了,例如如下代码:
public void put(String url){ FileOutputStream fileOutputStream=null; try{ fileOutputStream=new FileOutputStream(cacheDir+url); }catch(Exception e){ e.printStackTrace(); }finally{ if(fileOutputStream != null){ try{ fileOutputStream.close(); }catch(Exception e){ e.printStackTrace(); } } } }
上面的代码可读性很差,各种try..catch嵌套都是写简单的代码,但是会严重的影响代码的可读性,并且多层级的大括号很容易将代码写到错误的层级中。
我们都知道java中有一个Closeable接口,该接口标识了一个可关闭的对象,它只有一个close方法。FileOutputStream就实现了这个接口,其他的还有100多类实现了Closeable接口,这意味着,在关闭这100多个对象时,都要写出像put方法中finally代码段那样的代码。下面的工具类就是用来统一关闭实现了Closeable接口的对象。
public final class CloseUtils{ private CloseUtils(){} public static void closeQuietly(Closeable closeable){ if(null!=closeable){ try{ closeable.close(); }catch(IOException e){ e.printStackTrace(); } } }}
我们再看看把这段代码用到上述的put方法中:
public void put(String url){ FileOutputStream fileOutputStream=null; try{ fileOutputStream=new FileOutputStream(cacheDir+url); }catch(Exception e){ e.printStackTrace(); }finally{ CloseUtils.closeQuietly(fileOutputStream); } }
CloseUtils的closeQuietly()方法原理就是依赖于Closeable抽象而不是具体的实现,并且建立在最小依赖原则的基础上,它只需要知道这个对象时可以关闭的,其他的一概不管,也就是这里说的接口隔离原则。
6、迪米特原则
一个类应该对自己要耦合或调用的类知道得最少,类的内部如何实现与调用者或依赖者无关,调用者或依赖者只需要知道它需要的方法即可,其他的可以一概不管。
就拿SD卡缓存来说吧,ImageLoader调用ImageCache,而SD卡缓存内部却是使用jake wharton的DiskLruCache实现,用户根本不知道DiskLruCache的存在,他们只知道ImageCache而已。
- 读书笔记——面向对象的六大原则
- 面向对象的六大原则之 —— 单一原则
- 面向对象的六大原则之 —— 开闭原则
- 面向对象的六大原则之 —— 迪米特原则
- 设计模式之——面向对象的六大原则
- 什么是面向对象设计的六大原则—详解
- 面向对象的六大原则
- 面向对象的六大原则
- 面向对象的六大原则
- 面向对象的六大原则
- 面向对象的六大原则
- 面向对象的六大原则
- 面向对象的六大原则
- 面向对象的六大原则
- 面向对象的六大原则
- 面向对象的六大原则
- 面向对象的六大原则
- 面向对象的六大原则
- 一文读懂CNN
- 内联函数
- 将二叉树拆成链表
- 安装Sublime Text 3插件的方法
- C# study1
- 读书笔记——面向对象的六大原则
- 初识常量和变量
- POJ3013
- hdoj1205 抽屉原理
- 网站搭建
- 洛谷 P3197 [HNOI2008]越狱
- C++实验4
- 1015. 德才论 (25)
- 2017.4.20 总结