Clean Code - 對象和數(shù)據(jù)結(jié)構(gòu)
1、數(shù)據(jù)抽象
不曝露數(shù)據(jù)細節(jié),更愿意以抽象形態(tài)表述數(shù)據(jù)。
代碼 1:
public?class?Point{
????public?double?x;
????public?double?y;
}
代碼 2:
public?interface?Point{
????double?getX();
????double?getY();
????void?setCartesian(double?x,?double?y);
????
????double?getR();
????double?getTheta();
????void?setPolar(double?r,?double?theta);
}
上面兩段代碼都表示笛卡爾兒平面上的一個點,你覺得哪個代碼更好些?
答案是代碼 2,為什么代碼 2 更好一些,原因有以下兩個方面。
首先代碼 1 沒有封裝。
代碼 1 中的 x 和 y 是完全暴露的,任何人都可以直接訪問和設(shè)置新的值。
代碼 2 中加了一層封裝,你只能通過get方法獲取坐標值和set方法設(shè)置原子坐標值。
其次是具象與抽象。
代碼 1 是具象的一個點,只能表示在直角坐標系中的一個點。
代碼 2 是抽象的一個點,既可以表示直角坐標系中的一個點,也可表示極坐標系中的一個點。
2、數(shù)據(jù)、對象的反對稱性(過程式代碼和面向?qū)ο蟠a的反對稱性)
我們用一個例子來說明這個規(guī)則的含義。
現(xiàn)在我們有一個需求,需要繪制三種幾何圖形,分別是正方形、長方形和圓形,并計算每個圖形的面積。
代碼 1 是過程式代碼,每個形狀類都是簡單的數(shù)據(jù)結(jié)構(gòu),只負責存儲數(shù)據(jù),不具備行為。具體的計算行為放在了 Geometry 類中。
代碼 1:
public?class?Square{
????public?Point?topLeft;
????public?double?side;
}
public?class?Rectangle{
????public?Point?topLeft;
????public?double?width;
????public?double?height;
}
public?class?Circle{
????public?Point?center;
????public?double?radius;
}
public?class?Geometry{
????public?final?double?PI?=?3.14159265358;
????
????public?double?area(Object?shape)?throws?NoSuchShapeException{
????????if(shape?instanceof?Square){
????????????Square?square?=?(Square)shape;
????????????return?square.side?*?square.side;
????????}?else?if?(shape?instanceof?Rectangle){
????????????Rectangle?rec?=?(Rectangle)shape;
????????????return?rec.width?*?rec.hight;
????????}?else?if?(shape?instanceof?Circle){
????????????Circle?cir?=?(Circle)shape;
????????????return?PI?*?cir.radius?*?cir.radius;
????????}
????????throw?new?NoSuchShapeException();
????}
}
代碼 2 是面向?qū)ο蟠a。每個形狀類是一個對象,不僅存儲數(shù)據(jù),還包含其行為。這三個形狀類都實現(xiàn) Shape 接口,每種形狀類都有各自的計算面積的方法。
代碼 2:
public?interface?Shape{
????double?area();
}
public?class?Square?implements?Shape{
????private?Point?topLeft;
????private?double?side;
????
????public?double?area(){
????????return?side*side;
????}
}
public?class?Rectangle?implements?Shape{
????private?Point?topLeft;
????private?double?width;
????private?double?height;
????
????public?double?area(){
????????return?width?*?height;
????}
}
public?class?Circle?implements?Shape{
????private?static?final?double?PI?=?3.14159265358;?
????
????private?Point?topLeft;
????private?double?radius;
????
????public?double?area{
????????return?PI?*?radius?*?radius;
????}
}
有人可能會說,既然 Java 是一門面向?qū)ο箝_發(fā)語言,那肯定代碼 2 要優(yōu)于代碼 1,我們在平常的開發(fā)過程中,要多寫代碼 2 這種面向?qū)ο蟠a,少寫代碼 1 這種過程式代碼。
但是!?。〔⒉皇沁@樣的!實際情況是,在某種情況下,過程式代碼優(yōu)于面向?qū)ο蟠a,在另一種情況下,面向?qū)ο蟠a優(yōu)于過程式代碼。我們是選擇過程式代碼還是面向?qū)ο蟠a,需要根據(jù)具體情況具體分析。
舉個栗子!現(xiàn)在增加了一個需求,計算這三種圖形的周長。如果是代碼 1 的話,只需要給 Geometry 類添加一個 perimeter() 函數(shù),三個形狀類不會受到任何影響。如果是代碼 2 的話,需要給每個形狀類都添加一個 perimeter() 函數(shù)。
現(xiàn)在增加另外一個需求,需要添加一個新形狀。如果是代碼 2 的話,只需要添加一個新的形狀類,既有的形狀類不會受到任何影響。如果是代碼 1 的話,Geometry 類的每個函數(shù)都要修改,增加一個 if else 分支。
所以,我們得出以下結(jié)論:
過程式代碼便于在不改動既有數(shù)據(jù)結(jié)構(gòu)的前提下添加新的函數(shù),而面向?qū)ο蟠a便于在不改動既有函數(shù)的前提下添加新類。
所以,對于面向?qū)ο筝^難的事,對于過程式代碼卻較容易,反之亦然!這就是過程式代碼和面向?qū)ο蟠a的反對稱性!由于數(shù)據(jù)結(jié)構(gòu)是過程式代碼的基本單元,對象是面向?qū)ο蟠a的基本單元,所以,這也可以說是數(shù)據(jù)和對象的反對稱性。
在任何復雜系統(tǒng),都會有需要添加新數(shù)據(jù)類型而不是新函數(shù)的時候,這時,對象和面向?qū)ο缶捅容^適合。另一方面,也會有想要添加新函數(shù)而不是數(shù)據(jù)類型的時候,在這種情況下,數(shù)據(jù)結(jié)構(gòu)和過程式代碼更合適。
3、得墨忒耳定律
著名的得墨忒耳定律認為,模塊不應該了解它所操作對象的內(nèi)部情形。
即類? C? 的方法? f? 只應該調(diào)用以下對象的方法:
- C
public?class?C{
??public?void?f1(){
????f2();
??}
??
??public?void?f2(){
??
??}
}
- 由? f? 創(chuàng)建的對象
public?class?C1{
??public?void?f(){
????C2?c2?=?new?C2();
????c2.f();
??}
}
public?class?C2{
??public?void?f(){
????/*...*/
??}
}
- 作為參數(shù)傳給? f? 的對象
public?class?C1{
??public?void?f(C2?c2){
????c2.f();
??}
}
public?class?C2{
??public?void?f(){
????/*...*/
??}
}
- 由? C? 的實體變量持有的對象
public?class?C1{
??private?List?names?=?new?ArrayList();
??
??public?void?f(String?name){
????names.add(name);
??}
}
方法不應調(diào)用由任何函數(shù)返回的對象的方法。
下列代碼違反了得墨忒耳定律,因為它調(diào)用了 getOptions() 函數(shù)返回的對象的 getScratchDir() 函數(shù),又調(diào)用了 getScratchDir() 函數(shù)返回的對象的 getAbsolutePath() 函數(shù)。
final?String?outputDir?=?ctxt.getOptions().getScartchDir().getAbsolutePath();
這類代碼常被稱作火車失事,因為涉及到多個函數(shù)的級聯(lián)調(diào)用,一旦出了問題,不知問題出在哪。最好做類似如下的切分:
Options?opts?=?ctxt.getOptions();
File?scratchDir?=?opts.getScratchDir();
final?String?outputDir?=?scratchDir.getAbsolutePath();
優(yōu)化后的代碼是否違反了得墨忒耳定律呢?
這要看 ctxt、Options、ScratchDir 是對象還是數(shù)據(jù)結(jié)構(gòu),如果是對象,就違反了得墨忒耳定律,因為模塊知道它要操作的所有對象的內(nèi)部情形(為什么這么說呢?看優(yōu)化后的代碼,模塊知道?ctxt 對象包含有多個選項,每個選項都有一個臨時目錄,而每個臨時目錄都有一個絕對路徑。模塊對于它要操作的這三個對象,每個對象內(nèi)部有啥,了解地清清楚楚)。
如果是數(shù)據(jù)結(jié)構(gòu),由于數(shù)據(jù)結(jié)構(gòu)只包含數(shù)據(jù),沒有什么行為,則他們自然會暴露其內(nèi)部數(shù)據(jù)結(jié)構(gòu),得墨忒耳定律也就失效了。
如果是數(shù)據(jù)結(jié)構(gòu),就應該這樣寫代碼:
final?String?outputDir?=?ctxt.options.scratchDir.absolutePath;
如果是對象,這段代碼違反了得墨忒耳定律,那如何優(yōu)化讓其不違反這個定律呢?我們可以將這段代碼的邏輯全部抽取到 ctxt 的某個函數(shù)中,此時 ctxt 隱藏了內(nèi)部結(jié)構(gòu),防止當前函數(shù)因瀏覽它不該知道的對象而違反得墨忒耳定律。
BufferedOutputStream?bos?=?ctxt.createScratchFileStream(classFileName);
4、數(shù)據(jù)傳送對象
DTO(數(shù)據(jù)傳送對象)是一種只有公共變量,沒有函數(shù)(這個函數(shù)是指除 get、set 之外的函數(shù))的類。常見的數(shù)據(jù)傳送對象還有 Bean,這種結(jié)構(gòu)有賦值器和取值器操作私有變量。
見下面的示例 1 和示例 2 ,二者都是 DTO,區(qū)別就是示例 1 只有數(shù)據(jù),由于數(shù)據(jù)權(quán)限是 public ,我們可以直接讀取數(shù)據(jù)或者給數(shù)據(jù)賦值,示例 2 使用 private 隱藏數(shù)據(jù),然后使用取值器 get 和賦值器 set 操作這些私有變量。
示例 1:
public?class?Address{
??public?String?street;
??public?String?city;
??public?String?state;
??public?String?province;
??
??public?Address(String?street,?String?city,?String?state,?String?province){
????this.street?=?street;
????this.city?=?city;
????this.state?=?state;
????this.province?=?province;
??}
}
示例 2:
public?class?Address{
????private?String?street;
????private?String?city;
????private?String?state;
????private?String?province;
????
????public?Address(String?street,?String?city,?String?state,?String?province){
????????this.street?=?street;
????????this.city?=?city;
????????this.state?=?state;
????????this.province?=?province;
????}
????
????public?void?setStreet(String?street){
????????this.street?=?street;
????}
????
????public?String?getStreet(){
????????return?stree;
????}
????
????public?void?setCity(String?city){
????????this.city?=?city;
????}
????
????public?String?getCity(){
????????return?city;
????}
????
????public?void?setState(String?state){
????????this.state?=?state;
????}
????
????public?String?getState(){
????????return?state;
????}
????
????public?void?setProvince(String?province){
????????this.province?=?province;
????}
????
????public?String?getProvince(){
????????return?province;
????}
}
DTO 是非常有用的結(jié)構(gòu),尤其是在與數(shù)據(jù)庫通信,或解析套接字傳遞信息之類的場景之中。
數(shù)據(jù)傳送對象應該是簡單的數(shù)據(jù)結(jié)構(gòu),不應該包含業(yè)務邏輯。如果對象有較多需要處理的業(yè)務邏輯,應當新建類來包含業(yè)務邏輯、隱藏內(nèi)部數(shù)據(jù)進行處理。
舉個栗子來解釋一下,對于 Address 我們有查找地址 find() 和保存地址 save() 的需求,如果把這兩個業(yè)務邏輯寫入 Address 類中,Address 就不能說是一個數(shù)據(jù)傳送對象了。
反例:
public?class?Address{
????private?String?street;
????private?String?city;
????private?String?state;
????private?String?province;
????
????public?Address(String?street,?String?city,?String?state,?String?province){
????????this.street?=?street;
????????this.city?=?city;
????????this.state?=?state;
????????this.province?=?province;
????}
????
????//?所有屬性的?set、get?方法
????
????public?Address?find(){
????/*...*/
????return?address;
??}
??
??public?void?set(String?street,?String?city,?String?state,?String?province){
????Address?address?=?new?Address();
????address.setStreet(street);
????address.setCity(city);
????address.setState(state);
????address.setProvince(province);
??}
}
想要保證 Address 是一個數(shù)據(jù)傳送對象,那么這兩個業(yè)務邏輯就不應該寫到 Address 類中,我們可以這樣操作,Adderss 類依舊保持上述示例 2 的樣子,然后創(chuàng)建一個新的類,在這個新的類中編寫這兩個業(yè)務邏輯。
正例:
public?class?AddressOperator{
??public?Address?find(){
????/*...*/
????return?address;
??}
??
??public?void?set(String?street,?String?city,?String?state,?String?province){
????Address?address?=?new?Address();
????address.setStreet(street);
????address.setCity(city);
????address.setState(state);
????address.setProvince(province);
??}
}
5、小結(jié)
對象曝露行為,隱藏數(shù)據(jù)。便于添加新對象類型而無需修改既有行為,同時也難以在既有對象中添加新的行為。
數(shù)據(jù)結(jié)構(gòu)曝露數(shù)據(jù),沒有明顯的行為,便于向既有數(shù)據(jù)結(jié)構(gòu)添加新的行為,同時也難以向既有函數(shù)添加新的數(shù)據(jù)結(jié)構(gòu)。
在任何系統(tǒng)中,我們有時會希望能夠靈活地添加新數(shù)據(jù)類型,所以更喜歡在這部分使用對象和面向?qū)ο蟠a。另外一些時候,我們希望能靈活地添加新行為,這時我們更喜歡使用數(shù)據(jù)結(jié)構(gòu)和過程式代碼。優(yōu)秀的軟件開發(fā)者能夠根據(jù)手邊工作的性質(zhì)靈活地選擇其中一種手段。
