前言:以最简单的语言叙述每个人都能懂的概念。
面向对象六大原则
开闭原则
类,对象,模块,函数等对于扩展是开放的,对于修改是封闭的。
例如图片缓存功能,缓存形式有多种,甚至可能用户需要自定义缓存方法,这时我们应该是通过抽象出缓存的通用接口,而不是去修改图片加载的具体方法体来实现支持多种缓存。
例如我们有个图片加载方法,现在是没有加入缓存功能的:
public void displayImage(String url,ImageView view)
如果要加入缓存功能,我们可能会这么改:
public void displayImage(String url,ImageView view,boolean enableCache)
然后方法内部通过判断enableCache来决定是否使用缓存,虽然这样确实可以实现上述需求, 但是有个问题,如果我们需要区分内存缓存和SD卡缓存,仅仅通过单一个布尔参数,我们是无法 进行判断的,虽然我们也能够通过再添加一个参数来解决,比如这样:
public void displayImage(String url,ImageView view,boolean enableSdCache,boolean enableDiskCache)
但是明显,这么改显得非常笨拙,而且我们现在如果我们需要可以支持自定义缓存策略又该什么处理呢? 我们可以试着抽象缓存方法:
public interface ImageCache{
void put(String url,Bitmap image);
Bitmap get(String url);
}
这样不管是我们想要使用何种缓存策略都可以不改动源代码了,因为我们现在的方法如下:
public void displayImage(String url,ImageView view,ImageCache cache)
对于方法内来说,缓存策略现在是面向抽象的接口而没有面向某个具体实现,这样其实就实现了方法 对扩展是支持的,对修改是关闭的(新增缓存策略无需改动方法本身代码)。
里式替换原则
所有引用基类的地方必须能透明的使用其子类的对象
以控件为例,我们都知道Button和ImageView都是View的子类,假如现有一个方法:
void showView(View view);
这个方法可以显示一个控件。显然,调用此方法的时候我们不管是传入Button或者ImageView的实例,此方法都应该能正常显示,而不是非得要View的直接对象。但是,明显如果我们的参数列表使用Button或者ImageView来声明参数,我们传入View的直接实例时,方法就会调用出错。这就是使用父类的地方可以完全使用子类对象进行替换而不会引起任何错误。
这其实也是我们常说的面向接口编程或者面向抽象编程,就如我们需要显示一个按钮,那么我们的参数类型可以使用其父类,这样的好处在于,如果我们还需要显示其他控件的时候,无需更改任何代码,因为我们都知道所有控件都是View的子类。但是这里需要注意一点,参数列表的类型并不是声明的越高越好(如果越高越好,我们用Object不就好了,笑),具体应该使用什么类型还是要结合实际业务逻辑的,声明类型过高会造成丢失某些方法,而造成业务逻辑难以处理。
例如我们现在有A,B两个类(伪代码):
class A {
int countMoney();
}
class B extends A {
int countNum();
}
父类A有个计算金额的方法,子类B有个计算数量的方法,下面是某个计算单价的方法:
int getPrice(A a){
return a.countMoney()/a.countNum();
}
很明显,这段代码是编译不过的,因为父类A并没有countNum方法,而方法getPrice明显是必须依赖countMoney和countNum这两个方法才能计算出单价,这时用父类声明,明显是无法实现业务逻辑的。
当然了,这里只是举例说明,实际业务中,我们也往往需要调用子类的各种方法,但是却也不是一定要使用子类声明类型,因为大量使用子类声明会造成代码严重耦合,所以,我们会采用其他方法来优化。
依赖倒置原则
高层模块不应该依赖底层模块的实现细节,两者都应该依赖抽象,抽象不依赖细节,细节应该依赖抽象。
依然以图片缓存为例
public void displayImage(String url,ImageView view,ImageCache cache)
假如我们把方法改成这样:
public void displayImage(String url,ImageView view,MemoryCache cache)
也就依赖了一个具体的缓存实现类,这样会产生什么后果呢?如果我们现在需要使用DiskCache缓存,也就是意味着我们的参数列表类型要更改,甚至方法内部也需要修改。这其实就是一种依赖了细节实现而造成的强耦合,牵一发而动全身。
相反的,如果我们是用的是ImageCache这个抽象接口进行依赖就不会出现这种问题,因为不论哪种缓存,只要你实现了ImageCache这个接口,那么我的方法都可以正常工作,也就是说使用ImageCache为参数类型时,我们的方法实际上依赖的是一个抽象的缓存,而没有具体到某个实现类,从而大大降低了耦合。
接口隔离原则
类之间的关系应该建立在最小的接口上
迪米特原则
一个对象应该对另一个对象有最少的了解
这两个原则放在一起说,因为我个人的理解是这两个原则其实是类似的,只不过接口隔离原则说的是更为细致的实现方法(接口实现),而迪米特原则更多是一种设计思想,两者其实都是强调类之间应该保证最少的了解。
以生活中的事件为例,例如我和张三合作,我现在要修一栋房子,而我只要知道张三可以提供给我水泥,砖,钢筋,那么这就够了,我不需要关心,他的文化程度,是否已婚,多大年龄,是否会打篮球,是否会开车,这些东西和我的工作没有直接关系,如果我知道了他这些东西可能反而会影响我的工作进度,比如知道他可能抛弃妻子,那么我就会不屑于与他合作,对于工作进度来讲这是不利的。
当然上面这个例子只能稍微看看,不能深究,肯定是不那么严谨的,大家能懂那么点意思就对了。下面以代码为例:
class DataModel{
public void downloadImage(String url){
ImageDownloadUtil.download(url);
log("下载成功");
}
protected void log(String msg){
Log.e(TAG,msg);
}
}
class ImageDownloadUtil{
private ImageCache mCache;
public static void download(String url){
//开始下载
Bitmap bitmap = XXXXX(url);
//缓存图片
cache(bitmap);
}
private void cache(Bitmap bitmap){
mCache.cache(bitmap);
}
}
class Activity{
void click(){
new DataModel().downloadImage("http://XXX.jpg");
}
}
以上为伪代码,细节部分勿深究。
首先我们可以看到Activity内有个点击事件,明显是下载图片的,那么他是怎么实现的呢?他实例化了一个DataModel(数据源)对象,并调用其downloadImage方法传入了下载地址,以此完成了下载图片的任务,好了,对于Activity来说,他知道这一切已经够了,因为这已经可以满足他的工作了,他不需要知道DataModel是自己下载的,还是调用其他类下载的,也无需知道DataModel下载完成后是否打印了log,因为这些对于Activity来说没有用,Activity现在唯一想要的就是下载一张图片而已。
再看DataModel,内部有两个方法,一个下载图片方法,一个Log方法,明显log方法是不对外暴露的,但是其子类是可以使用的,而他的下载方法明显是调用ImageDownloadUtil工具类的下载方法实现的,然后自己进行了log输出,对于DataModel而言,他只需要知道ImageDownloadUtil有个下载图片的方法,他依然不需要管ImageDownloadUtil是自己下载的还是调用其他类实现的,而这里的缓存也是DataModel不需要关心的,所以cache方法也被声明为private,同样的,缓存对象mCache,也是不需要外界知道的,所以同样声明为私有。
迪米特原则说的东西,基本上也就是上面这个例子了,一个对象应该对另一个对象有最少的了解,这样的好处是可以降低两个对象直接的耦合,试想一下如果上面几个类的所有方法和字段都是public的,那么也就是说Activity可以访问最底层mCache和cache方法,这样做的坏处就是造成了Activity耦合的类过多。假设Activity调用了mCache的某些方法,那么mCache类的一些变更就有极大可能直接影响到Activity,这当然不是我们所想看到的。相反,采用隐藏部分字段和方法的话,外界无法直接调用mCache,那么mCache类的任何改变,影响的也只是ImageDownloadUtil类,我们需要修改也可能只是ImageDownloadUtil而不会影响到Activity。
关于java中几个访问权限关键字的使用,我们还是应该按照面向对象的思想来声明,不能一味的全部声明成public,这样做的后果也许在代码初期看不出什么问题,但是到了后面,绝对是灾难性的问题。
上面说的,大部分都是迪米特原则,其实接口隔离原则,本身就可以看成是迪米特原则的一种实现,即通过抽象接口的方式,对外暴露最少的方法,距离说明就是,假如我们现在有几种数据类型ArrayList,LinkedList,HashList,GoodList,这些类型各自都有不同的方法和字段属性,但是我们可以抽象出一个接口List,接口包含集合操作的几个基本方法,用伪代码表示如下:
public interface List<T>{
void add(T t);
void remove(int index);
T get(int index);
}
public class ArrayList<T> extends List<T>{
public void add(T t){
//自定义的添加方法
}
public void remove(int index)){
//自定义的删除方法
}
public T get(int index)){
//自定义的获取方法
return t;
}
}
public class LinkedList<T> extends List<T>{
public void add(T t){
//自定义的添加方法
}
public void remove(int index)){
//自定义的删除方法
}
public T get(int index)){
//自定义的获取方法
return t;
}
}
按照上面的思路,那么我在使用的时候,如果声明一个对象,就会用List接口去声明,而不是LinkedList或者ArrayList或者其他的某个具体实现类,因为我当前需要的功能,就是添加删除查找数据,所以List的方法完全满足我,而且使用这种方式声明,你会发现不管你的这个对象用LinkedList还是ArrayList去实例化,编译都是不会报错的(注意,只是编译不错,实际运行是有可能和预期不同的),这样其实就是通过接口的方式隔离了当前类与具体子类LinkedList或者ArrayList的相关性,当前类直接依赖的类其实是List接口而不是某个具体类,怎么样,是不是感觉和迪米特原则类似呢