简述面向对象编程(oop)的六大原则
SOLID+迪米特原则
在应用开发的过程中,最难的往往不是开发的工作,而是在后续的升级维护过程中让应用系统能够灵活地变化!也就是我们经常强调的代码健壮性和稳定性!在满足用户需求的前提下,不破坏系统稳定性的前提下保持高度可扩展性,高内聚低耦合,在经历各个版本后依旧保持清晰灵活稳定的系统架构,那么编程的时候应当尽量根据面向对象变成的六大原则来做。
<h3>S 单一职责原则(Single Responsibility Principle)</h3>
解释:
就一个类而言,应该仅有一个引起他变化的原因,简单来说,一个类中应该是一组相关性很高的函数、数据的封装!
<pre>
我们来写一个很简单的类吧!写一个缓存图片的类
public class ImageLoader{
LurCache<String,Bitmap> caches;
ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
public ImageLoader(){
initImageCache();
}
//显示图片
public void display(String url,ImageView imageview){
Bitmap bm = caches.get(url);
if(bm!=null){
imageview.setImageBitmap(bm);
return;
}
imageview.setTag(url);
mExecutorService.submit(new Runnable() {
@Override
public void run() {
Bitmap bm = downloadImage(url);
if (bitmap == null) {
return;
}
if (imageView.getTag().equals(url)) {
imageView.setImageBitmap(bm);
}
cache.putBitmapToCache(url, bitmap);
}
});
}
//初始化缓存参数
public void initImageCache(){
long maxMemory = Runtime.getRuntime().maxMemory()/1024;
int cacheSize = (int) maxMemory/4;
mImageCache = new LruCache<String,Bitmap>(cacheSize){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes()*value.getHeight()/1024;
}
};
}
//获取图片
public Bitmap get(String url){
return caches.get(url);
}
//存储图片
public void put(String url,Bitmap bm){
if(cache!=null){
cache.put(url,bm);
}
}
//下载图片
public Bitmap downloadImage(String imageUrl) {
Bitmap bm = null;
try {
URL url = new URL(imageUrl);
URLConnection connection = url.openConnection();
bm = BitmapFactory.decodeStream(connection.getInputStream());
return bm;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return bm;
}
}
</pre>
上面这个类,有三个功能,获取网络图片,显示图片,缓存处理,单一职责就是要一个类实现一个功能,那么需要把两个功能给分开。那就变成了
<pre>
public class ImageDownloader{
//下载图片
public static Bitmap downloadImage(String imageUrl) {
Bitmap bm = null;
try {
URL url = new URL(imageUrl);
URLConnection connection = url.openConnection();
bm = BitmapFactory.decodeStream(connection.getInputStream());
return bm;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return bm;
}
}
</pre>
<pre>
public class ImageCache{
LrcCache<String,Bitmap> caches;
public ImageCache(){initImageCache();
public void initImageCache(){
long maxMemory = Runtime.getRuntime().maxMemory()/1024;
int cacheSize = (int) maxMemory/4;
mImageCache = new LruCache<String,Bitmap>(cacheSize){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes()*value.getHeight()/1024;
}
};
}
//获取图片
public Bitmap get(String url){
return caches.get(url);
}
//存储图片
public void put(String url,Bitmap bm){
if(cache!=null){
cache.put(url,bm);
}
}
}
</pre>
<pre>
public class ImageLoader{
ImageCache cache = new ImageCache();
ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
//加载图片
public void display(final String url, final ImageView imageView) {
final Bitmap bitmap = cache.getBitmapFromCache(url);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
imageView.setTag(url);
mExecutorService.submit(new Runnable() {
@Override
public void run() {
Bitmap bm = ImageDownloader.downloadImage(url);
if (bitmap == null) {
return;
}
if (imageView.getTag().equals(url)) {
imageView.setImageBitmap(bm);
}
cache.putBitmapToCache(url, bitmap);
}
});
}
}
</pre>
<h3>O 开闭原则(Open Close Principle)</h3>
解释:
软件中的对象(类,模块,函数等)对扩展来说是开放的,对修改是封闭的。
在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会将错误引入原本已经经过测试的就代码中,破坏原有系统。
当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改该已有的代码来实现。
<pre>
还是用回上一个例子扩展代码,而不修改代码,要做到这样应该怎么改之前的代码呢,例如加上一个本地图片的功能还有设置允许缓存,那我们需要新建一个DiskCache类和一个DoubleCache类,Cache类都需要get和put房方法,所以我们只要将ImageCache作为接口让所有缓存的类都继承这个类就可以了
//接口实现类
public class DiskCache extends ImageCache {
String cachePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getPath();
@Override
public void put(String fileUrl, Bitmap bm) {
FileOutputStream fos = null;
try {
fos = new FileOutputStream(cachePath + File.separator + fileUrl);
bm.compress(Bitmap.CompressFormat.PNG, 100, fos);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (fos!=null){
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
@Override
public Bitmap get(String url) {
return BitmapFactory.decodeFile(cachePath + File.separator + url);
}
}
//接口类
public abstract class ImageCache {
public abstract void put(String url, Bitmap bm);
public abstract Bitmap get(String url);
}
//接口实现类
public class DoubleCache extends ImageCache {
MemoryCache mImageCache = new MemoryCache();
DiskCache dImageCache = new DiskCache();
@Override
public void put(String url, Bitmap bm) {
mImageCache.put(url, bm);
mImageCache.put(url, bm);
}
@Override
public Bitmap get(String url) {
Bitmap bm = mImageCache.get(url);
if (bm == null) {
return dImageCache.get(url);
}
return bm;
}
}
最后在ImageLoader里面添加一个依赖注入方法
public void setCache(ImageCache cache) {this.cache = cache;}
就实现了
</pre>
<h3>L 里氏替换原则(Liskov Substitution Principle)</h3>
解释:
所有引用基类的地方必须能透明地使用其子类的对象。
只要父类能出现的地方子类就可以出现,而且替换成子类也不会产生任何错误或异常
里氏替换原则的核心原理是抽象,抽象又依赖于继承这个特性,在OOP当中,继承的优缺点都相当明显:
优点:
- 代码重用,减少创建类的成本,每个子类都拥有父类的方法和属性
- 子类与父类基本想死,但又与父类有所区别
- 提高代码的可扩展性
缺点:
- 继承是侵入性的,只要继承就必须拥有父类所有属性和方法**
- 可能造成子类代码冗余,灵活度降低**
<pre>
那么父类和子类在上面的代码哪里显示了?就是一下这句依赖注入
public void setCache(ImageCache cache) { this.cache = cache;}
作为ImageCache的子类,DiskCache和DoubleCache都可以带入,而不会影响到原本的结果
</pre>
<h3>I 接口隔离原则 (Interface Segregation Principle)</h3>
解释:
客户端不应该依赖他不需要的接口
类的依赖关系应该建立在最小的接口上
让客户端依赖的接口尽可能小
<pre>
接口隔离原则,其实相比来说是更简单,当前类如果功能上无相关,则可以新建一个类专门处理该接口。
例如上面代码的这部分:
try {
fos = new FileOutputStream(cachePath + File.separator + fileUrl);
bm.compress(Bitmap.CompressFormat.PNG, 100, fos);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (fos!=null){
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
</pre>
FileOutputStream的close方式所导致的IOException,在这里显得多余,所以把它隔离开新增个类来封装好
<pre>public class CloseUtils {
public static void closeQuietly(Closeable closeable) {
if (null != closeable) {
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}}}
变成以下代码
try {
fos = new FileOutputStream(cachePath + File.separator + fileUrl);
bm.compress(Bitmap.CompressFormat.PNG, 100, fos);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
CloseUtils.closeQuietly(fos);//接口隔离,在另一个实现类里面负责
}
</pre>
<h3>D 依赖倒置原则(Dependence Inversion Principle)</h3>
解释:
高层模块不应该依赖低层模块,两者之间都应该依赖抽象
抽象不应该依赖细节(实现类),细节应该依赖抽象
模块间的依赖关系通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。
for ex:
如果是直接实现类与实现类之间发生联系,那么MemoryCache和ImageLoader,就会有冲突,所以需要一个setCache方法来设置依赖,以后如果需要修改的话,写类直接继承ImageCache或其子类,就可以了
<pre>
实体类与实体类之间,如果发生改变,通过依赖注入的方式更改
ImageLoader.setCache(ImageCache cache);
</pre>
<h3>最少知识原则 (迪米特原则)(Dependence Inversion Principle)</h3>
解释:
一个对象应该对其它对象有最小的了解。或者一个类应该对自己需要耦合或调用的类知道得最少,类的内部如何实现与调用者或者依赖者关系都没关系,调用者只需要它需要的方法就可以了
迪米特原则,是我觉得最为容易理解的!一个类,负责一个功能,那么尽量地它必须要和其它的类之间的关系少一点!只和直接接触的类进行联系,例如,找房子,租户的条件需要筛选价格地区,租户从中介中找到房源,然后查看哪家好,下结论,这怎么可能!我们都找中介了,那么要把自己的条件和中介说,说好了再看,那么我们的直接关系对象就是中介。所以租户其实是没必要和房间数据做交流,不过现实我们还是要去看房子。
第二个例子,上面写的ImageCache及其子类,ImageLoader.setCache(ImageCache cache),用户根本不需要知道ImageCache里面具体的实现是啥,我们只需要知道ImageCache是怎么使用的,就是里面的put和get!因为ImageCache才是和ImageLoader直接打交道的类,DiskCache和DoubleCache是ImageCache具体的实现类,But who care,用户又不关心这个。
<h3>总结</h3>
面向对象语言的三大特点:继承、封装,多态,而程序扩展性取决于代码的耦合度,保持一个程序高类聚低耦合是我们写代码需要注意的事情。当然,这只是个理想的状态。没有百分百,不过,灵活使用接口,尽量做到这几个原则,未免以后太多Bug的出现。我在写这篇文章的时候,在想面向对象编程好像很多都与接口相关,利用接口作为父类(里氏替换,开闭原则),添加依赖注入(依赖倒置),然后代码功能分离(单一职责,接口隔离),数据逻辑和页面分离(迪米特原则),好像也可以这样解释,当然,毕竟是设计思想,看自己怎么去理解