简单来说单一职责就是一个类只负责一个功能。更加具体的说就是对一个类而言,应该是一组相关性很高的函数、数据的封装,是高内聚低耦合的,对外界而言应该仅有一个引起它变化的原因。
单一职责在项目中的使用:
1.项目中的新手引导变量的管理可以统一在各自的Modle中用单独的类来管理
2.MVP模式P层生命周期与V层生命周期的同步可以用单独的包装类来实现,
3.各种基础框架功能的定义,例如:图片的加载、缓存、显示等都应该在各自的类中去做。
下面以一个图片加载库的实现为例:成功加载一张图片可分为:请求、缓存、加载三个步骤,那我们就按照单一职责去创建三个类分别实现这三个功能
/**
* 图片显示
*/
public class ImageLoad {
public void displayImage(String url, ImageView imageView) {
}
}
/**
* 图片缓存
*/
public class ImageCache {
public void put(String url,Bitmap bitmap){
}
public Bitmap get(String url){
}
}
/**
* 图片加载
*/
public class ImageRequest {
public Bitmap downloadImage(String url){
}
}
上面的例子:ImageLoad只负责显示图片,ImageRequest负责从网络下载图片,ImageCache负责处理图片缓存逻辑,这样设计各自的类职责相当单一,假如我们需要修改缓存的逻辑,我们只需要修改ImageCache的逻辑,若项目升级我们需要升级网络请求库,那涉及到图片部分我们也只需要修改ImageRequest类即可。总之:单一职责所要表达的用意就是”单一”二字,但是如何划分一个类、甚至是一个函数的职责,这就需要每一个开发者自己去设计。
2.开闭原则
开闭原则的英文全称是Open Close Principle缩写即OCP。开闭原则的定义是:软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是对于修改是封闭的。在软件的生命周期内,因为变化、升级和维护等原因需要对软件的原有代码进行修改时,可能会将错误的代码引入,从而破坏原有系统。因此当软件需求发生变化时,我们应该尽量通过扩展的方式 来实现变化,而不是通过修改已有的代码。
开闭原则在项目中的使用:
1.基类与子类,子类可以继承父类并扩展父类的功能
2.接口与实现类,接口定义功能,实现类按照各自的需求实现
继续以上面的图片加载框架为例:上面我们定义了三个类:ImageLoad,ImageRequest,ImageCache分别来做显示、加载、缓存这三件事,很简单的满足了单一职责的原则,但是当我们不同情况下需要执行不同的缓存策略时是不是每次又要去修改ImageCache这个类?这是不是跟我们的开闭原则相违背?ImageRequest同样如此。因此我们需要保证在添加新的缓存策略时不需要修改原来的类,只需要在此基础上扩展就可以了,这样就可以避免修改原来的类引起的未知的错误。
/**
* 图片显示
*/
public class ImageLoad {
public void displayImage(String url, ImageView imageView) {
}
}
/**
* 图片缓存
*/
public interface ImageCache {
public void put(String url,Bitmap bitmap);
public Bitmap get(String url);
}
/**
* 图片加载
*/
public interface ImageRequest {
public Bitmap downloadImage(String url);
}
注意:我们只是将ImageCache类和ImageRequest类改成了接口,这样的好处就是我们需要添加新的缓存策略时只需要实现ImageCache接口,ImageRequest同样如此。下面我们实现内存缓存和SD卡缓存
/**
* 内存缓存
*/
public class MemoryCache implements ImageCache {
@Override
public void put(String url, Bitmap bitmap) {
}
@Override
public Bitmap get(String url) {
return null;
}
}
/**
* SD卡缓存
*/
public class DiskCache implements ImageCache {
@Override
public void put(String url, Bitmap bitmap) {
}
@Override
public Bitmap get(String url) {
return null;
}
}
然后在我们的ImageLoad中可以动态注入ImageCahce实现:
/**
* 图片显示
*/
public class ImageLoad {
private ImageCache mCache = new MemoryCache();//默认缓存策略为内存缓存
/**
* 缓存策略注入
*/
public void setImageCache(ImageCache imageCache) {
this.mCache = imageCache;
}
public void displayImage(String url, ImageView imageView) {
}
}
在上面的例子中,通过setImageCache方法开发者可以对ImageLoad注入不同的缓存实现,这使得ImageLoad更加简单、健壮、扩展性、灵活性更高,同时也使得ImageCache的添加简单高效(只需要实现ImageCache接口然后通过setImageCache注入),并且这些扩展不会不会导致ImageLoad类修改。这正是开闭原则的核心思想:对扩展开发,对修改封闭。
3.里氏替换原则
里氏替换原则的定义:如果对每一个类型为S的对象O1,都有类型为T的对象O2,程序P在所有的对象O1都带换成O2时,程序P的行为没有发生变化,那么类型S是类型T的子类型换言之就是所有引用基类的地方必须能透明的使用其子类的对象。更通俗的讲就是只要父类出现的地方子类就可以出现,而且替换为子类也不会产生任何的错误或者异常。
里氏替换原则的核心是抽象,而抽象又依赖于继承这个特性,在OOP当中,继承的优缺点都相当明显。
优点:
1.代码重用,减少创建类的成本,每个子类都拥有父类的方法和属性
2.子类与父类基本相似,但又与父类有所区别
3.提高代码的可扩展性
缺点:
1.继承是侵入性的,只要继承就必须拥有父类的方法和属性
2.可能造成子类代码冗余,灵活性降低,因为子类必须拥有父类的属性和方法
在上面的例子中,我们通过ImageCache建立起了一套缓存的规范,在通过setImageCache注入不同的具体实现,保证了系统的扩展性和灵活性。因此开闭原则和里氏替换原则往往是生死相依,形影不离的,通过里氏替换原则来达到对扩展开放,对修改关闭的效果。
4.依赖倒置原则
依赖倒置原则指定了一种特定的解耦形式,使得高层次的模块不依赖与低层次模块的实现细节的目的,依赖模块被颠倒了。依赖倒置原则有以下几个关键点:
1.高层模块不应该依赖于低层模块,两者都应该依赖其抽象
2.抽象不应该依赖于细节
3.细节应该依赖于抽象
在Java语言中,抽象就是指接口或者抽象类,二者都是不能够被直接实例化的:细节就是实现类,实现接口或者抽象类而产生的类就是细节,其特点就是可以直接被实例化,也就是可以使用关键字new产生一个对象。高层模块就是指调用端,底层模块就是指具体的实现类。依赖倒置原则在Java语言中的表现就是:模块间的依赖通过抽象产生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或者抽象类产生的。使用一句话概括就是:面向接口编程或者说是面向抽象编程。
如果类与类直接依赖细节,那么这几个类之间就有直接的耦合,当具体的需求发生变化是,意味着同时修改依赖者的代码。在上面图片加载的例子中,ImageLoad这个类依赖于ImageCache这个接口,而具体的实现可以通过setImageCache注入,这样当Cache策略需要升级时,只需要实现ImageCache接口,然后通过setImageCache注入到ImageLoad中。这样就保证了细节与依赖的隔离。
5.接口隔离原则
接口隔离原则的定义是:客户端不应该依赖于他不需要的接口。另一种定义是:类之间的依赖关系应该建立在最小的接口上。接口隔离原则将非常庞大,臃肿的接口拆分成更小的和更具体的接口,这样客户端将会值需要知道它们感兴趣的方法。接口隔离原则的目的是系统解开耦合,从而容易重构、更改和部署。
在上面的IamgeLoad例子中,ImageCache指向ImageLoad提供了get和put方法,其他一概不管,这使得具体的缓存策略实现对ImageLoad隐藏,这就是使用最小化接口隔离了实现类的细节,也促使我们将更加庞大的接口拆分到更细粒度的接口当中,这同样使得我们的系统具有更低的耦合性、更高的灵活性。
6.迪米特原则
迪米特原则:一个对象应该对其他对象有最少的了解,通俗的讲,一个类应该对自己需要耦合或调用的类知道的最少,类的内部如何实现与调用者或者依赖者没有关系,调用者或者依赖者只需要知道他需要的方法即可,其他的一概不管。类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
在上面的图片加载框架设计中,我们的IamgeLoad只依赖于ImageCache的get和put方法,对其内部实现细节一无所知,即使通过setImageCache改变注入的对象时,ImageLoad也是对其内部实现细节不关心的,ImageLoad只关心ImageCache提供的get和put方法。这样使得系统具有更加低的耦合性和更好的扩展性。
小结:
在应用开发过程中,最难的不是完成应用的开发工作,而是在后续的升级、维护过程中让应用系统能够拥抱变化。拥抱变化也意味着在满足需求而且不破坏系统稳定的前提下保持高可扩展性、高内聚、低耦合,在经历了各版本的变更之后依然保持着清晰、灵活、稳定的系统架构。