单一职责很容易理解,也很容易实现。所谓单一职责,就是一个设计元素只做一件事。什么是“只做一件事”?简单说就是少管闲事。现实中就是如此,如果要你专心做一件事情,任何人都有信心可以做得很出色。
一、定义。
SRP(Single Responsibility Principle):单一职责的原则:一个类应该只有一个发生变化的原因。
每一个职责都是变化的轴线。当需求变化时,该变化会反应为类的职责变化。如果一个类承担了多于一个的职责,那么引起它变化的原因就会有多个。
如果一个类承担的职责过多,就等于把这些职责耦合在了一起。一个职责的变化可能会消弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当发生变化时,设计会遭受到意想不到的破坏。
如下图的设计。Rectangle类具有两个方法,一个方法把矩形绘制在屏幕上,另一个方法计算矩形的面积。
有两个不同的应用程序使用Rectangle类,一个应用程序是有关计算几何学方面的,利用Rectangle类计算几何形状,但不会在屏幕上绘制矩形。另外一个应用程序实质上是有关图形绘制方面的,它可能也会进行一些计算几何学方面的工作,但是他肯定会在屏幕上绘制矩形。
这个设计违反了单一职责的原则(SRP)。Rectangle类具有两个职责。
第一:提供了一个矩形几何形状的数学模型;
第二:把矩形在一个图形用户界面上绘制出来。
导致的问题:
首先,我们必须在计算几何应用程序中包含进GUI代码。在.NET中,就必须要把GUI组建和计算几何应用一起构建、部署。
其次,如果GraphicalApplication的改变由于一些原因导致了Rectangle的改变,那么这个改变会迫使我们重新构建、测试以及部署ComputationalGeometryApplication。如果忘记了这样做,ComputationalGeometryApplication可能会有不可预测的问题。
一个较好的设计是把这两个职责分离到下图中所示的两个完全不同的类中。
二、定义职责
在SRP中,我们把职责定义为变化的原因。如果你能够想到多于一个的动机去改变一个类,那么这个类就具有多于一个的职责。
如下代码中Modem的接口,大都数人都会认为这个接口看起来非常合理。该接口所声明的4个方法确实是调制解调器所具有的功能。
1
2
3
4
5
6
7
|
public
interface
IModem
{
void
Dial(
string
pno);
void
Hangup();
void
Send(
char
c);
char
Recv();
}
|
然而,该接口中却显示出两个职责。第一:链接管理;第二:数据通信。Dial和Hangup方法进行调制解调器的链接处理,而Send和recv方法进行数据通信。
这两个职责应该分开吗?这依赖于应用程序的变化方式。如果应用程序的变化会影响到链接方法的签名(signature),那么这个设计就具有僵化性的臭味,因为调用Send和Recv的类必须要重新编译、部署的次数常常会超过我们希望的次数。在这种情况下,这两个职责应该被分离,如下图。这样做避免了客户应用程序和这两个职责耦合在一起。
另一方面,如果应用程序的变化方式总是导致这两个职责同时变化,那么就不必分离他们。这时,分离它们就会具有不必要的复杂性的臭味。
仅当变化发生时,变化的轴线(职责)才具有实际的意义。
我们把两个职责都耦合进了ModemImplementation类中,这不是所希望的,但或许是必要的,常常由于一些和硬件或者操作系统的细节有关的原因,迫使我们这样去处理。我们可以把它看做一个有缺陷的类,所有的依赖关系都是从它出发,谁也不需要依赖它。除了Main以为,谁也不需要知道它的存在。因此我们已经把丑陋的部分隐藏起来。
开闭原则是面向对象设计中“可复用设计”的基石,是面向对象设计中最重要的原则之一,其它很多的设计原则都是实现开闭原则的一种手段。 所谓的开闭原则(Open-Closed Principle,简称 OCP)说的是:软件实体(类、模块、功能等)应该可以被扩展,但不可被修改。开闭原则说白了就是,应该在不修改现有代码的基础上,引入新功能。
开闭原则中的“开”,是指对于组件功能的扩展是开放的,是允许对其进行功能扩展的;开闭原则中的“闭”,是指对于原有代码的修改是封闭的,即不应该修改原有的代码。
而实际上,要做到百分之百的封闭是不可能的,但是在系统设计的时候,还是应该尽量做到这一点。
一个简单的例子,假设现在需要实现一个加法的功能,很简单,如图示1:
现在的问题是,需求变了,要求还要实现一个减法的功能,这也很简单,如图示2:
如果需求再变,还要求能实现乘法和除法的功能,依然简单,如图示3:
从第2和第3步来看,很明显,在需求改变,需要引进新的功能的时候,做法是在已有的类的基础上通过新添方法来实现功能,假设在第2步之后,第3步的时候发现加法和减法功能最终都没有用到,反而乘法和除法需要用到,那么在第3步的时候就要推翻第2步之前的实现,在需求发生改变需要引进新的功能的时候,就要推翻整个之前的系统,很明显这样的做法是不可取的,说明设计上出现了问题,这个缺陷明显的是违反了OCP(开闭原则)。
需求总是在变的,如果可能,就要做到尽量不要去修改已有的实现,而应该通过扩展的手段来稳定需求的变动。
OCP原则替换图2:
OCP原则替换图3:
在我们最初编写代码的时候,我们假设变化不会发生,但最后变化发生的时候,可以通过创建抽象来隔离以后将要发生的同类变化。
开闭原则是面向对象设计中“可复用设计”的基础,是面向对象设计中最重要的原则之一。 从开闭原则中可以看出,面对对象的重要原则是创建抽象化,并且从抽象化中导出具体化,具体化可以有许多不同的版本,而每个不同的版本可以给出不同的实现。
出处:http://www.blogjava.net/fancydeepin ]
里氏置换原则(Liskov Substitution Principle),简称LSP
定义:
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
所有引用基类的地方必须能够透明的使用其子类对象。
也就是说,只要父类出现的地方子类就能够出现,而且替换为子类不会产生任何错误或异常。但是反过来,子类出现的地方,替换为父类就可能出现问题了。
这个原则是为良好的继承定义一个规范,简单的讲,有4层含义:
一、子类必须完全实现父类的方法
定义一个抽象类
public
abstract
class
ViewPoint {
//去丽江旅游
public
abstract
void
where();
}
|
下面两个类是实现这个抽象类
public
class
Lijiang
extends
ViewPoint {
@Override
public
void
where() {
System.out.println(
"欢迎来到丽江..."
);
}
}
|
public
class
Zhangjiajie
extends
ViewPoint {
@Override
public
void
where() {
System.out.println(
"欢迎来到张家界..."
);
}
}
|
人物是涂涂,在里面设置类类型来传递参数。此时涂涂要去的旅游景点还是抽象的
public
class
Tutu {
//定义要旅游的景点
private
ViewPoint viewpoint;
//涂涂要去的景点
public
void
setViewPoint(ViewPoint viewpoint)
{
this
.viewpoint = viewpoint;
}
public
void
travelTo()
{
System.out.println(
"涂涂要去旅游了"
);
viewpoint.where();
}
}
|
场景类。设置具体要去的景点
public
class
Sence {
public
static
void
main(String args[])
{
Tutu tutu =
new
Tutu();
//设置要去的旅游景点
tutu.setViewPoint(
new
Lijiang());
tutu.travelTo();
}
}
|
运行结果:
涂涂要去旅游了
欢迎来到丽江...
二、子类可以有自己的特性
也就是说在类的子类上,可以定义其他的方法或属性
三、覆盖或者实现父类的方法时输入参数可以被放大
父类能够存在的地方,子类就能存在,并且不会对运行结果有变动。反之则不行。
父类,say()里面的参数是HashMap类型,是Map类型的子类型。(因为子类的范围应该比父类大)
import
java.util.Collection;
import
java.util.HashMap;
public
class
Father {
public
Collection say(HashMap map)
{
System.out.println(
"父类被执行..."
);
return
map.values();
}
}
|
子类,say()里面的参数变成了Map类型,Map范围比HashMap类型大,符合LSP原则。注意这里的say不是覆写父类的say,因为参数类型不同。而是重载。
import
java.util.Collection;
import
java.util.Map;
/*
* 子类继承了父类的所有属性
*/
public
class
Son
extends
Father {
//方法输入参数类型
public
Collection say(Map map)
{
System.out.println(
"子类被执行..."
);
return
map.values();
}
}
|
场景类
import
java.util.HashMap;
public
class
Home {
public
static
void
main(String args[])
{
invoke();
}
public
static
void
invoke()
{
//父类存在的地方,子类就应该能够存在
//Father f = new Father();
Son s =
new
Son();
HashMap map =
new
HashMap();
//f.say(map);
s.say(map);
}
}
|
无论是用父类还是子类调用say方法,得到的结果都是
父类被执行...
但是,如果将上面Father里的say参数改为Map,子类Son里的say参数改为HashMap,得到的结果就变成了
f.say(map)结果:父类被执行...
s.say(map)结果: 子类被执行...
这样会造成逻辑混乱。所以子类中方法的前置条件必须与父类中被覆写的前置条件相同或者更宽。
四、覆写或者实现父类的方法时输出结果可以被缩小
其实与上面的类似,也就是父类能出现的地方子类就可以出现,而且替换为子类不会产生任何错误或者异常,使用者也无需知道是父类还是子类。但是反过来就不行了,有子类出现的地方,父类未必就适应。(毕竟子类的范围要>=父类的范围)
依赖倒置(Dependence InversionPrinciple)原则讲的是:要依赖于抽象,不要依赖于具体。
简单的说,依赖倒置原则要求客户端依赖于抽象耦合。原则表述:
抽象不应当依赖于细节;细节应当依赖于抽象;
要针对接口编程,不针对实现编程。
依赖倒置原则(Dependence Inversion Principle),简称DIP
定义
High level modules should depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.
即
1、高层模块不应该依赖低层模块,两者都应该依赖于抽象(抽象类或接口)
2、抽象(抽象类或接口)不应该依赖于细节(具体实现类)
3、细节(具体实现类)应该依赖抽象
抽象:即抽象类或接口,两者是不能够实例化的
细节:即具体的实现类,实现接口或者继承抽象类所产生的类,两者可以通过关键字new直接被实例化
而依赖倒置原则的本质骑士就是通过抽象(抽象类或接口)使各个类或模块的实现彼此独立,不相互影响,实现模块间的松耦合。但是这个原则也是6个设计原则中最难以实现的了,如果没有实现这个原则,那么也就意味着开闭原则(对扩展开发,对修改关闭)也无法实现。
依赖倒置有三种方式来实现
1、通过构造函数传递依赖对象
比如在构造函数中的需要传递的参数是抽象类或接口的方式实现。
2、通过setter方法传递依赖对象
即在我们设置的setXXX方法中的参数为抽象类或接口,来实现传递依赖对象
3、接口声明实现依赖对象
例如下面的例子
涂涂是个女僧
public
class
Tutu {
//涂涂是个女孩,会煮面
public
void
cook(Noodles noodles)
{
noodles.eat();
}
}
|
面条(目前只会煮面)
public
class
Noodles {
//吃面条
public
void
eat()
{
System.out.println(
"涂涂吃面条..."
);
}
}
|
涂涂坐在家里吃面(场景类)
public
class
Home {
public
static
void
main(String args[])
{
Tutu tutu =
new
Tutu();
Noodles food =
new
Noodles();
tutu.cook(food);
}
}
|
运行结果:涂涂吃面条...
但是这有个问题,涂涂只会做面条,不可能每次都吃面条吧,天天吃面吃死你,所以在上面的Tutu类中的cook方法中,如果涂涂会做其他吃的,那岂不是更好。于是她向家庭主妇迈进了一步,使用了依赖倒置原则。
也就是涂涂通过学习还可以焖米饭,炒鱿鱼(虽然听着不爽,但是很好吃),京酱肉丝啊等等。要想在代码中实现,就需要实现两个接口:ITutu和IFood
public
interface
ITutu {
//这样就会做很多饭菜了
public
void
cook(IFood food);
}
|
实现类
public
class
Tutu
implements
ITutu {
@Override
public
void
cook(IFood food) {
food.eat();
}
}
|
食物接口
public
interface
IFood {
public
void
eat();
}
|
这样就为扩展留出了很大的空间,方面扩展其他的类。也不会对细节有变动。以后涂涂想吃什么学一下就可以自己做了
实现面条
public
class
Noodles
implements
IFood {
@Override
public
void
eat() {
System.out.println(
"涂涂吃面条..."
);
}
}
|
实现米饭
public
class
Rice
implements
IFood {
@Override
public
void
eat() {
System.out.println(
"涂涂吃米饭(终于吃上米饭了)..."
);
}
}
|
场景类:涂涂在家里开吃了,想吃什么直接做就是了
public
class
Home {
public
static
void
main(String args[])
{
//接口使不能实例化滴
ITutu tutu =
new
Tutu();
//实例化米饭,涂涂可以吃米饭了
IFood rice =
new
Rice();
//吃面条
//IFood noodles = new Noodles();
tutu.cook(rice);
}
}
|
这样各个类或模块的实现彼此独立,不互相影响,实现了模块间的松耦合。
使用多个专门的接口比使用单一的总接口要好。广义的接口:一个接口相当于剧本中的一种角色,而此角色在一个舞台上由哪一个演员来演则相当于接口的实现。因此一个接口应当简单的代表一个角色,而不是一个角色。,如果系统设计多个角色的话,则应当每一个角色都由一个特定的接口代表。狭义的接口(Interface):接口隔离原则讲的就是同一个角色提供宽、窄不同的接口,以对付不同的客户端。
一、原理介绍
1、官方定义
接口隔离原则,英文缩写ISP,全称Interface Segregation Principle。
原始定义:Clients should not be forced to depend upon interfaces that they don't use,还有一种定义是The dependency of one class to another one should depend on the smallest possible interface。
官方翻译:其一是不应该强行要求客户端依赖于它们不用的接口;其二是类之间的依赖应该建立在最小的接口上面。简单点说,客户端需要什么功能,就提供什么接口,对于客户端不需要的接口不应该强行要求其依赖;类之间的依赖应该建立在最小的接口上面,这里最小的粒度取决于单一职责原则的划分。
2、自己理解
2.1、原理解释
- 不应该强行要求客户端依赖于它们不用的接口。语句很好理解,即客户端需要什么接口,就依赖什么接口,不需要的就不依赖。那么我们反过来说,如果客户端依赖了它们不需要的接口,那么这些客户端程序就面临不需要的接口变更引起的客户端变更的风险,这样就会增加客户端和接口之间的耦合程度,显然与“高内聚、低耦合”的思想相矛盾。
- 类之间的依赖应该建立在最小的接口上面。何为最小的接口,即能够满足项目需求的相似功能作为一个接口,这样设计主要就是为了“高内聚”。那么我们如何设计最小的接口呢?那就要说说粒度的划分了,粒度细化的程度取决于我们上一章讲的的单一职责原则里面接口划分的粒度。从这一点来说,接口隔离和单一职责两个原则有一定的相似性。
2.2、接口隔离原则和单一职责原则
从功能上来看,接口隔离和单一职责两个原则具有一定的相似性。其实如果我们仔细想想还是有区别的。
(1)从原则约束的侧重点来说,接口隔离原则更关注的是接口依赖程度的隔离,更加关注接口的“高内聚”;而单一职责原则更加注重的是接口职责的划分。
(2)从接口的细化程度来说,单一职责原则对接口的划分更加精细,而接口隔离原则注重的是相同功能的接口的隔离。接口隔离里面的最小接口有时可以是多个单一职责的公共接口。
(3)单一职责原则更加偏向对业务的约束,接口隔离原则更加偏向设计架构的约束。这个应该好理解,职责是根据业务功能来划分的,所以单一原则更加偏向业务;而接口隔离更多是为了“高内聚”,偏向架构的设计。
二、场景示例
下面就以订单的操作为例来说明下接口隔离的必要性。
1、胖接口
软件设计最初,我们的想法是相同功能的方法放在同一个接口里面,如下,所有订单的操作都放在订单接口IOrder里面。理论上来说,这貌似没错。我们来看看如何设计。
public interface IOrder { //订单申请操作 void Apply(object order); //订单审核操作 void Approve(object order); //订单结束操作 void End(object order); }
刚开始只有销售订单,我们只需要实现这个接口就好了。
public class SaleOrder:IOrder { public void Apply(object order) { throw new NotImplementedException(); } public void Approve(object order) { throw new NotImplementedException(); } public void End(object order) { throw new NotImplementedException(); } }
后来,随着系统的不断扩展,我们需要加入生产订单,生产订单也有一些单独的接口方法,比如:排产、冻结、导入、导出等操作。于是我们向订单的接口里面继续加入这些方法。于是订单的接口变成这样:
public interface IOrder { //订单申请操作 void Apply(object order); //订单审核操作 void Approve(object order); //订单结束操作 void End(object order); //订单下发操作 void PlantProduct(object order); //订单冻结操作 void Hold(object order); //订单删除操作 void Delete(object order); //订单导入操作 void Import(); //订单导出操作 void Export(); }
我们生产订单的实现类如下
//生产订单实现类 public class ProduceOrder : IOrder { /// <summary> /// 对于生产订单来说无用的接口 /// </summary> /// <param name="order"></param> public void Apply(object order) { throw new NotImplementedException(); } /// <summary> /// 对于生产订单来说无用的接口 /// </summary> /// <param name="order"></param> public void Approve(object order) { throw new NotImplementedException(); } /// <summary> /// 对于生产订单来说无用的接口 /// </summary> /// <param name="order"></param> public void End(object order) { throw new NotImplementedException(); } public void PlantProduct(object order) { Console.WriteLine("订单下发排产"); } public void Hold(object order) { Console.WriteLine("订单冻结"); } public void Delete(object order) { Console.WriteLine("订单删除"); } public void Import() { Console.WriteLine("订单导入"); } public void Export() { Console.WriteLine("订单导出"); } }
销售订单的实现类也要相应做修改
//销售订单实现类 public class SaleOrder:IOrder { public void Apply(object order) { Console.WriteLine("订单申请"); } public void Approve(object order) { Console.WriteLine("订单审核处理"); } public void End(object order) { Console.WriteLine("订单结束"); } #region 对于销售订单无用的接口方法 public void PlantProduct(object order) { throw new NotImplementedException(); } public void Hold(object order) { throw new NotImplementedException(); } public void Delete(object order) { throw new NotImplementedException(); } public void Import() { throw new NotImplementedException(); } public void Export() { throw new NotImplementedException(); } #endregion }
需求做完了,上线正常运行。貌似问题也不大。系统运行一段时间之后,新的需求变更来了,要求生成订单需要一个订单撤销排产的功能,那么我们的接口是不是就得增加一个订单撤排的接口方法CancelProduct。于是乎接口变成这样:
public interface IOrder { //订单申请操作 void Apply(object order); //订单审核操作 void Approve(object order); //订单结束操作 void End(object order); //订单下发操作 void PlantProduct(object order); //订单撤排操作 void CancelProduct(object order); //订单冻结操作 void Hold(object order); //订单删除操作 void Delete(object order); //订单导入操作 void Import(); //订单导出操作 void Export(); }
这个时候问题就来了,我们的生产订单只要实现这个撤销的接口貌似就OK了,但是我们的销售订单呢,本来销售订单这一块我们不想做任何的变更,可是由于我们IOrder接口里面增加了一个方法,销售订单的实现类是不是也必须要实现一个无效的接口方法?这就是我们常说的“胖接口”导致的问题。由于接口过“胖”,每一个实现类依赖了它们不需要的接口,使得层与层之间的耦合度增加,结果导致了不需要的接口发生变化时,实现类也不得不相应的发生改变。这里就凸显了我们接口隔离原则的必要性,下面我们就来看看如何通过接口隔离来解决上述问题。
2、接口隔离
我们将IOrder接口分成两个接口来设计
//删除订单接口 public interface IProductOrder { //订单下发操作 void PlantProduct(object order); //订单撤排操作 void CancelProduct(object order); //订单冻结操作 void Hold(object order); //订单删除操作 void Delete(object order); //订单导入操作 void Import(); //订单导出操作 void Export(); } //销售订单接口 public interface ISaleOrder { //订单申请操作 void Apply(object order); //订单审核操作 void Approve(object order); //订单结束操作 void End(object order); }
对应的实现类只需要实现自己需要的接口即可
//生产订单实现类 public class ProduceOrder : IProductOrder { public void PlantProduct(object order) { Console.WriteLine("订单下发排产"); } public void CancelProduct(object order) { Console.WriteLine("订单撤排"); } public void Hold(object order) { Console.WriteLine("订单冻结"); } public void Delete(object order) { Console.WriteLine("订单删除"); } public void Import() { Console.WriteLine("订单导入"); } public void Export() { Console.WriteLine("订单导出"); } } //销售订单实现类 public class SaleOrder : ISaleOrder { public void Apply(object order) { Console.WriteLine("订单申请"); } public void Approve(object order) { Console.WriteLine("订单审核处理"); } public void End(object order) { Console.WriteLine("订单结束"); } }
这样设计就能完美解决上述“胖接口”导致的问题,如果需要增加订单操作,只需要在对应的接口和实现类上面修改即可,这样就不存在依赖不需要接口的情况。通过这种设计,降低了单个接口的复杂度,使得接口的“内聚性”更高,“耦合性”更低。由此可以看出接口隔离原则的必要性。
三、总结
通过以上订单功能的优化,我们看到了接口隔离原则的必要性,当然,关于接口隔离原则和单一职责原则的细节我们也不必过多追究,不管何种原则,能解决我们的设计问题就是好的原则、我们必须遵守的原则。欢迎园友拍砖斧正。如果园友们觉得本文对你有帮助,请帮忙推荐,博主将继续努力~~