在看某些知名的Android开源项目的时候,比如Volley(个人觉得该开源项目是面向接口编程的绝佳典范,可扩展性极强,强烈推荐每一个想设计出优秀框架的人学习),OKhttp等,发现他们某些代码逻辑实现很优雅,而且可扩展性很强(表现出来的特征就是某些功能具备很强的用户自定义性),这里其实就是运用到了一些设计思想,比如OOP六大原则和设计模式等。再加上本来一直对设计模式比较感兴趣,所以后面打算系统的学习下设计模式,然后根据学习与理解整理一个设计模式系列的专题。
个人学习设计模式的一些准则
- 重在应用:也就是是说我不会过多的去讲这个设计模式的定义,UML类图之类的,而是会偏向该设计模式应用场景和使用到该设计模式的知名开源项目的代码举例分析,这样在我们遇到同样场景的情况下写代码时就会自然而然的想到可以使用某种设计模式来提高代码的优雅度和扩展性。
- 不会所有的设计模式都讲:原因还是重在应用,设计模式总共包含23种,但并不是所有的设计模式都会在项目中用到,某些设计模式应用场景很小,在android开发中基本不会用到,所以不必花过多精力
- 会详细分析优缺点:也就是会详细分析使用某种设计模式和不使用的差异性,以及该设计模式的优缺点,尤其是优点(因为优点就是你需要使用它的原因或使用它带来的好处)
学习和了解设计模式资料推荐
- 前面说过,我的设计模式系列专题将以应用为主,不会过多介绍各个设计模式所涉及到的概念,所以如果你想了解关于各设计模式的概念的详细定义和UML类图结构,可以参看:https://design-patterns.readthedocs.io/zh_CN/latest/read_uml.html#id4 以及权威资料:https://java-design-patterns.com/patterns/
- 推荐一本讲解设计模式时某些举例比较贴切的书籍《Android源码设计模式解析与实战》,本人的设计模式讲解过程中的某些代码示例就是参考自这本书。
OOP六大原则
为何在讲解设计模式之前先讲OOP的六大原则?原因还是我前面提到的学习设计模式以应用为主,设计模式是手段不是目的,我们学习设计模式的目的不是为了去学这些概念,而是为了在项目开发中能够写出更优雅,可扩展性更强的代码,而达到这个目的不一定需要死死遵循设计模式,而且在我看来很多设计模式本质上就是OOP六大基本原则在某些具体场景下的提炼和概括,所以如果你能够对OOP的六大原则领悟的非常透彻,或许还能自己创造一种设计模式。
- 单一职责原则:一个类尽量只做一件事,老生常谈的标准,在讲代码规范时往往也会提到
- 开闭原则:对扩展开放,对修改关闭。这个其实在实际开发中会经常遇到,比如刚接手某个新项目的时候,需要你开发新功能,交接人一般都会说尽量不要修改已有代码(除非出现bug),而是通过新建一个类的形式来扩展功能。这就是开闭原则的一种体现。
- 依赖倒置原则:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。本质上就是面向接口/抽象编程。这一点在谷歌的Volley框架中体现的淋漓尽致
- 里氏替换原则:所有引用基类的地方必须能透明地使用其子类的对象
- 接口隔离原则:类间的依赖关系应该建立在最小的接口上。个人理解其本质上就是说将非常庞大、臃肿的接口拆分成更小的和更具体的接口,这样客户端只需要实现他们需要的方法
- 迪米特原则:一个类应该对其他类有最少的了解。通俗的说是talk only to your immediate friends,只和直接朋友对话。个人理解本质上是说一个类A应该尽可能的减少对其他类的依赖。这样能降低类之间的耦合,否则如果依赖过多的类,那么这些依赖类中某个类修改之后很可能需要修改类A。单一职责原则和迪米特原则合起来就类似我们常说的“低耦合,高内聚”。遵守迪米特原则的一个很大的好处就是能极大提高代码复用率。想想我们在创建一个新工程时可以很容易的把原先旧工程的Utils类的代码copy到新项目不加多少修改就直接使用就是因为Utils类对其他非系统类的依赖往往非常少。
依赖倒置原则
在六大原则中重点讲解下依赖倒置原则,原因主要包括2点:
- 除了依赖倒置原则之外的其他原则相对而言很容易从字面层面去理解,比如单一职责原则,开闭原则,接口隔离原则,仅仅从语文角度就能理解
- 其他的5大原则更像一种思想,一种方法论,无一个固定的实操模式,比如单一职责原则实际上很难严格确定一个类的唯一职责是什么?就像工作中你很难去界定哪些工作本不属于你做的但领导强行让你做了,对于该原则个人觉得只要把握一点就可以,那就是一个类不要做太多事,至于是否是单一的一件事不是很重要,关键是不能做太多事,否则这个类必然过于庞大。
这里以代码示例的形式来讲解使用依赖倒置原则的好处。这个示例来自《Android源码设计模式解析与实战》,个人觉得作者的这个示例举得非常恰当,所以直接引用过来:有一个ImageLoader类,该类的作用是给一个ImageView控件设置图片。假设该类是这么实现的:
public class ImageLoader {
//内存缓存(直接依赖于细节)
MemoryCache mMemoryCache = new MemoryCache();
//加载图片到ImageView中
public void displaylmage(String url, ImageView imageView) {
Bitmap bmp = mMemoryCache.get(url);
if (bmp == null) {
downloadlmage(url, imageView);
} else {
imageView.setlmageBitmap(bmp);
}
}
public void setlmageCache(MemoryCache cache) {
mCache = cache;
}
//代码省略
}
这个初始版本的ImageLoader类的实现是在设置图片资源时首先会判断该图片资源在内存中是否存在缓存,如果存在就直接使用缓存,如果不存在就从网络端下载,然后放入缓存供下次缓存命中使用。咋一看好像没什么不好的,我们一般不都这么实现吗?仔细看看注释的地方就会发现ImageLoader直接关联了MemoryCache这个具体实现类,如果随着项目的推移当MemoryCache不能满足ImageLoader而需要被其他缓存实现替代时,(比如如果不仅需要把图片缓存到内存而且也需要缓存到磁盘。这样即使用户退出程序,重新进入App还可以从磁盘中加载缓存图片而不用从网络端下载,这样能节省用户流量,提供更好的用户体验。)那么此时必须修改ImageLoader的代码,不符合开闭原则。也就是可扩展性差,这样写为何会导致可扩展性差呢?原因就是这么写不符合依赖倒置原则:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的,在上述代码中具体细节类ImageLoader和MemoryCache发生了直接的依赖。下面再来看看使用依赖倒置原则的代码:
//ImageCache缓存抽象:
public interface ImageCache {
public Bitmap get(String url);
public void put(String url, Bitmap bmp);
}
//ImageLoader 类:
public class ImageLoader {
//图片缓存类,依赖于抽象,并且有一个默认的实现
ImageCache mCache = new MemoryCache();
//加载图片
public void displaylmage(String url, ImageView imageView) {
Bitmap bmp = mCache.get(url);
if (bmp == null) {
//异步加载图片
downloadlmageAsync(url, imageView);
} else {
imageView, setImageBitmap(bmp);
}
}
//设置缓存策略,依赖于抽象
public void setlmageCache(ImageCache cache) {
mCache = cache;
}
//代码省略
}
- 首先定义一个抽象接口ImageCache,具体的实现类MemoryCache实现该接口。
- ImageLoader类依赖抽象接口ImageCache,而不再是具体实现MemoryCache
- ImageLoader类依赖抽象接口ImageCache可以有一个默认的实现,比如MemoryCache。
- ImageLoader类提供抽象接口ImageCache的注入函数setlmageCache(ImageCach),也就是依赖注入
这样如果需要替换MemoryCache的缓存实现,比如如果不仅需要把图片缓存到内存而且也需要缓存到磁盘,那么只需要定义一个DoubleCache类实现ImageCache接口,然后通过依赖注入调用setlmageCache(ImageCach)函数就可以完成该功能,而无需修改ImageLoader类的任何代码,符合开闭原则。另外会发现这么做还有另外一个好处就是如果使用你框架的用户想自己实现一套缓存来替换你开发的框架中的缓存机制,那么只需要实现ImageCache接口,然后通过setlmageCache进行依赖注入即可。框架的扩展性大大增强。
仔细思考上面的使用了依赖倒置原则的示例代码,会发现本质上就是面向接口/抽象编程。在一个具体类中尽量不要依赖其他具体类,而应该依赖其抽象。(除非这个具体类所承担的功能以后基本不会修改,这样就不需要为其定义一个接口了)
依赖倒置原则的好处
从上面的示例可以看到使用依赖倒置原则能够让项目更灵活,能拥有应对变化的能力,也就是在开发中预估到某个地方的功能可能会随着项目的推移其实现细节会不断调整,那么对于该功能最好抽象出一个接口提供最基本的功能,依赖该功能的类直接关联该接口,不要关联其具体实现类。这样以后如果功能实现发生变化,只需要定义一个新的类实现该接口,然后通过依赖注入的形式设置新的实现类即可。同时可扩展性大大增强。
单例模式
单例模式应该算得上是设计模式中使用非常广泛的一种模式了。
正如其名称所说的,单例模式的作用是为了确保某个类只存在一个实例对象。而需要确保只存在一个类的场景主要包括以下3个场景:
- 某个类具备统筹全局的功能:那么适合将该类设置为单例,提供一个全局访问点。比如插件化框架中,一般会存在一个类来管理所有的插件,那么这个类就适合以单例的形式存在。
- 某个类使用很频繁:为了节约资源可将其设置为单例避免重复创建。比如java开发中的日志类,几乎在项目的每一个关键类中都会使用到日志功能,那么可考虑将其设置为单利模式,避免频繁创建对象
- 天然的只能/只需要存在一个实例的场景:比如多线程场景下的计数器,比如古代皇帝,安卓Application等
网上关于单例模式不同的实现的方式的文章很多,这里不做过多介绍,这里只提关于单例模式几个重要的点:
- 构造函数不对外开放,设置为private
- 提供一个public的静态方法返回该返回单例类对象
- 需要确保即使在多线程场景下,单例的对象也只存在一个
- 确保单例模式在反序列化时不会重新创建实例
最常见而且能确保多线程安全的单例模式是DCL模式,也就是双重判空加锁的形式
public class Singleton {
//使用volatile修饰
private volatile static Singleton slnstance = null;
private Singleton() {//构造函数私有
}
//提供一个公有的静态方法返回该单例对象
public static Singleton getlnstance() {
//先检查实例是否存在,如果不存在才进入下面的同步块
if (slnstance == null) {
synchronized (Singleton.class) {
//第二次检测实例是否存在,如果不存在才创建实例
if (slnstance == null) {
slnstance = new Singleton();
}
}
}
return slnstance;
}
}
一种比较新颖的写法:使用静态内部类
public class Singleton {
private Singleton() {
}
public static Singleton getlnstance() {
return SingletonHolder.slnstance;
}
//静态内部类
private static class SingletonHolder {
private static final Singleton slnstance = new Singleton();
}
}
安卓中著名的图片加载框架UIL就使用到了单例模式,比如在其demo示例中就有如下初始化代码
public static void initImageLoader(Context context) {
// This configuration tuning is custom. You can tune every option, you may tune some of them,
// or you can create default configuration by
// ImageLoaderConfiguration.createDefault(this);
// method.
ImageLoaderConfiguration.Builder config = new ImageLoaderConfiguration.Builder(context);
config.threadPriority(Thread.NORM_PRIORITY - 2);
config.denyCacheImageMultipleSizesInMemory();
config.diskCacheFileNameGenerator(new Md5FileNameGenerator());
config.diskCacheSize(50 * 1024 * 1024); // 50 MiB
config.tasksProcessingOrder(QueueProcessingType.LIFO);
config.writeDebugLogs(); // Remove for release app
// Initialize ImageLoader with configuration.
ImageLoader.getInstance().init(config.build());
}
这里的ImageLoader.getInstance()很显然就是属于单例模式了,看下具体实现:
private volatile static ImageLoader instance;
/** Returns singleton class instance */
public static ImageLoader getInstance() {
if (instance == null) {
synchronized (ImageLoader.class) {
if (instance == null) {
instance = new ImageLoader();
}
}
}
return instance;
}
protected ImageLoader() {
}
确实是单例模式,而且是DCL的单例模式,不过这里的构造函数并未声明为private,而是protected。不过只要是非public的,外界就不能通过new操作符私自创建实例对象,而是只能通过ImageLoader自身提供的全局访问接口来获取。
这里的单例模式是前面总结的3种应用场景的第1种,ImageLoader类提供统筹全局的功能,该类负责维护框架内部其他组件的所有功能与状态,对外提供一个全局访问点,类似一家公司的CEO,比如init初始化接口,displayImage显示图片接口,loadImage同步加载图片接口,loadImageSync异步加载图片等功能。
再比如国内的插件化框架DroidPlug和VirtualApk管理插件的功能类对外都是以单例的形式提供的。(这里不再贴代码,感兴趣的可以自己去看)。
单例模式的好处
- 对于第1种应用场景下的单例模式:其优点在于因为单例提供了一个全局访问点,因此可以在代码的很多地方共享运行时的数据访问,这也是为何很多安卓开发者喜欢在Application中存放一些全局共享数据的原因,再比如插件化框架中管理插件的单例PluginManager,通过该单例能够获取到每一个插件对应的资源信息
- 对于第2中应用场景下的单例模式:其优点在于因为是单例只创建一次实例,所以能够减少内存消耗。尤其是当创建一个对象需要用到较多资源时,比如数据库操作
- 对于第3种应用场景下的单例模式:其优点在于避免在访问资源时出现并发问题,也就是通过确保内存中只存在一个实例(加锁同步)来控制对同一资源的并发访问,因此常常应用在多线程/多进程操作的场景下。比如windows操作系统的文件管理器,这种场景往往属于必须使用单例模式的场景。
文章评论