前言:有色无阉割版请参见 Android面向对象六大基本原则-团队技术分享
这是团队技术分享前,编写的文档,一篇文章,讲满了技术分享的3个小时。
围绕ImageLoader通俗易懂的讲解了各原则的使用场景与优缺点。帮助非常的大!不管是新的,还是老的程序员,都推荐来看一下。复习一下。相比理解了,记住了,更推荐朋友们能够面向大家讲出来,进步更大。
Android 面向对象六大基本原则
本文几个方面来介绍
1、面向对象六大基本原则的定义+举例说明
2、代码中的使用
3、优缺点,
4、小结
会围绕一个图片加载框架,一步步讲解代码优化代码,从而表明六大基本原则的应用与效果。
因为这是设计模式的基础与核心参照原则
第一章、走向灵活软件之路-面向对象六大基本原则
前言:面向对象六大基本原则,是代码编写和代码优化的基础,也是核心,虽然是基础,但是非常重要,而且没有深入了解过的话,一知半解,也不好。本次围绕 图片加载库 举例说明。深入浅出透彻的理解什么是六大基本原则,如何运用。以及优缺点。
基本原则就好比 建造房租一样,首先要打地基,明白建造房租的流程,哪些节点是可以优化的,比如水电如何优化、开关如何设计、接口如何预留等等。明白了基本原则后,做任何事,都会在正常安全的范围内,不会导致大错
这种例子 生活中其实比比皆是,比如相机、手机的设计和制造 都有属于各自领域的基本原则,遵循原则可以让一切变得有规律、顺畅、符合逻辑。
如果某家单反生产商,搞特殊,相机镜头搞了个18:9的接口,那基本上就废了,市面上所有的镜头接口都不适合他。无法更换镜头,也注定了,他永远都做不大。不管多牛逼,遵循基本原则,是首要前提。
比如一个摄影师,要拍人像,却用广角镜头,人当然可以拍出来,但是效果就差太多了。或者说P图的基本原则,磨皮、瘦脸、缩鼻翼,大眼、瘦腰、大长腿。遵循这些基本原则,不管是拍出来的照片、还是p出来的图片,都要比无章法的自由发挥好很多。
1.1 优化代码的第一步-【单一职责原则(SRP)】
1.1.1 定义,什么是单一职责原则?
英文名称是 SIngle Responsibility Principle(SRP),定义为:就一个类而言,应该仅有一个引起他变化的原因。简单来说,一个类中,应该是一组相关性很高的函数、数据的封装。
但是 单一职责的划分界限并不算很清晰,很多时候都需要靠个人经验来界定,最大的问题就算对职责的定义,什么是类的职责,以及怎么划分类的职责。
比如:我经常看到一些Android开发在Activity中写Bean文件,网络数据处理,如果有列表的话Adapter 也写在Activity中,问他们为什么除了好找也没啥理由了,把他们拆分到其他类岂不是更好找,如果Activity过于臃肿行数过多,显然不是好事,如果我们要修改Bean文件,网络处理和Adapter都需要上这个Activity来修改,就会导致引起这个Activity变化的原因太多,我们在版本维护时也会比较头疼。也就严重违背了定义“就一个类而言,应该仅有一个引起它变化的原因”。
当然如果想争论的话,这个模式是可以引起很多争论的,但请记住一点,你写代码不只是为了你也是为了其他人
举个栗子:小到灯的开关、马桶的冲水按钮、大一点到房屋设计,厨房是厨房、卫生间是卫生间,电闸是单独在一个固定区域的。从来没见过卫生间和厨房是在一起的吧?也没见过,按一下马桶冲水按钮,既能冲水,又能充话费吧?
1.1.2 代码举例说明:
举例就举大家最熟悉的例子,以 图片加载库为例,可以分析下,这个类,在设计上有什么问题
public class ImageLoader{
// 图片缓存
LruCache<String,Bitmap> mImageCache;
// 线程池,线程数量为CPU的数量
ExecutorService mExecutorService = ExecutorService.newFixedThreadPool(Runtime.getRunTime().availiableProcesssors());
public ImageLoader(){
initImageCache();
}
// 初始化
private void initImageCache(){
// 计算可使用的最大内存
final int maxMenory = (int)(Runtime.getRuntime().maxMenory()/1024);
// 取1/4的可用内存作为缓存
final int cacheSize = maxMenory / 4;
mImageCache = new LryCache<String,Bitmap>(cacheSize){
@Override
protected int sizeOf(String key,Bitmap Bitmap){
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
}
}
public void displayImage(final String url,final ImageView ImageView){
image.setTag(url);
mExecutorService.submit(new Runnable(){
@Override
public void run(){
Bitmap bitmap = downloadImage(url);
if(bitmap == null){
return ;
}
// 图片显示
if(imageView.getTag().equals(url)){
imageView.setImageBitmap(bitmap)
}
// 图片缓存
mImageCache.put(url,bitmap);
}
});
}
public 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();
}cache(Exception e){
e.printStackTrace();
}
return bitmap;
}
}</pre>
问题解析:1、耦合严重 2、拓展性,灵活性差。
主要表现在:图片缓存逻辑 与 展示逻辑,写在同一个类,随着业务增多,个性化需求增多,代码会越来越复杂和无法维护
1.1.3 通过单一职责原则优化
public class ImageLoader{
// 图片缓存-新建一个图片缓存类
ImageCache mImageCache = new ImageCache() ;
// 线程池,线程数量为CPU的数量
ExecutorService mExecutorService = ExecutorService.newFixedThreadPool(Runtime.getRunTime().availiableProcesssors());
public void displayImage(final String url,final ImageView ImageView){
image.setTag(url);
mExecutorService.submit(new Runnable(){
@Override
public void run(){
Bitmap bitmap = downloadImage(url);
if(bitmap == null){
return ;
}
// 图片显示
if(imageView.getTag().equals(url)){
imageView.setImageBitmap(bitmap)
}
// 图片缓存
mImageCache.put(url,bitmap);
}
});
}
public 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();
}cache(Exception e){
e.printStackTrace();
}
return bitmap;
}
}</pre>
将图片缓存逻辑拆分出来。
public class ImageCache{
// 图片LRU缓存
LruCache<String,Bitmap> mImageCache;
// 初始化
private void initImageCache(){
// 计算可使用的最大内存
final int maxMenory = (int)(Runtime.getRuntime().maxMenory()/1024);
// 取1/4的可用内存作为缓存
final int cacheSize = maxMenory / 4;
mImageCache = new LryCache<String,Bitmap>(cacheSize){
@Override
protected int sizeOf(String key,Bitmap Bitmap){
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
}
}
public void put(String url ,Bitmap bitmap){
mImageCache.put(url,bitmap);
}
public Bitmap get(String url){
return mImageCache.get(url);
}
}
优点:将ImageLoader一拆为二,ImageLoader只负责图片加载的逻辑,ImageCache只负责处理图片缓存的逻辑,这样相互代码量少了。职责也清晰了。 修改任一方逻辑,都相互不干预
小结:一个函数的职责,每个人都有自己的看法,这要根据经验而定,具体逻辑而定,但是也有基本指导原则:两个完全不一样的功能,就不应该放在一个类中,一个类中应该是一组相关性很高的函数、数据的封装。这需要不断的审视代码,根据具体业务、功能进行拆分优化;
1.2 让程序更稳定、更灵活-**【开闭原则(OCP)】重要
1.2.1 定义,什么是开闭原则
开闭原则的英文全称是 Open Close Principle ,缩写是OCP,定义为:软件中的对象(类、模块、函数等)应该对于拓展是开放的,但是,对于修改是封闭的
实际应用举例说明:在软件生命周期内,因为变化、升级和维护等原因,需要对软件原有代码进行修改时,可能会错误引入原本已经经过测试的旧代码中,破坏原有系统,本来是正常的,一修改 全挂了。 。 。
因此,当软件需要变化时,我们应该尽量通过拓展的方式来实现变化,而不是通过修改已有代码来实现。但是,实际开发中,拓展代码、在原有基础上修改,是经常同时存在的
举个栗子:生活中常见的,电源插座,当位置不够的时候,怎么操作?没有人 把插座拿去改造吧?都是再找个插头,续上。这其实就是开闭原则,我可以支持任意拓展,但是你不能来随意修改我。
再比如,房屋的设计,电表是有个电箱单独存放的,是开放的。没见过,把电表砌在卫生间墙里面的吧?万一晚上电表爆了,难道要抹黑去一锤子80去敲墙 找电表再替换?这显然是不可能的。电表都单独存在,并且易替换、易维护。修改、替换电表,对我房子没有任何影响。
1.2.2 代码举例说明
以上面的ImageLoader展示与缓存为例,通过内存缓存解决了每次从网络加载图片的问题,但是,Android应用内存有限,并且具有易失性,应用重启后,原有加载过的图片将丢失
因此,考虑加入SD卡缓存功能来解决这一问题,说干就干,初始代码如下:
新建一个SD卡缓存实现类:
public class DiskCache {
static String cacheDir = "sdcard/cache/"
// 从缓存中获取图片
public Bitmap get(String url){
return BitmapFactory.decodeFile(cacheDir +url);
}
// 将图片缓存SD卡
public void put(String url,Bitmap bitmap){
FileOutPutStream fileOutPutStream = null ;
try{
fileOutPutStream = new FileOutPutStream(cacheDir + url);
bitmap.compress(CompressFormat.PNG,100,fileOutPutStream);
}cache(Exception e){
e.printStackTrace();
}finally{
if(fileOutPutStream!=null){
try{
fileOutPutStream.close();
}cache(Exception e){
e.printStackTrace();
}
}
}
}
}
因为,要将图片缓存到SD卡中,对应ImageLoader代码修改如下
public class ImageLoader{
// 内存缓存
ImageCache mImageCache = new ImageCache() ;
// SD 卡缓存
DiskCache mDiskCache = new DiskCache();
// 是否
boolean isUseDiskCache = false;
// 线程池,线程数量为CPU的数量
ExecutorService mExecutorService = ExecutorService.newFixedThreadPool(Runtime.getRunTime().availiableProcesssors());
public void displayImage(final String url,final ImageView imageView){
// 判断使用哪种缓存
Bitmap bitmap = isUseDiskCache ?mDiskCache.get(url):mImageCache.get(url);
if(bitmap!=null){
imageView.setImageBitmap(bitmap);
return ;
}
// 如果没有缓存,交给线程池下载...
}
public void useeDiskCache(boolean useDiskCache){
isUseDiskCache = useDiskCache;
}
}</pre>
用户可以自由选择,使用内存缓存 或者 SD卡缓存
问题:使用内存缓存时,不能同时SD卡缓存,使用SD卡缓存,不能同时使用内存缓存
正常的策略:优先从内存缓存,如果内存缓存没有则使用SD卡缓存,如果SD卡也没有,才从网络下载图片,这才是好的缓存策略
于是按照这个思路,又优化代码。。。新增一个DoubleCache类:
/*
* 双缓存,获取图片时,从内存中获取,如果内存中没有缓存图片,再从SD卡中获取。
* 缓存图片在内存和SD卡中都缓存一份
*/
public class DoubleCache{
ImageCache mMemoryCache = new ImageCache();
DiskCache mDiskCache = new DiskCache();
// 优先从内存缓存中获取图片,如果没有,再从SD卡中获取图片
public Bitmap get(String url){
Bitmap bitmap = mMemoryCache.get(url);
if(bitmap==null){
bitmap = mDiskCache.get(url);
}
return bitmap;
}
public void put(String url,Bitmap bitmap){
mMemoryCache.put(url,bitmap);
mDiskCache.put(url,bitmap);
}
}</pre>
再看看最新的ImageLoader类:
public class ImageLoader{
// 内存缓存
ImageCache mImageCache = new ImageCache() ;
// SD 卡缓存
DiskCache mDiskCache = new DiskCache();
// 双缓存
DoubleCache mDoubleCache = new DoubleCache();
// 是否使用SD卡缓存
boolean isUseDiskCache = false;
// 是否使用双缓存
boolea isUseDoubleCache = false;
// 线程池,线程数量为CPU的数量
ExecutorService mExecutorService = ExecutorService.newFixedThreadPool(Runtime.getRunTime().availiableProcesssors());
public void displayImage(final String url,final ImageView imageView){
// 判断使用哪种缓存
Bitmap bitmap = null;
if(isUseDoubleCache){
bitmap = mDoubleCache.get(url);
}else if(isUseDiskCache){
bitmap = mDiskCache.get(url);
}else{
bitmap = mMemoryCache.get(url);
}
if(bitmap!=null){
imageView.setImageBitmap(bitmap);
}
// 如果没有缓存,交给线程池下载...
}
public void useeDiskCache(boolean useDiskCache){
isUseDiskCache = useDiskCache;
}
public void useDoubleCache(boolean useDoubleCache){
isUseDoubleCache = useDoubleCache;
}
}
问题:1、每次修改缓存逻辑,都需要修改ImageLoader类,然后通过一个布尔变量来选择使用哪种缓存,这样可能引入bug,并且使得代码越来越臃肿,容易出错
** 2、用户不能自己实现缓存策略注入到ImageLoader中,可拓展性差,可拓展性是框架最重要的特性之一**
回到开闭原则的定义,(类、模块、函数等)对于拓展应该是开放的,但是对于修改是封闭的。上面的代码明显不复合开闭原则。软件优化时,尽量通过拓展的方式来实现变化,而不是通过直接修改已有代码来实现
1.2.3 通过开闭原则重新设计与优化
public class ImageLoader{
// 内存缓存-修改为抽象接口
ImageCache mImageCache = new ImageCache() ;
// 线程池,线程数量为CPU的数量
ExecutorService mExecutorService = ExecutorService.newFixedThreadPool(Runtime.getRunTime().availiableProcesssors());
// 注入缓存实现
public void setImageCache(ImageCache cache){
mImageCache = cache;
}
public void displayImage(String url,ImageView imageView){
Bitmap bitmap = mImageCache.get(url);
if(bitmap != null){
imageView.setImageBitmap(bitmap);
return;
}
// 图片没有缓存,提交到线程池中下载图片
submitLoadRequest(imageUrl,imageView);
}
private void submitLoadRequest(final String imageUrl,final ImageView imageView){
imageView.setTag(imageUrl);
mExecutorService.submit(new Runnable(){
@Override
public void run(){
Bitmap bitmap = downloadImage(imageUrl);
if(bitmap==null){
return;
}
if(imageView.getTag().equals(imageUrl)){
imageView.setImageBitmap(bitmap);
}
mImageCache.put(imageUrl,bitmap);
}
});
}
public 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();
}cache(Exception e){
e.printStackTrace();
}
return bitmap;
}
}
这里的ImageCache并不是原有的ImageCache,而是提取成了一个图片缓存接口,用来抽象图片缓存功能,具体声明如下:
// 图片缓存接口
public interface ImageCache{
public Bitmap get(String url);
public void put(String url,Bitmap bitmap);
}</pre>
ImageCache定义了获取、缓存两个函数,缓存的key是图片的url,值是图片本身,内存缓存,SD卡缓存,双缓存都实现了该接口,看看几个缓存的实现:
public class MemoryCache implements ImageCache{
private LruCache<Stirng,Bitmap> mMemeryCache;
public MemoryCache(){
// 初始化LRU缓存
}
@Override
public Bitmap get(String url){
return mMemeryCache.get(url);
}
@Override
public void put(String url,Bitmap bitmap){
mMemeryCache.put(url,bitmap);
}
}
public class DiskCache implements ImageCache{
@Override
public Bitmap get(String url){
// 从本地文件获取图片
}
@Override
public void put(String url,Bitmap bitmap){
// 将Bitmap写入文件中
}
}
public class DoubleCache implements ImageCache{
ImageCache mMemeryCache = new MemoryCache();
ImageCache mDiskCache = new DiskCache();
// 先从内存缓存中获取图片,如果没有,再从SD卡中获取图片
public Bitmap get(String url){
Bitmap bitmap = mMemeryCache.get(url);
if(bitmap==null){
bitmap = mDiskCache.get(url);
}
return bitmap;
}
// 将图片写入到内存和SD卡中
public void put(String url,Bitmap bitmap){
mMemeryCache.put(url,bitmap);
mDiskCache.put(url,bitmap);
}
}
细心的人可能发现了。ImageLoader类中增加了一个setImageCache(ImageCache cache)函数,用户可以童工该函数设置缓存实现,也就是通常说的依赖注入,设置方法如下:
ImageLoader imageLoader = new ImageLoader();
// 使用内存缓存
imageLoader.setImageCache(new MemoryCache());
// 使用SD卡缓存
imageLoader.setImageCache(new DiskCache());
// 使用双缓存
imageLoader.setImageCache(new DoubleCache());
// 用户自定义缓存
imageLoader.setImageCache(new Imagecache(){
@Override
public void put(Strign url,Bitmap bitmap){
// 自定义将图片缓存策略
}
@Override
public Bitmap get(String url){
// 自定义从缓存中获取图片
}
})
小结:在上述代码中,通过setImageCache(ImageCache cache)方法注入不同的缓存实现,不仅能够使ImageLoader更简单,健壮,也使得ImageLoader的可拓展性,灵活性更高。通过ImageCache接口,注入到ImageLoader中,可以实现千变万化的缓存策略,并且拓展这些缓存策略不会导致ImageLoader修改。这就是标准的开闭原则。
PS:当软件需要优化时,应该尽量通过拓展的方式来实现变化,而不是通过修改已有代码来实现 “应该尽量”说明并非绝对不可以修改原始类。如果代码恶心到吐,还是应该尽早的重构,具体要根据开发时间以及配套资源来判定。
1.3 构建拓展新更好的系统-【里氏替换原则(LSP)】
1.3.1 定义,什么里氏替换原则?
里氏替换原则英文全称是(LisKov SubStitution Principle),缩写是LSP,定义为:所有引用基类的地方,都能透明的使用其子类对象;
面向对象三大特点,继承、封装、多态,里氏替换就是依赖了继承、多态两大特性;通俗点讲,只要父类出现的地方,子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者根本就不需要知道父类还是子类,但是反过来就不行了,有子类的地方,父类未必能适应。。。
总结其实就两个字:抽象
举个栗子:耳机,老的有3.5mm接口的二级,新的有type-c接口的耳机或者蓝牙耳机。
手机只有一个,但是耳机我有十个,是什么让我可以任意的、方便的替换耳机,而手机不需要任何更改。核心就在于耳机接口的设计。 只要耳机是3.5mm的,我就可以支持。不管你是森海塞尔的、铁三角的,还是华为、小米的。随便你换。
还有就是单反相机,单反相机的镜头都是可替换的,广角、微距、人像定焦、一镜走天下等等。我既可以使用原厂的镜头,也可以使用第三方镜头替换,为什么可以做到这样?因为单反设计了镜头的接口,只要是符合单反接口的镜头。都可以支持。
试想一下,如果不设计这种接口,那会怎么样?那只能是小作坊。永远做不大了。
****1.3.2 代码举例说明**
[图片上传失败...(image-b23d7a-1554990639319)]
**1.3.3 通过里氏替换原则优化代码****
可以看上面讲过的 图片加载示例,其实已经很好的体现了里氏替换原则,MemoryCache、DiskCache、DoubleCache 都可以替换ImageCache的工作,并且保证正常运行。
小结:里氏替换原则与开闭原则生死相依、不离不弃 ,通过里氏替换原则达到对拓展的开放,对修改的关闭效果,俩原则同时强调了一个OOP的重要特性-抽象,因此,在开发过程中,运用抽象是走向代码优化的重要一步。
里氏替换原则核心原理是抽象,抽象又依赖于继承这个特性,在OOP中,继承的优缺点都特别明显,
优点有以下几个方面:
(1)代码重用,减少创建类的成本,每个子类都拥有父类的方法和属性
(2)子类与父类基本相似,但又与父类有所区别
(3)提高代码的可拓展性
继承的缺点:
(1)集成是侵入性的,只要继承就必须拥有父类的所有属性和方法
(2)可能造成子类代码冗余,灵活性降低,因为子类必须拥有父类的属性和方法;
1.4 让项目拥有变化的能力-【依赖倒置原则(DIP)】****
1.4.1 定义 什么是依赖倒置原则
依赖倒置英文全称(Dependence Inversion Principle) 缩小是DIP
定义:高层模块不应该依赖低层模块,两个都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
依赖倒置原则有以下几个关键点:
(1)高层模块不应该依赖底层模块,两者应该都依赖其抽象
(2)抽象接口不应该依赖具体实现
(3)实现类应该依赖抽象接口
解释说明:在Java中,抽象就是指接口或者抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或者继承抽象类而产生的就是细节,也就是可以加上一个关键字new产生的对象。高层模块就是调用端,低层模块就是具体实现类。
依赖倒置原则在Java中的表现就是:模块间通过抽象发生,实现类之间不发生直接依赖关系,其依赖关系是通过接口或者抽象类产生的。如果类与类直接依赖细节,那么就会直接耦合,那么当修改时,就会同时修改依赖者代码,这样限制了可扩展性。
举个栗子:单反相机不应该直接依赖某个镜头型号,即使这个镜头跟单反可以匹配。而是依赖4:3镜头连接接口,相机支持4:3接口的所有镜头。而镜头本身也是遵循4:3接口。那么两者就可以关联到一起。****相机就是高层模块,镜头就是具体实现,而抽象接口 就是4:3的镜头连接接口。
类似的有 插座、手机耳机、usb、type-c、hdmi等
1.4.2 代码举例
public class ImageLoader{
//内存缓存(直接依赖细节)
MemoeryCache mMenoryCache = new MenoryCache();
// 加载图片到ImageLoader中
public void displayImage(String url,ImageView imageView){
Bitmap bitmap = mMenoryCache.get(url);
if(bitmap==null){
downloadImage(url,imageView);
}else if(bitmap != null){
imageView.setImageBitmap(bitmap);
}
}
public void setImageCache(MenoryCache cache){
mMemeryCache = cache;
}
// 下载图片
...
}
问题分析:可以看到,ImageLoader 直接依赖了MemoryCache,MemoeryCache是具体实现,不是一个抽象类或者接口,当MemoryCache不能满足ImageLoader恶如需要被其他缓存实现替换时,此时就必须要修改ImageLoader的代码。
而且,抽象接口的定义要具体化,不要定义不需要的接口。让类的依赖关系也建立在最小的接口上。而不是提供一大堆接口,这样很臃肿,也会存在耦合不易修改。
小结:这与前文的开闭原则,其实也是对应的。里氏替换原则能够很好的保证代码的可拓展性,能随时替换具体实现并做到不影响调用端使用者,有拥抱变化的能力。让代码更灵活。其核心就是:抽象
1.4.3 通过依赖倒置原则优化代码
见1.2 开闭原则即可
1.5 系统拥有更高的灵活性-【接口隔离原则(ISP)】
1.5.1 定义 什么是接口隔离原则?
接口隔离英文全称(InterfaceSegregation Principles),缩写是ISP
定义:一个类对另一个类的依赖应该建立在最小的接口上。臃肿的接口,拆分细化成更小和更具体的接口,这样客户端将会只需要知道他们感兴趣的方法,接口隔离的原则与目的是系统解开耦合,从而容易被重构、更改和重新部署
换种说法:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
采用接口隔离原则对接口进行约束时,要注意以下几点:
(1)接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
(2)为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
(3)提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
举个栗子:灯的****开关、马桶的冲水按钮等等。灯的开关就是专门来控制开关的,冲水按钮就是专门冲水的。一看就知道干嘛的。而不是,我想关灯,要从无数按钮中查找。对我来说使用成本太大。而且无法量产,这是不科学的。
1.5.2 代码举例说明
// 将图片韩村到内存中
public void put(String url,Bitmap bmp){
FileOutPutStream fileOutPutStream = null ;
try{
fileOutPutStream = new FileOutPutStream(cacheDir+url);
bmp.compress(CompressFormat.PNG,100,fileOutPutStream);
}cache(Exception e){
e.printStackTrace();
}finally{
// 注意这一段代码
if(fileOutPutStream!=null){
try{
fileOutPutStream.close();
}cache(Exception e){
}
}// end if
}//end if finally
}</pre>
代码分析:代码可读性非常的差,各种try...cache嵌套的都是些简单的代码,但是会严重影响代码的可读性,写代码的时候,也容易发生错误。
追溯fileOutPutStream.close()方法,发现,fileOutPutStream的clsoe方法,实现了Closeable接口,如果我的项目中,而系统中,有100多个类都实现了Closeable接口。这意味着,在使用这些类结束,最后关闭这100多个对象的时候,都需要写上面finally里类似的代码。
这是无法容忍的。
既然都实现了Closeable接口,干脆把这段代码抽离出来,新建一个方法统一关闭这些对象。以后只要是实现了Closeable接口的类,都可以使用。
1.5.3 通过接口隔离原则优化代码
public final class CloseUtils{
private CloseUtils(){ }
/**
* 关闭Closeable对象
*/
public static void closeQuitely(Closeable closeable){
if(closeable != null){
try{
closeable.close();
}cache(Exception e){
e.printStackTrace();
}
}
}
}
再看看,把这段代码运用到刚才put方法中的效果:
public void put(String url,Bitmap bmp){
FileOutPutStream fileOutPutStream = null ;
try{
fileOutPutStream = new FileOutPutStream(cacheDir+url);
bmp.compress(CompressFormat.PNG,100,fileOutPutStream);
}cache(Exception e){
e.printStackTrace();
}finally{
CloseUtils.closeQuitely(fileOutPutStream);
}
}
代码明显简洁了很多,并且,这个类可以应用到所有实现了Closeable的对象中,保证了代码的重用性;
小结:CloseUtils的closeQuality方法的基本原理,就在于依赖了Closeable抽象而不是具体实现(其实就是1.4中的依赖倒置原则),建立在最小化依赖原则的基础上,它只需要知道这个对象是可关闭的,其它的一概不关心。这就是接口隔离。
思考:设想,如果关闭一个对象时,它却暴露了其它的接口函数,比如OutPutStream的write方法,这样使得更多的细节暴露在客户端代码面前,不仅没有很好的隐藏实现,还增加了接口的使用难度。还有上文说到的ImageLoader中的ImageCache,调用者ImageLoader,只需要知道该缓存对象有存、取图片的接口即可,其它的根本不关心。将庞大的接口,拆分到更细颗粒度的接口当中,这样代码就会有更低的耦合性,更高灵活性。
Bob大叔(Robert C Martin)在21世纪早期,将 单一职责、开闭原则、里氏替换、接口隔离 以及 依赖倒置5个原则定义了SOLID原则,作为面向对象变成的5个基本原则。当这些原则被启用时,系统软件变得更加清晰、简单、最大程度的拥抱变化。
1.1-1.5的总结和概括,其实可以为:抽象、单一职责、最小化。
1.6 更好的拓展性-【迪米特原则(LOD)】
1.6.1 定义,什么是迪米特原则?
定义:迪米特原则英文全称 (LAW of Demeter) ,缩写是LOD,也被称为最少了解原则(Least Knowledage Principle),一个对象应该对其它对象有最少的了解**
通俗的讲,一个类应该对自己需要耦合或者调用类知道的最少,类的内部,如何实现与调用者或者依赖者没有关系、调用者或者依赖者,只需要知道它需要的方法即可,其具体实现,内部如何骚造作,一概不管。
类与类之间的关系越密切,耦合程度就越大,当一个类需要改变时,对另一个类的影响也越大。
举个栗子:图片加载类缓存类,ImageCache,MemoryCache、DiskCache,对于调用者ImageLoader,我只需要知道,存图、取图 就可以了。你也只需要开放这两个接口给我就好了。我根本不需要知道你是怎么设置参数的、怎么设置缓存的。我根本不关心,也不应该将这些业务逻辑暴露给调用者,
这样会增加我使用的成本也会导致不必要的问题发生。比如调用者调用约定方法后,又同时调用了你提供的其它方法。这就有可能导致不必要的错误。