想要写出跟知名开源库一样的代码,首先你需要知道这些
在工作初期,我们可能会有这样的感觉,自己的代码接口设计混乱、代码耦合较为严重、一个类的代码过多等,当自己回过头再看这些代码时可能都会感慨怎么写成那样。再看那些知名的开源库,他们大多有着整齐的代码、清晰简单的接口、职责单一的类,这个时候我们通常会捶胸顿足而感叹:什么时候老夫能写出这样的代码!
其实在做开发这些年中,我渐渐的感觉到,其实国内一些初、中级工程师写的东西不规范或者说不够清晰的原因是缺乏一些指导原则。他们手中挥舞着面向对象的大旗,写出来的东西却充斥着面向过程的气味。也许是他们不知道有这些原则,也许是他们知道但是不能很好运用到实际代码中,亦或是他们没有在实战中体会到这些原则能够带来的优点,以至于他们对这些原则并没有足够的重视。
面向对象六大原则
在此之前,有一点需要大家知道,熟悉这些原则并不是说你写出的程序就一定灵活、清晰,只是为你的优秀代码之路铺上了一层栅栏,在这些原则的指导下你才能避免陷入一些常见的代码泥沼,从而让你专心写出优秀的东西。
下面我们就以Android网络框架SimpleNet为例来学习这六大面向对象的基本原则,体会这些原则在开发过程中带来的强大能量。
1 单一职责原则
单一职责原则的英文名称是Single Responsibility Principle,简称是SRP,简单地说就是一个类只做一件事。这个设计原则备受争议却又极其重要。只要你想和别人争执、怄气或者是吵架,这个原则是屡试不爽的。因为单一职责的划分界限并不是如马路上的行车道那么清晰,很多时候都是需要靠个人经验来界定。当然,最大的问题就是对职责的定义,什么是类的职责,以及怎么划分类的职责。
试想一下,如果你遵守了这个原则,那么你的类就会划分得很细,每个类都有比较单一的职责,这不就是高内聚、低耦合么!当然,如何界定类的职责这需要你的个人经验了。
在SimpleNet中,我觉得很能够体现SRP原则的就是HttpStack这个类族了。HttpStack定义了一个执行网络请求的接口,代码如下:
public interface HttpStack {
/**
* 执行Http请求,并且返回一个Response
*/
public Response performRequest(Request<?> request);
}
从上述程序中可以看到,HttpStack只有一个performRequest函数,它的职责就是执行网络请求并且返回一个Response。它的职责很单一,这样在需要修改执行网络请求的相关代码时,只需要修改实现HttpStack接口的类,而不会影响其他的类的代码。如果某个类的职责包含有执行网络请求、解析网络请求、进行gzip压缩、封装请求参数等,那么在你修改某处代码时就必须谨慎,以免修改的代码影响了其他的功能。当你修改的代码能够基本上不影响其他的功能。这就在一定程度上保证了代码的可维护性。注意,单一职责原则并不是说一个类只有一个函数,而是说这个类中的函数所做的工作是高度相关的,也就是高内聚。HttpStack抽象了执行网络请求的具体过程,接口简单清晰,也便于扩展。
优点
(1)类的复杂性降低,实现什么职责都有清晰明确的定义。
(2)可读性提高,复杂性降低,那当然可读性提高了。
(3)可维护性提高,可读性提高,那当然更容易维护了。
(4)变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。
2 里氏替换原则
面向对象的语言的三大特点是继承、封装、多态,里氏替换原则就是依赖于继承、多态这两大特性。里氏替换原则简单来说就是所有引用基类、接口的地方必须能透明地使用其子类的对象。通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。
还是以HttpStack为例,SimpleNet定义了HttpStack来表示执行网络请求这个抽象概念。在执行网络请求时,只需要定义一个HttpStack对象,然后调用performRequest即可,至于HttpStack的具体实现由更高层的调用者指定。这部分代码在RequestQueue类中,示例如下:
/**
* @paramcoreNums线程核心数
* @paramhttpStack http执行器
*/
protected RequestQueue(intcoreNums, HttpStackhttpStack) {
mDispatcherNums = coreNums; mHttpStack = httpStack != null ? httpStack : HttpStackFactory.createHttpStack();
}HttpStackFactory类的createHttpStack函数负责根据API版本创建不同的HttpStack,实现代码如下:
// 根据API版本选择HttpClient或者HttpURLConnection
public final class HttpStackFactory { // API 9 private static final int GINGERBREAD_SDK_NUM = 9; /** * 根据SDK版本号来创建不同的Http执行器,即SDK 9之前使用HttpClient,之后则使用HttlUrlConnection * @return
*/ public static HttpStackcreateHttpStack() {
intruntimeSDKApi = Build.VERSION.SDK_INT; if (runtimeSDKApi>= GINGERBREAD_SDK_NUM) {
return new HttpUrlConnStack();
} return new HttpClientStack();
}}
上述代码中,RequestQueue类中依赖的是HttpStack接口,而通过HttpStackFactory的createHttpStack函数返回的是HttpStack的实现类HttpClientStack或HttlUrlConnStack。这就是所谓的里氏替换原则,任何父类、父接口出现的地方子类都可以出现,这不就保证了可扩展性吗!
任何实现HttpStack接口的类的对象都可以传递给RequestQueue实现网络请求的功能,这样SimpleNet执行网络请求的方法就有很多种可能性,而不是只有HttpClient和HttpURLConnection。例如,用户想使用OkHttp作为SimpleNet的执行引擎,那么创建一个实现了HttpStack接口的OkHttpStack类,然后在该类的performRequest函数中执行网络请求,最终将OkHttpStack对象注入RequestQueue即可。
细想一下,很多应用框架不就是这样实现吗?框架定义一系列相关的逻辑骨架与抽象,使得用户可以将自己的实现注入到框架中,从而实现变化万千的功能。
优点
(1)代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性。
(2)提高代码的重用性。
(3)提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,很多开源框架的扩展接口都是通过继承父类来完成的。
(4)提高产品或项目的开放性。
缺点
(1)继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法。
(2)降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束。
(3)增强了耦合性。当父类的常量、变量和方法被修改时,必需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大片的代码需要重构。
3 依赖倒置原则
依赖倒置原则这个名字看着有点不好理解,“依赖”还要“倒置”,这到底是什么意思?依赖倒置原则的几个关键点如下:
(1)高层模块不应该依赖低层模块,两者都应该依赖其抽象。
(2)抽象不应该依赖细节。
(3)细节应该依赖抽象。
在Java语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的。细节就是实现类、实现接口或继承抽象类而产生的类就是细节,其特点就是可以直接被实例化,也就是可以加上一个关键字 new 产生一个对象。依赖倒置原则在 Java 语言中的表现就是:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。软件先驱们总是喜欢将一些理论定义得很抽象,弄得不是那么容易理解,其实就是一句话:面向接口编程,或者说是面向抽象编程,这里的抽象指的是接口或者抽象类。面向接口编程是面向对象精髓之一。
采用依赖倒置原则可以减
原创不易,完成人机校验,阅读全文