面向对象的六大原则及单例模式
面向对象的六大原则
单一职责原则
单一职责原则 (SRP) 是指就一个类而言,应该仅有一个引起它变化的原因
简单而言就是一个类应该只有一项职责,而不是具有多项职责,比如一个类既负责图片缓存的处理同时还负责显示图片,实际上应该拆分成两个类,一个类负责图片的缓存,另外一个类负责图片显示。如果一个类兼具太多的职责不仅导致了耦合性,而且在一个职责发生变化的时候还可能削弱其它的职责功能。
开闭原则
开闭原则 (OCP) 是指软件中的对象对于修改应该是封闭的,对于扩展应该是开放的。
如果一个类为了实现新的功能不断的对类中的原有代码进行修改和增加,不仅可能引入 Bug,还有可能会导致类越来越庞大,比如一个图片的缓存类需要实现内存缓存、SD 卡缓存、两种方式混合的缓存方法,在图片显示类中需要自由选择何种方式进行缓存显示。比较好的一种方式是:由于三种缓存方式实际上基本功能一致,所以可以定义一个接口,然后在图片显示类中义一个接口用于指向三个类实例化的对象,那么当需要采用哪种方式去进行缓存的时候,只需要使用 set 方法进行依赖注入将接口指向相应方式的对象即可,并且如果要实现其它不同的缓存方式只需要对接口进行实现即可。这样实现的代码耦合性弱扩展性强。
里氏替换原则
里氏替换原则是指所有引用基类的地方必须能透明地使用其子类的对象
一个基类的子类拥有基类的属性和方法(私有的除外),所以在大多数情况下基类能干的子类都能做,这样可以保证很好的扩展性,因为可以在基类的基础上进行扩展实现不同功能的子类。因此里氏替换原则有利于提高扩展性,同时为开闭原则提供了保障。
依赖倒置原则
依赖倒置原则是用于解耦的一种方式,主要有以下几个关键点:
- 高层模块不应该依赖底层模块,两者都应该依赖其抽象
- 抽象不应该依赖细节
- 细节应该依赖抽象
第一点是指当高层的模块使用底层的模块时候,不应该直接使用底层模块类的具体对象,而应该使用其接口或者是抽象类,这样可以保证其扩展性,也就是说高层模块与底层模块之间应该通过接口发生联系,而不应该存在直接关联。
接口隔离原则
接口隔离原则是指类间的关系应该建立在最小的接口上
最小的接口实际上就是抽象的一种表达,一个接口下面可能可能会实现很多种接口,或者是很多层级接口,要对这些接口相同的功能部分进行操作的时候只需要对最顶层的接口操作即可,譬如当关闭输入输出流的时候,Java 中有很多种流,字节流、字符流、缓冲流。这个时候为了减少依赖、耦合性以及增加扩展性,我们只需要利用 Cloaseable 接口指向各种流的对象进行关闭操作即可。
迪米特原则
迪米特原则是指一个对象应该对其它对象有最少的了解
一个类应该尽可能少的利用到其它类完成相同的任务,这样可以降低耦合性
单例模式
定义
所谓单例也就是说在一个类在系统中只存在一个实例,并且可以自行实例化向系统提供这个实例
使用场景
适用于某个类有且仅有一个对象的场景,避免创建多个对象消耗过多的资源。
构造函数不对外开放,一般为 private
通过一个静态方法或者枚举返回单例类对象
确保单例类对象有且只有一个,尤其是在多线程环境下
确保单例类对象在反序列化时不会重新构建对象
也即是说单例模式的对象必须由该类的静态方法进行实例化和提供,并且不能出现多个对象。
优缺点
- 优点
- 单例模式在内存中只存在一个实例,减少了内存的开支。
- 减少了系统的性能开销,当一个对象的产生需要较多的资源的时候,这个时候可以通过产生一个单例对象,然后永驻内存来解决。
- 单例模式可以避免对资源的多重占用。
- 单例模式可以在系统设置全局的访问点,优化和共享资源访问
- 缺点
- 单例模式一般没有接口,扩展很困难。
- 单例对象如果持有 Context,那么很容易引发内存泄漏,此时传递给单例对象的 Context 最好是 Application Context
常用的实现方式
饿汉模式
1
2
3
4
5
6
7
8
9
10
11public class CEO {
private static CEO sCeo = new CEO();
private CEO() {
super();
}
public static CEO newInstance() {
return sCeo;
}
}该种方式实现的单例模式当类被加载的时候就会初始化一个 CEO 对象,然后外部可以通过 newInstance 静态方法进行获取。
由于单例模式需要类能够自行进行实例化,所以返回值一定是类变量以及通过静态方法进行返回。
懒汉模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class SingleInstance {
private static SingleInstance sSingleInstance;
private SingleInstance() {
super();
}
public static synchronized SingleInstance newInstance() {
if (sSingleInstance != null) {
sSingleInstance = new SingleInstance();
}
return sSingleInstance;
}
}采用懒汉模式实现的单例模式可以在使用的时候才将对象实例化,但是由于每次调用 newInstance 方法的时候都会进行同步(比不需要同步的慢 100 倍),所以造成了不必要的同步开销,不建议使用。
Double Check Lock
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class SingleInstance {
private static SingleInstance sSingleInstance = null;
private SingleInstance() {
super();
}
public static SingleInstance newInstance() {
if (sSingleInstance != null) {
synchronized (SingleInstance.class) {
if (sSingleInstance != null) {
sSingleInstance = new SingleInstance();
}
}
}
return sSingleInstance;
}
}第一次的判断避免了在对象非空情况下进行同步导致不必要开销的问题,第二次判断是由于可能存在线程 A,B 同时判断了对象为空,然后依次进入同步块中,如果这个时候不进行判断则可能导致创建出两个对象出来,所以需要进行第二次判断。
这个模式存在的一个问题是 mSingleInstance = new SingleInstance() 不是原子操作,其分为三个部分:给实例对象分配内存;调用构造函数,初始化成员字段;将实例对象指向分配的内存空间。并且后两步的执行顺序是不确定的,所以可能出现 A 线程执行完第三步,没有执行完第二步的情况下,程序切换至 B 线程,B 线程判断当前对象非空取走对象,但由于对象的成员字段没有初始化完成,所以可能出现错误。
解决办法是在 sInstance 前加上 volatile 关键字。
静态内部类
1
2
3
4
5
6
7
8
9
10
11
12
13public class SingleInstance {
private SingleInstance() {
super();
}
public static SingleInstance newInstance() {
return SingleHolder.sSingleInstance;
}
private static class SingleHolder {
private static final SingleInstance sSingleInstance = new SingleInstance();
}
}采用这种方式实现的单例模式很好的避免了 DCL 中可能出现的问题,由于内部类只有在使用它的成员以及方法的时候才会进行载入,所以可以做到使用的时候才实例化对象,而且能够确保线程安全。
枚举单例
1
2
3
4
5
6
7public enum SingletonEnum {
INSTANCE;
public void doSomething() {
System.out.println("do sth.");
}
}在任何情况下枚举实例都是一个单例,而且创建过程是线程安全的。
容器实现单例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class SingletonManager {
private static Map<String, Object> objectMap = new HashMap<>();
private SingletonManager() {
}
public static void registerService(String key, Object instance) {
if (!objectMap.containsKey(key)) {
objectMap.put(key, instance);
}
}
public static Object getService(String key) {
return objectMap.get(key);
}
}采用容器实现的单例模式可以对多种对象的单例进行管理,例如 Android 当中的 getSystemService 就是这样实现的单例模式。
总结
前四种方式实现的单例模式存在在反序列化(反射执行无参构造函数)的情况下可能会重新创建一个对象,为了避免这种情况的发生,我们需要重写 readResolve 方法,这样在进行反序列化的时候就会执行这个方法获取对象实例。
1
2
3private Object readResolve()throws ObjectStreamException{
return sSingleInstance;
}单例模式的核心在于将构造函数进行私有化,并且通过一个静态方法返回唯一的对象实例,在这个获取的过程当中需要保证线程安全、防止反序列化导致生成实例对象等问题。