面向对象的六大基本原则与常用设计模式
一.单一职责原则(SRP)
就一个类而言,应该仅有一个引起它变化的原因。一个类中应该是一组相关性很高的函数、数据的封装
通俗的讲就是,一个类或一个方法中只做一件事,实现一个功能。有时单一职责的界定可能并不清晰,我的做法通常是将类和方法的注释写详细,这样一目了然地就知道是不是干了过多的事,并且别人看你的代码也能快速了解功能。
比如你写了一个ImageLoder类,主体功能是去调用加载网络图片,实现完后你发现里面既有从网络下载图片的代码,又有缓存的代码,这个时候就可以考虑将下载图片和进行缓存的部分抽离出来单独成类,主ImageLoader里仅保持接口调用即可。
二.开闭原则(OCP)
软件中的对象(类、模块、函数等)应该对于扩展是开放的,对于修改是封闭的
需求是不断变化的,如果我们每次应对变化时都需要修改原先的类或方法,就可能会导致原本稳定的系统变的不稳定。我们只应该在原有的系统本来有错误的时候进行改正,在产生新的变化时只进行拓展,当然这同时要求你的既有系统设计时就要考虑的全面一些,否则如何能愉快的扩展呢?所以基础架构就显得格外重要。
举个工作中的小例子,我们可以通过继承很好的进行拓展。像我们的项目中,有个专门处理头像的自定义View HeadImageView ,后来美术要求在一些位置的头像需要加个白色描边,这时我并没有直接对 HeadImageView 进行修改,而是新建了一个类 StrokeHeadImageView 重写onDraw 方法 drawCircle 加了一层描边。
三.里氏替换原则(LSP)
所有引用基类的地方必须能透明地使用其子类的对象
通俗的讲就是要善于利用抽象与继承,当我们接到某个功能,同时它可能有多种实现方式时,我们可以将核心方法进行一层接口抽象,在调用处使用抽象父类,具体使用哪一个实现子类都对整个系统框架不会产生影响。
工作中一个很好的例子就是图片加载框架,一般我们都会使用一个第三方库 Glide、Picasso、Fresco等,如果我们直接使用的话,这时项目需要,要进行框架的替换,你会发现这是一项浩大的工程,但是如果我们早有预见进行了一层统一的封装,你会发现这只是替换一下一个具体的加载引擎实现类而已。其实这也是策略模式的重要思想。
Github上有类似思想的封装:https://github.com/ladingwu/ImageLoaderFramework
同样的一些视频播放库也采用了这样的思想,将播放内核提取出来,具体使用IJKPlayer、ExoPlayer还是系统 MediaPlayer 都可以一键替换 。BiliBili开源的相册选择Boxing,加载图片的框架可选的设计等都是很好的体现。
四.依赖倒置原则(DIP)
高层模块不应该依赖低层模块,两者都应该依赖抽象,抽象不应该依赖细节,细节应该依赖抽象
总结就是面向接口编程,模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。
还是单一职责里那个例子,我们的ImageLoader 里不应该依赖于具体的某种缓存,具体的图片下载实现,这些都应该被抽象为接口,ImageLoader中只做接口间的打交道,你们怎么做的我不管,甚至ImageLoader就应该定义为接口,我们具体实现一个ImageLoader然后将各个功能的实现再进行拼装组合。
五.接口隔离原则(ISP)
类间的依赖关系应该建立在最小的接口上
接口隔离将庞大、臃肿的接口拆分成更小更具体的接口,使系统解耦合,层次更清晰
比如我们在对流进行操作时,完毕后需要将流关闭,就会产生这样的代码:
try{
...
}catch(){
...
}finally{
if(null != stream){
try{
stream.close();
}catch(IOException ex){
ex.printStackTrace();
}
}
}
虽然只是流关闭接口的调用,但却多出来一坨很垃圾的嵌套代码,使得程序可读性,类体结构变大,这时我们可以写一个工具类来统一处理这种实现 Closeable 接口的对象的关闭
public final class CloseUtils{
private CloseUtils(){}
public static void closeQuietly(Closeable closeable){
if(null != closeable){
try{
closeable.close();
}catch(Exception ex){
ex.printStackTrace();
}
}
}
}
再比如我们平时对ViewPager进行监听处理时,都会addOnPageChangeListener,然后实现三个回调方法,但是我们通常是只使用其中的一个,无故的导致代码变多,这时我们可以实现一个默认实现三个方法的Adapter类,再使用时 addOnPageChangeListener(传入我们的Adapter)并且只实现我们需要的方法就好了
public class OnPageChangeListenerAdapter implements ViewPager.OnPageChangeListener {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
}
@Override
public void onPageScrollStateChanged(int state) {
}
}
同样的,系统的AnimatorListenerAdapter 也是同样的思想,虽然动画监听的接口有很多,但我们不是每个都要使用。
六.迪米特原则(LOD)
一个对象应该对其他对象有最少的了解
简单来讲就是我们只与直接必须依赖的对象进行通信,并且最好是仅调用最简单的接口方法,不要在接口方法中掺杂没有直接关系的对象。
比如我们实现磁盘缓存时,只需要使用 DiskImageCahce.cache方法,具体怎么缓存的,使用DiskLruCache 还是什么都不需要知道,屏蔽了细节。
这个原则就好像最近的 gradle依赖由 compile 变为了 implementation
A imp B , B imp C , A 只能调用 B的东西,B 依赖了什么我都不care ,也不能访问 B依赖的东西
工作中常用的设计模式
1.单例模式
这应该是我们最常用的模式了,可以很方便的存储和访问一些数据,比如我们项目中AccountManager 就是使用单例存储了一些用户数据便于在应用活动期间进行数据访问。
但是这个模式有几个弊端需要注意:
<1>容易造成内存泄漏,所以要避免持有一些长生命周期的类。
<2>要区分清单例和静态类静态方法的区别,避免滥用。
<3>在Android 中,应用退到后台,时间长了应用可能会被杀掉,这时再从最近任务栈中将应用拉到前台时,系统会帮我们恢复一些Activity相关的东西,但是单例存的内容就没有了,所以一些数据还是要做持久化处理。
2.策略模式
就如同我上面讲的,在对一些第三方库进行一层抽取时格外好用。
还有一些例如 工厂方法模式,建造者模式,观察者模式等都是平时工作或自己写个库时比较常用的模式,就不一一赘述了。
总结
面向对象的设计模式就是基于三大特性(抽象、继承、多态)和六大基本原则,在一代代软件开发的过程中凝结的智慧,我们应该在深入理解这些特性和原则后,灵活的使用这些模式而不是生搬硬套过度设计,这就好比需要将内功心法修炼好,招式要随机应变,急于求成可能走火入魔,毒发攻心哦~
参考资料
《Android源码设计模式解析与实战》