单一职责原则SRP ·定义
就一个类而言,应该仅有一个引起它变化的原因。
变化是指具体类的改变,如一个moden类具有连接管理和数据通讯的功能,那么这个类就有连
接管理和数据通讯这两个变化方向,此时就违背了“单一职责的原则”。
·关于“单一职责”
“单一职责”也就是“单一变化原因”。“职责”也就是引起类变化的原因。
“单一职责原则”是面向对象设计的第一个基本原则,它可能是最简单的也可能是最难运用的一个
原则!
通常,一个类的“职责”越多,导致其变化的因素也就越多。因为每个职责都可能是一个变化的轴
线。这与我们在设计类时所采用的方法有关系。一般情况下,我们在设计一个类的时候会把与该
类有关操作都组合到这个类中,这样做的后果就有可能将多个职责“耦合”到了一块,当这个类的
某个职责发生变化时,很难避免类的其它部分不受影响,这最终导致程序的“脆弱”和“僵硬”。
解决这种问题的方法就是“分耦”,将不同的职责分别进行封装,不要将其组合在一个类中,使这
个类只有一个可能会引起它变化的原因。这样做将会使你的程序易于修改和维护。但这个过程可
能是很困难的,因为我们不总是能轻易知道那些职责会发生变化,那些职责应该被提取出来。所
以,你的程序可能会有一个演化的过程,从中得出这些会发生的职责并进行另外的封装。需要注
意的一点就是当实标情况中的职责确实发生了变化,应用该原则才是有意义的。如果一个类组合
了多个职责,但这些职责在实际情况中根本不会发生变化,那完全没有必要提前费尽心机去应用
这个原则。
总结:
SRP好处:
消除耦合,减小因需求变化引起代码僵化性臭味
注意点:
1.一个合理的类,应该仅有一个引起它变化的原因,即单一职责;
2.在没有变化征兆的情况下应用SRP或其他原则是不明智的;
3.在需求实际发生变化时就应该应用SRP等原则来重构代码;
4.使用测试驱动开发会迫使我们在设计出现臭味之前分离不合理代码;
4.如果测试不能迫使职责分离,僵化性和脆弱性的臭味会变得很强烈,那就应该用Facade或
Proxy模式对代码重构;
实例:
违反SRP原则代码:
modem接口明显具有两个职责:连接管理和数据通讯;
class Modem
{
public void dial(string pno);
public void hangup();
public void send(char c);
public void recv();
}
此时应该把的modem的两个职责分到两个类中
class DataChannel
{
public void send(char c);
public void recv();
}
class Connection
{
public void dial(string pno);
public void hangup();
}
新的modem类:
class modem
{
//构造
public modem()
{
datachannel = new DataChannel();
con = new Connection();
}
//字段
private DataChannel datachannel;
private Connection con;
//操作
public void dial(string pno)
{
datachannel.dial();
}
public void hangup()
.......
public void send(char c)
.......
public void recv()
.......
}
一、什么是开放封闭原则
开放封闭原则(Open-Closed Principle):一个软件实体应当对扩展开
放,则修改关闭。
在设计一个模块时,应当使得这个模块可以在不被修改的前提下被扩
展。也就是说,应当可以在不必修改源代码的情况下修改这个模块的行
为。
设计的目的便在于面对需求的改变而保持系统的相对稳定,从而使得
系统可以很容易的从一个版本升级到另一个版本。
二、怎样做到开放封闭原则
实际上,绝对封闭的系统是不存在的。无论模块是怎么封闭,到最后,
总还是有一些无法封闭的变化。而我们的思路就是:既然不能做到完全
封闭,那我们就应该对那些变化封闭,那些变化隔离做出选择。我们做
出选择,然后将那些无法封闭的变化抽象出来,进行隔离,允许扩展,
尽可能的减少系统的开发。当系统变化来临时,我们要及时的做出反应。
我们并不害怕改变的到来。当变化到来时,我们首先需要做的不是修
改代码,而是尽可能的将变化抽象出来进行隔离,然后进行扩展。面对
需求的变化,对程序的修改应该是尽可能通过添加代码来实现,而不是
通过修改代码来实现。
实际上,变化或者可能的变化来的越早,抽象就越容易,相对的,代
码的维护也就越容易;而当项目接近于完成而来的需求变化,则会使抽
象变得很困难——这个困难,并不是抽象本身的困难,抽象本身并没有
困难,困难在于系统的架构已经完成,修改牵扯的方面太多而使得抽象
工作变得很困难。
三、繁忙的银行业务员
四、轻松的银行业务员
三、开放封闭原则的优越性
1.通过扩展已有的软件系统,可以提供新的行为,以满足对软件的新需
求,是变化中的软件有一定的适应性和灵活性。
2.已有的软件模块,特别是最重要的抽象模块不能再修改,这就使变化
中的软件系统有一定的稳定性和延续性。
没有实现OCP原则的例子:
BankWorker.java
/* * 银行业务员 */public class BankWorker { //负责存款业务 public void
saving() { System.out.println("进行存款操作"); } //负责取款业务 public
void drawing() { System.out.println("进行取款操作"); } //负责转账业务
public void zhuanzhang() { System.out.println("进行转账操作"); } //负
责基金的申购 public void jijin() { System.out.println("进行基金申购操
作"); }}
MainClass.java
public class MainClass { public static void main(String[] args)
{ BankWorker bankWorker = new BankWorker(); //存款 bankWorker.saving();
//取款 bankWorker.drawing(); //转账 bankWorker.zhuanzhang(); //基金
bankWorker.jijin(); }}
实现了OCP原则的代码例子:
BankWorker.java
package com.ibeifeng.ex2;/* * 银行业务员接口,是所有银行业务员的抽象父
类。 */public interface BankWorker { public void operation();}
SavingBankWorker.java
package com.ibeifeng.ex2;/* * 负责存款业务的业务员 */public class
SavingBankWorker implements BankWorker { public void operation()
{ System.out.println("进行存款操作"); }}
DrawingBankWorker.java
package com.ibeifeng.ex2;/* * 负责取款业务的业务员 */public class
DrawingBankWorker implements BankWorker{ public void operation()
{ System.out.println("进行取款操作"); }}
ZhuanZhangBankWorker.java
package com.ibeifeng.ex2;/* * 负责转账业务的业务员 */public class
ZhuanZhangBankWorker implements BankWorker { public void operation()
{ System.out.println("进行转账操作"); }}
JiJinBankWorker.java
package com.ibeifeng.ex2;public class JiJinBankWorker implements
BankWorker { public void operation() { System.out.println("进行基金申
购操作"); }}
MainClass.java
package com.ibeifeng.ex2;public class MainClass { public static void
main(String[] args) { BankWorker bankWorker = new SavingBankWorker();
bankWorker.operation(); BankWorker bankWorker2 = new
DrawingBankWorker(); bankWorker2.operation(); BankWorker bankWorker3 =
new ZhuanZhangBankWorker(); bankWorker3.operation(); BankWorker
bankWorker4 = new JiJinBankWorker(); bankWorker4.operation(); }}
Liskov substitution principle (LSP) –里氏代换原则
软件工程大师Robert C. Martin把里氏代换原则最终简化为一句
话:“Subtypes must be substitutable for their base types”。也就是,
子类必须能够替换成它们的基类。即:子类应该可以替换任何基类能够
出现的地方,并且经过替换以后,代码还能正常工作。另外,不应该在
代码中出现if/else之类对子类类型进行判断的条件。里氏替换原则LSP
是使代码符合开闭原则的一个重要保证。正是由于子类型的可替换性才
使得父类型的模块在无需修改的情况下就可以扩展。
这么说来,似乎有点教条化,我非常建议大家看看这个原则个两个
最经典的案例——“正方形不是长方形”和“鸵鸟不是鸟”。通过这两个案
例,你会明白《墨子小取》中说的——“娣,美人也,爱娣,非爱美人也….
盗,人也;恶盗,非恶人也。”——妹妹虽然是美人,但喜欢妹妹并不代
表喜欢美人。盗贼是人,但讨厌盗贼也并不代表就讨厌人类。这个原则
让你考虑的不是语义上对象的间的关系,而是实际需求的环境。
在很多情况下,在设计初期我们类之间的关系不是很明确,LSP则
给了我们一个判断和设计类之间关系的基准:需不需要继承,以及怎样
设计继承关系。
The Stable Abstractions Principle (SAP) - OO设计的稳定抽象等价原则
2009-02-12 9:53
概要
Packages that are maximally stable should be maximally abstract. Instable packages shouldshould be in proportion to its stability.
最稳定的包应该是最抽象的包。不稳定的包应该是具体的包。包的抽象程度跟它的稳定性成正比。
换成另一个说法是:
Stable packages should be abstract packages.
稳定的包应该是抽象的包。
包的稳定抽象等价原则
我们在The Stable Dependencies Principle (SDP) - OO设计的稳定依赖原则 一文中谈到了包的定性。
一个包的抽象程度越高,它的稳定性就越高。反之,它的稳定性就越低。一个稳定的包必须是抽象稳定的包的构成
抽象类或接口通过子类继承扩展行为,这表示抽象类或接口比它们的子类更具有稳定性。总之,为接口的比率;它们的子类可以放在另一个不稳定的包内,该包依赖上述稳定的包,从而遵循了稳定理想的体系结构应该是:
不稳定的(容易改变的)包处于上层
- 它们是具体的包实现
稳定的(不容易改变的)包处于下层
- 不容易改变,但容易扩展
- 接口比实现(具体的运行代码)在内在特性上更具有稳定性
图1:遵循稳定依赖原则(SDP)的理想的体系结构
小结
稳定抽象等价原则(SAP)为我们解决包之间的关系耦合问题。在设计包结构时,稳定的包应该是包应该是具体的(由具体的实现类
The Stable Dependencies Principle (SDP) - OO设计的稳定依赖原则
2009-02-12 9:44
概要
The dependencies between packages in a design should be in the direction of the stabilitdepend upon packages that are more stable that it is.
一个设计中的包之间的依赖应该朝着稳定的方向进行。一个包只应该依赖那些比自己更稳定的包。
换成另一个说法是:
Depend in the direction of stability.
朝着稳定的方向进行依赖。
包的依赖
如果一个包A 中的类引用了包B中的类,我们称包A依赖包B。
“依赖”在具体的程序语言中表现为,如果A依赖B,C/C++语言则在A包的文件/类中通过#inclA包的类中通过import语句引入B包中的类。
图1(包A依赖包B)
虚线表示一种依赖关系,箭头表示依赖的方向,箭头所在的一侧就是被依赖的包。
包的稳定依赖原则
包应该依赖比自己更稳定的包。因为如果依赖一个不稳定的包,那么当这个不稳定的包发生变化时稳定了。
所谓稳定,在现实生活中是指一个物体具有稳固不变的属性使它很难发生变化。应用到软件概念上软件很难发生改变,或更确切地说,是不需要发生改变。一个设计良好,能应对各种变化不需要修一个软件常常需要对应某个事先没有预测到的用户需求而不得不发生改变,当这种改变发生时,能工作(包括软件本身以及依赖它的其它软件实体等),我们也会认为该软件是相对稳定的。
怎么样让一个软件足够稳定呢?一个确切的方法是,让大量其它软件的包依赖它。一个包被很多其包为了协调其他包必须做很多的工作来对应各种变化(责任的负担者)。
图1:稳定的包X
我们认为X是稳定的,因为:
- X被很多其他包依赖。相当于责任担当着。
- X没有依赖别的包,它是独立的。
相反,下面列出了一个非常不稳定的包Y,如图:
图2:不稳定的包Y
我们认为Y是不稳定的,因为:
- Y没有被其他的包所依赖。不是责任的担当着。
- Y依赖很多别的包。
包的稳定性的判断原则
可以通过下面的方法来判断一个包的稳定系数:
Ca:Afferent Coupling。向心耦合。依赖该包(包含的类)的外部包(类)的数目(i.e. incomCe: Efferent Coupling。离心耦合。被该包依赖的外部包的数目(i.e. outgoing dependencieI: Instability。不稳定性。I=Ce/(Ce+Ca)。它的值处于[0,1]之间。
如图1,X的Ce=0,所以不稳定性I=0,它是稳定的。相反,如图2,Y的Ce=3,Ca=0,所以它的
SDP要求一个包的不稳定性I要大于它所依赖的包的不稳定性。“Depend upon packages whose 换句话说,沿着依赖的方向,包的不稳定性应该逐渐降低,稳定性应该逐渐升高。
小结
稳定依赖原则(SDP)为我们解决包之间的关系耦合问题。在设计包结构时,包应该只依赖比自己
The Acyclic Dependencies Principle (ADP) - OO设计的无环依赖原则
2009-02-12 9:28
概要
The dependency structure between packages must be a directed acyclic graph (DAG). That is,structure.
包之间的依赖结构必须是一个直接的无环图形(DAG)。也就是说,在依赖结构中不允许出现环(
换成另一个说法是:
The dependencies between packages must not form cycles.
包之间的依赖不能是一个环状形式。
包的依赖
如果一个包A 中的类引用了包B中的类,我们称包A依赖包B。
“依赖”在具体的程序语言中表现为,如果A依赖B,C/C++语言则在A包的文件/类中通过#inclA包的类中通过import语句引入B包中的类。
图1(包A依赖包B)
虚线表示一种依赖关系,箭头表示依赖的方向,箭头所在的一侧就是被依赖的包。
包的循环依赖
我们上面讨论了并用图形表示了包之间的依赖关系。如果存在2个或2个以上的包,它们之间的依循环依赖关系。
也就是说它们的依赖结构图根据箭头的方向形成了一个环状的闭合图形。如图:
图2:包的循环依赖
如图:A依赖B,B依赖C,C依赖A,形成了一个环状依赖。
包的非循环依赖原则
包是一个比较合适的发布粒度,当修改了包中的代码(类,模块等)并发布新的版本时,我们需要布之后,还需要验证系统是否能在新发布的版本下正常运作。
如果多个包之间形成了循环依赖,比如如图2,A依赖B,B依赖C,C依赖A,我们修改了B并需以发布时应该包含C,但C同时又依赖A,所以又应该把A也包含进发布版本里。
也就是说,依赖结构中,出现在环内的所有包都不得不一起发布。它们形成了一个高耦合体,当项包与包之间的关系便变得错综复杂,各种测试也将变得非常困难,常常会因为某个不相关的包中的杂,需要把所有的包一起发布,无疑增加了发布后的验证难度。
循环依赖的打破方法
如果包的依赖形成了环状结构,怎么样打破这种循环依赖呢?
有2种方法可以打破这种循环依赖关系:第一种方法是创建新的包,第二种方法是使用DIP(依赖则。
方法一:创建新的包
比如对于图2这种依赖结构:
图2:包的循环依赖
包C要依赖包A,必定A中包含有A,C共通使用的类,把这些共同类抽出来放在一个新的包D里。赖D,从而打破了循环依赖关系。如图:
这样,包的依赖关系就从A->B->C->A变成了:
A->B->C->D
A->D
方法二:DIP与ISP设计原则
ISP(接口分隔原则)可以剔除美用到的接口。DIP(依赖倒置原则)在类的调用之间引入抽象层。
如图,,包A依赖包B(因为包A中的类U使用了包B中的类X);反过来,包B又依赖包A
(因为
包A,包B之间就形成了一种循环依赖。
我们使用DIP设计原则为V抽象一个接口IVforY,并把该接口放在B包中。
这样就把Y对V的调用转化为:
V继承IVforY
Y调用IVforY
如图:
这样一来,包B中的类就不依赖任何包A中的类了。
小结
无环依赖原则(ADP)为我们解决包之间的关系耦合问题。在设计包结构时,不能有循环依赖。
一、ISP简介(ISP--Interface Segregation Principle):
使用多个专门的接口比使用单一的总接口要好。
一个类对另外一个类的依赖性应当是建立在最小的接口上的。
一个接口代表一个角色,不应当将不同的角色都交给一个接口。没有关系的接口
合并在一起,形成一个臃肿的大接口,这是对角色和接口的污染。
“不应该强迫客户依赖于它们不用的方法。接口属于客户,不属于它所在的类层
次结构。”这个说得很明白了,再通俗点说,不要强迫客户使用它们不用的方法,
如果强迫用户使用它们不使用的方法,那么这些客户就会面临由于这些不使用的
方法的改变所带来的改变。
二、举例说明:
参考下图的设计,在这个设计里,取款、存款、转帐都使用一个通用界面接口,
也就是说,每一个类都被强迫依赖了另两个类的接口方法,那么每个类有可能因
为另外两个类的方法(跟自己无关)而被影响。拿取款来说,它根本不关心“存款
操作”和“转帐操作”,可是它却要受到这两个方法的变化的影响。
那么我们该如何解决这个问题呢?参考下图的设计,为每个类都单独设计专门的操作接口,使得它们只依赖于它们关系的方法,这样就不会互相影了!
单一职责原则SRP ·定义
就一个类而言,应该仅有一个引起它变化的原因。
变化是指具体类的改变,如一个moden类具有连接管理和数据通讯的功能,那么这个类就有连
接管理和数据通讯这两个变化方向,此时就违背了“单一职责的原则”。
·关于“单一职责”
“单一职责”也就是“单一变化原因”。“职责”也就是引起类变化的原因。
“单一职责原则”是面向对象设计的第一个基本原则,它可能是最简单的也可能是最难运用的一个
原则!
通常,一个类的“职责”越多,导致其变化的因素也就越多。因为每个职责都可能是一个变化的轴
线。这与我们在设计类时所采用的方法有关系。一般情况下,我们在设计一个类的时候会把与该
类有关操作都组合到这个类中,这样做的后果就有可能将多个职责“耦合”到了一块,当这个类的
某个职责发生变化时,很难避免类的其它部分不受影响,这最终导致程序的“脆弱”和“僵硬”。
解决这种问题的方法就是“分耦”,将不同的职责分别进行封装,不要将其组合在一个类中,使这
个类只有一个可能会引起它变化的原因。这样做将会使你的程序易于修改和维护。但这个过程可
能是很困难的,因为我们不总是能轻易知道那些职责会发生变化,那些职责应该被提取出来。所
以,你的程序可能会有一个演化的过程,从中得出这些会发生的职责并进行另外的封装。需要注
意的一点就是当实标情况中的职责确实发生了变化,应用该原则才是有意义的。如果一个类组合
了多个职责,但这些职责在实际情况中根本不会发生变化,那完全没有必要提前费尽心机去应用
这个原则。
总结:
SRP好处:
消除耦合,减小因需求变化引起代码僵化性臭味
注意点:
1.一个合理的类,应该仅有一个引起它变化的原因,即单一职责;
2.在没有变化征兆的情况下应用SRP或其他原则是不明智的;
3.在需求实际发生变化时就应该应用SRP等原则来重构代码;
4.使用测试驱动开发会迫使我们在设计出现臭味之前分离不合理代码;
4.如果测试不能迫使职责分离,僵化性和脆弱性的臭味会变得很强烈,那就应该用Facade或
Proxy模式对代码重构;
实例:
违反SRP原则代码:
modem接口明显具有两个职责:连接管理和数据通讯;
class Modem
{
public void dial(string pno);
public void hangup();
public void send(char c);
public void recv();
}
此时应该把的modem的两个职责分到两个类中
class DataChannel
{
public void send(char c);
public void recv();
}
class Connection
{
public void dial(string pno);
public void hangup();
}
新的modem类:
class modem
{
//构造
public modem()
{
datachannel = new DataChannel();
con = new Connection();
}
//字段
private DataChannel datachannel;
private Connection con;
//操作
public void dial(string pno)
{
datachannel.dial();
}
public void hangup()
.......
public void send(char c)
.......
public void recv()
.......
}
一、什么是开放封闭原则
开放封闭原则(Open-Closed Principle):一个软件实体应当对扩展开
放,则修改关闭。
在设计一个模块时,应当使得这个模块可以在不被修改的前提下被扩
展。也就是说,应当可以在不必修改源代码的情况下修改这个模块的行
为。
设计的目的便在于面对需求的改变而保持系统的相对稳定,从而使得
系统可以很容易的从一个版本升级到另一个版本。
二、怎样做到开放封闭原则
实际上,绝对封闭的系统是不存在的。无论模块是怎么封闭,到最后,
总还是有一些无法封闭的变化。而我们的思路就是:既然不能做到完全
封闭,那我们就应该对那些变化封闭,那些变化隔离做出选择。我们做
出选择,然后将那些无法封闭的变化抽象出来,进行隔离,允许扩展,
尽可能的减少系统的开发。当系统变化来临时,我们要及时的做出反应。
我们并不害怕改变的到来。当变化到来时,我们首先需要做的不是修
改代码,而是尽可能的将变化抽象出来进行隔离,然后进行扩展。面对
需求的变化,对程序的修改应该是尽可能通过添加代码来实现,而不是
通过修改代码来实现。
实际上,变化或者可能的变化来的越早,抽象就越容易,相对的,代
码的维护也就越容易;而当项目接近于完成而来的需求变化,则会使抽
象变得很困难——这个困难,并不是抽象本身的困难,抽象本身并没有
困难,困难在于系统的架构已经完成,修改牵扯的方面太多而使得抽象
工作变得很困难。
三、繁忙的银行业务员
四、轻松的银行业务员
三、开放封闭原则的优越性
1.通过扩展已有的软件系统,可以提供新的行为,以满足对软件的新需
求,是变化中的软件有一定的适应性和灵活性。
2.已有的软件模块,特别是最重要的抽象模块不能再修改,这就使变化
中的软件系统有一定的稳定性和延续性。
没有实现OCP原则的例子:
BankWorker.java
/* * 银行业务员 */public class BankWorker { //负责存款业务 public void
saving() { System.out.println("进行存款操作"); } //负责取款业务 public
void drawing() { System.out.println("进行取款操作"); } //负责转账业务
public void zhuanzhang() { System.out.println("进行转账操作"); } //负
责基金的申购 public void jijin() { System.out.println("进行基金申购操
作"); }}
MainClass.java
public class MainClass { public static void main(String[] args)
{ BankWorker bankWorker = new BankWorker(); //存款 bankWorker.saving();
//取款 bankWorker.drawing(); //转账 bankWorker.zhuanzhang(); //基金
bankWorker.jijin(); }}
实现了OCP原则的代码例子:
BankWorker.java
package com.ibeifeng.ex2;/* * 银行业务员接口,是所有银行业务员的抽象父
类。 */public interface BankWorker { public void operation();}
SavingBankWorker.java
package com.ibeifeng.ex2;/* * 负责存款业务的业务员 */public class
SavingBankWorker implements BankWorker { public void operation()
{ System.out.println("进行存款操作"); }}
DrawingBankWorker.java
package com.ibeifeng.ex2;/* * 负责取款业务的业务员 */public class
DrawingBankWorker implements BankWorker{ public void operation()
{ System.out.println("进行取款操作"); }}
ZhuanZhangBankWorker.java
package com.ibeifeng.ex2;/* * 负责转账业务的业务员 */public class
ZhuanZhangBankWorker implements BankWorker { public void operation()
{ System.out.println("进行转账操作"); }}
JiJinBankWorker.java
package com.ibeifeng.ex2;public class JiJinBankWorker implements
BankWorker { public void operation() { System.out.println("进行基金申
购操作"); }}
MainClass.java
package com.ibeifeng.ex2;public class MainClass { public static void
main(String[] args) { BankWorker bankWorker = new SavingBankWorker();
bankWorker.operation(); BankWorker bankWorker2 = new
DrawingBankWorker(); bankWorker2.operation(); BankWorker bankWorker3 =
new ZhuanZhangBankWorker(); bankWorker3.operation(); BankWorker
bankWorker4 = new JiJinBankWorker(); bankWorker4.operation(); }}
Liskov substitution principle (LSP) –里氏代换原则
软件工程大师Robert C. Martin把里氏代换原则最终简化为一句
话:“Subtypes must be substitutable for their base types”。也就是,
子类必须能够替换成它们的基类。即:子类应该可以替换任何基类能够
出现的地方,并且经过替换以后,代码还能正常工作。另外,不应该在
代码中出现if/else之类对子类类型进行判断的条件。里氏替换原则LSP
是使代码符合开闭原则的一个重要保证。正是由于子类型的可替换性才
使得父类型的模块在无需修改的情况下就可以扩展。
这么说来,似乎有点教条化,我非常建议大家看看这个原则个两个
最经典的案例——“正方形不是长方形”和“鸵鸟不是鸟”。通过这两个案
例,你会明白《墨子小取》中说的——“娣,美人也,爱娣,非爱美人也….
盗,人也;恶盗,非恶人也。”——妹妹虽然是美人,但喜欢妹妹并不代
表喜欢美人。盗贼是人,但讨厌盗贼也并不代表就讨厌人类。这个原则
让你考虑的不是语义上对象的间的关系,而是实际需求的环境。
在很多情况下,在设计初期我们类之间的关系不是很明确,LSP则
给了我们一个判断和设计类之间关系的基准:需不需要继承,以及怎样
设计继承关系。
The Stable Abstractions Principle (SAP) - OO设计的稳定抽象等价原则
2009-02-12 9:53
概要
Packages that are maximally stable should be maximally abstract. Instable packages shouldshould be in proportion to its stability.
最稳定的包应该是最抽象的包。不稳定的包应该是具体的包。包的抽象程度跟它的稳定性成正比。
换成另一个说法是:
Stable packages should be abstract packages.
稳定的包应该是抽象的包。
包的稳定抽象等价原则
我们在The Stable Dependencies Principle (SDP) - OO设计的稳定依赖原则 一文中谈到了包的定性。
一个包的抽象程度越高,它的稳定性就越高。反之,它的稳定性就越低。一个稳定的包必须是抽象稳定的包的构成
抽象类或接口通过子类继承扩展行为,这表示抽象类或接口比它们的子类更具有稳定性。总之,为接口的比率;它们的子类可以放在另一个不稳定的包内,该包依赖上述稳定的包,从而遵循了稳定理想的体系结构应该是:
不稳定的(容易改变的)包处于上层
- 它们是具体的包实现
稳定的(不容易改变的)包处于下层
- 不容易改变,但容易扩展
- 接口比实现(具体的运行代码)在内在特性上更具有稳定性
图1:遵循稳定依赖原则(SDP)的理想的体系结构
小结
稳定抽象等价原则(SAP)为我们解决包之间的关系耦合问题。在设计包结构时,稳定的包应该是包应该是具体的(由具体的实现类
The Stable Dependencies Principle (SDP) - OO设计的稳定依赖原则
2009-02-12 9:44
概要
The dependencies between packages in a design should be in the direction of the stabilitdepend upon packages that are more stable that it is.
一个设计中的包之间的依赖应该朝着稳定的方向进行。一个包只应该依赖那些比自己更稳定的包。
换成另一个说法是:
Depend in the direction of stability.
朝着稳定的方向进行依赖。
包的依赖
如果一个包A 中的类引用了包B中的类,我们称包A依赖包B。
“依赖”在具体的程序语言中表现为,如果A依赖B,C/C++语言则在A包的文件/类中通过#inclA包的类中通过import语句引入B包中的类。
图1(包A依赖包B)
虚线表示一种依赖关系,箭头表示依赖的方向,箭头所在的一侧就是被依赖的包。
包的稳定依赖原则
包应该依赖比自己更稳定的包。因为如果依赖一个不稳定的包,那么当这个不稳定的包发生变化时稳定了。
所谓稳定,在现实生活中是指一个物体具有稳固不变的属性使它很难发生变化。应用到软件概念上软件很难发生改变,或更确切地说,是不需要发生改变。一个设计良好,能应对各种变化不需要修一个软件常常需要对应某个事先没有预测到的用户需求而不得不发生改变,当这种改变发生时,能工作(包括软件本身以及依赖它的其它软件实体等),我们也会认为该软件是相对稳定的。
怎么样让一个软件足够稳定呢?一个确切的方法是,让大量其它软件的包依赖它。一个包被很多其包为了协调其他包必须做很多的工作来对应各种变化(责任的负担者)。
图1:稳定的包X
我们认为X是稳定的,因为:
- X被很多其他包依赖。相当于责任担当着。
- X没有依赖别的包,它是独立的。
相反,下面列出了一个非常不稳定的包Y,如图:
图2:不稳定的包Y
我们认为Y是不稳定的,因为:
- Y没有被其他的包所依赖。不是责任的担当着。
- Y依赖很多别的包。
包的稳定性的判断原则
可以通过下面的方法来判断一个包的稳定系数:
Ca:Afferent Coupling。向心耦合。依赖该包(包含的类)的外部包(类)的数目(i.e. incomCe: Efferent Coupling。离心耦合。被该包依赖的外部包的数目(i.e. outgoing dependencieI: Instability。不稳定性。I=Ce/(Ce+Ca)。它的值处于[0,1]之间。
如图1,X的Ce=0,所以不稳定性I=0,它是稳定的。相反,如图2,Y的Ce=3,Ca=0,所以它的
SDP要求一个包的不稳定性I要大于它所依赖的包的不稳定性。“Depend upon packages whose 换句话说,沿着依赖的方向,包的不稳定性应该逐渐降低,稳定性应该逐渐升高。
小结
稳定依赖原则(SDP)为我们解决包之间的关系耦合问题。在设计包结构时,包应该只依赖比自己
The Acyclic Dependencies Principle (ADP) - OO设计的无环依赖原则
2009-02-12 9:28
概要
The dependency structure between packages must be a directed acyclic graph (DAG). That is,structure.
包之间的依赖结构必须是一个直接的无环图形(DAG)。也就是说,在依赖结构中不允许出现环(
换成另一个说法是:
The dependencies between packages must not form cycles.
包之间的依赖不能是一个环状形式。
包的依赖
如果一个包A 中的类引用了包B中的类,我们称包A依赖包B。
“依赖”在具体的程序语言中表现为,如果A依赖B,C/C++语言则在A包的文件/类中通过#inclA包的类中通过import语句引入B包中的类。
图1(包A依赖包B)
虚线表示一种依赖关系,箭头表示依赖的方向,箭头所在的一侧就是被依赖的包。
包的循环依赖
我们上面讨论了并用图形表示了包之间的依赖关系。如果存在2个或2个以上的包,它们之间的依循环依赖关系。
也就是说它们的依赖结构图根据箭头的方向形成了一个环状的闭合图形。如图:
图2:包的循环依赖
如图:A依赖B,B依赖C,C依赖A,形成了一个环状依赖。
包的非循环依赖原则
包是一个比较合适的发布粒度,当修改了包中的代码(类,模块等)并发布新的版本时,我们需要布之后,还需要验证系统是否能在新发布的版本下正常运作。
如果多个包之间形成了循环依赖,比如如图2,A依赖B,B依赖C,C依赖A,我们修改了B并需以发布时应该包含C,但C同时又依赖A,所以又应该把A也包含进发布版本里。
也就是说,依赖结构中,出现在环内的所有包都不得不一起发布。它们形成了一个高耦合体,当项包与包之间的关系便变得错综复杂,各种测试也将变得非常困难,常常会因为某个不相关的包中的杂,需要把所有的包一起发布,无疑增加了发布后的验证难度。
循环依赖的打破方法
如果包的依赖形成了环状结构,怎么样打破这种循环依赖呢?
有2种方法可以打破这种循环依赖关系:第一种方法是创建新的包,第二种方法是使用DIP(依赖则。
方法一:创建新的包
比如对于图2这种依赖结构:
图2:包的循环依赖
包C要依赖包A,必定A中包含有A,C共通使用的类,把这些共同类抽出来放在一个新的包D里。赖D,从而打破了循环依赖关系。如图:
这样,包的依赖关系就从A->B->C->A变成了:
A->B->C->D
A->D
方法二:DIP与ISP设计原则
ISP(接口分隔原则)可以剔除美用到的接口。DIP(依赖倒置原则)在类的调用之间引入抽象层。
如图,,包A依赖包B(因为包A中的类U使用了包B中的类X);反过来,包B又依赖包A
(因为
包A,包B之间就形成了一种循环依赖。
我们使用DIP设计原则为V抽象一个接口IVforY,并把该接口放在B包中。
这样就把Y对V的调用转化为:
V继承IVforY
Y调用IVforY
如图:
这样一来,包B中的类就不依赖任何包A中的类了。
小结
无环依赖原则(ADP)为我们解决包之间的关系耦合问题。在设计包结构时,不能有循环依赖。
一、ISP简介(ISP--Interface Segregation Principle):
使用多个专门的接口比使用单一的总接口要好。
一个类对另外一个类的依赖性应当是建立在最小的接口上的。
一个接口代表一个角色,不应当将不同的角色都交给一个接口。没有关系的接口
合并在一起,形成一个臃肿的大接口,这是对角色和接口的污染。
“不应该强迫客户依赖于它们不用的方法。接口属于客户,不属于它所在的类层
次结构。”这个说得很明白了,再通俗点说,不要强迫客户使用它们不用的方法,
如果强迫用户使用它们不使用的方法,那么这些客户就会面临由于这些不使用的
方法的改变所带来的改变。
二、举例说明:
参考下图的设计,在这个设计里,取款、存款、转帐都使用一个通用界面接口,
也就是说,每一个类都被强迫依赖了另两个类的接口方法,那么每个类有可能因
为另外两个类的方法(跟自己无关)而被影响。拿取款来说,它根本不关心“存款
操作”和“转帐操作”,可是它却要受到这两个方法的变化的影响。
那么我们该如何解决这个问题呢?参考下图的设计,为每个类都单独设计专门的操作接口,使得它们只依赖于它们关系的方法,这样就不会互相影了!