面向对象的六大原则
单一职责原则
Single Responsibility Principle(SRP),定义是:就一个类而言,应该仅有一个引起它变化的原因。简单来说,一个类中应该使一组相关性很高的函数、数据的封装。
例如一个ImageLoader实现图片加载,并要将图片缓存起来。
-
有的人可能会这样实现:是直接在一个类里实现图片下载、LruCache、displayImage等。但这样耦合太严重,毫无扩展性。随着功能增加,类会越来越大,系统越来越脆弱。
-
较好的实现是对ImageLoader进行拆分,添加一个类ImageCache类用于处理图片缓存。ImageLoader只负责图片加载的逻辑。
开闭原则
Open Close Principle(OCP),定义是:软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是,对于修改是封闭的。在软件的生命周期内,因为变化、升级和维护等原因需要对软件进行原有代码修改时,可能会将错误引入原本已经过测试的旧代码中,破坏原有系统。因此,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。
《面向对象软件构造》中提出了开闭原则。这一想法认为,程序一旦开发完成,程序中一个类的实现只应该因错误而被修改,新的或者改变的特性应该通过新建不同的类而实现,新建的类可通过继承的方式来重用原类的代码。
以ImageLoader为例,需要实现内存缓存、SD卡缓存以及双缓存:
-
有的人可能会这样实现: ImageCache里持有内存缓存类ImageCache、SD卡缓存类DiskCache,再通过设定布尔值来使用if-else判断语句来确定使用哪种缓存。随着逻辑的引入,代码会变得越来越脆弱、复杂,如果一不小心写错某个if条件,就需要更多的时间来排除。最重要的是,用户不能自己实现缓存注入到ImageLoader中,可扩展性差,可扩展性是框架的最重要特性之一。
-
较好的实现是:提取一个图片缓存的接口,用来抽象图片缓存的功能:
public interface ImageCache { public Bitmap get(String url); public void put(String url, Bitmap bmp); } 复制代码
内存缓存、SD卡缓存、双缓存都实现了该接口。ImageLoader类中增加了一个
setImageCache(ImageCache cache)
函数,用户可以通过该函数设置缓存实现,也就是通常说的依赖注入。通过函数注入不同的缓存实现,这样不仅是ImageLoader更简单、健壮,也是可扩展性、灵活性更高。几种缓存的具体实现不一样,但是,它们有个特点是都实现了ImageCache接口。当用户需要自定义实现缓存策略时,只需要新建一个实现ImageLoader接口的类,然后改造该类的对象,并通过设置函数注入到ImageLoader中,这样就实现了千变万化的缓存策略。
里氏替换原则
全称是Liskov Substitution Principle(LSP)。定义是:如果对每一个类型为S的对象O1,都有类型为T的对象O2,使得以T定义的所有程序P在所有的O1都代换为O2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。
里氏替换原则第二种定义:所有引用基类的地方必须能透明地使用其之类的对象。通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行,有子类出现的地方,父类未必就能适应。最终总结就两个字:抽象。
在OOP中(面向对象编程,Object Oriented Programming,OOP),继承的优缺点都相当明显。
优点:
- 代码重用,减少创建类的成本,每个子类都拥有父类的方法和属性;
- 子类与父类基本相似,但又与父类有所区别;
- 提高代码的可扩展性。
继承的缺点:
- 继承是侵入性的,只要继承就必须拥有父类的所有属性和方法;
- 可能造成子类代码冗余、灵活性降低,因为子类必须拥有父类的所有属性和方法。
里氏替换原则就是为这类问题提供指导原则,也就是建立抽象。通过抽象建立规范,具体的实现在与形式替换掉抽象,保证系统的扩展性、灵活性。开闭原则和里氏替换原则往往是生死相依的,通过里氏替换来达到对扩展开放,对修改关闭的效果。两个原则都同时强调了OOP的一个重要特性——抽象。
依赖倒置原则
Dependence Inversion Principle(DIP)。依赖倒置原则指代了一种特定的解耦形式,使得高层次的模块不依赖于低层次的模块的实现细节的目的,依赖模块被颠倒了。其有几个关键点:
- 高层模块不应该依赖低层模块,两者都应该依赖其抽象;
- 抽象不应该依赖细节;
- 细节应该依赖抽象。
JAVA中,抽象就是指接口或抽象类;细节就是实现类;高层模块就是调用端,低层模块就是具体实现类。依赖倒置在JAVA语言中的表现是:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。一句话概括:面向接口(抽象)编程。
如果类与类直接依赖于细节,那么它们之间就有直接的耦合,当具体实现需要变化时,意味着要同时修改依赖者的代码,这限制了系统的可扩展性。例如之前的例子,ImageLoader直接依赖于MemoryCache,这个MemoryCache是一个具体实现而不是抽象,这导致了ImageLoader直接依赖了具体细节,当MemoryCache不能满足ImageLoader而需要被其他缓存实现替代时,就必须修改ImageLoader。
接口隔离原则
InterfaceSegregation Principles(ISP)。定义是:客户端不应该依赖它不需要的接口。接口隔离原则的目的是系统解开耦合,从而容易重构、更改和重新部署。说白了就是让客户端依赖的接口尽可能地小。
例如ImageLoader中的ImageCache就是接口隔离原则的运用,ImageLoader只需要知道该缓存对象有存、取缓存图片的接口即可,其他的一概不管,这就使得缓存功能的具体实现对ImageLoader隐藏,用最小化接口隔离了实现类的细节,也促使我们将庞大的接口拆分到更细粒度的接口当中,使系统具有更低的耦合性,更高的灵活性。
迪米特原则(最少只是原则)
Law of Demeter(LOD)。一个对象应该对其他对象有最少的了解。还有一种解释是:只与直接的朋友通信。
例如,租房子,中介拥有房源,客户对租金和面积有要求。
-
有的人可能会这样实现:Room有属性area、price,Mediator(中介)有属性List,Tenant(客户)有属性myPrice、myArea,有方法
isSuitable(Room room)
来判断房源是否符合要求。Tenant需要Mediator遍历拥有的房源,然后再对Room进行比较是否符合条件。
-
较好的实现:
将对于Room的判定操作移到Mediator类中,这本应该是中介的职责,根绝用户的条件查找防止,用户只需要我们的朋友——中介沟通就好了。“只与直接的朋友通信。”
单例模式
定义:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
一般有如下几个关键点:
- 构造函数不对外开放,一般为Private;
- 通过一个静态方法或者枚举返回单例类对象;
- 确保单例类的对象有且只有一个,尤其是在多线程环境下;
- 确保单例类对象在反序列化时不会重新构建对象。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
复制代码
建造者模式
定义
将一个复杂对象的构建与它的表示分离,使得同样的构建过程创建不同的表示
使用场景
- 相同的方法,不同的执行顺序,产生不同的事件结果
- 多个部件或零件,都可以装配到一个对象中,但产生的运行结果又不相同
- 产品类非常复杂,或者产品类中的调用顺序不同产生不同的作用
- 初始化一个对象特别复杂,如参数多,且很多参数都有默认值