面向對象實踐之路:提升抽象層次

作者: 橫刀天笑  來源: 博客園  發布時間: 2011-05-01 11:23  閱讀: 2885 次  推薦: 0   原文鏈接   [收藏]  

  多少次有人問我如何構建一個比較好的類階層次,如何使用面向對象進行設計,或者問為什么我看了那么多面向對象和設計模式的書一到使用的時候卻總是寫出面向過程的代碼。每當我碰到這些問題的時候我總是回答,其實我也不知道。真的,其實我也不知道。

  雖然我總是張口閉口面向對象,總是看到一個問題后就談這個有點XXX模式的影子,但大部分時候碰到一個問題我還是一片空白,不知道如何去分析設計和實現出好的面向對象。所以,我只想談談我是如何實踐面向對象的,這對我自己有用但不一定對你有用。嗯,回到正題。

  回顧編程方法的發展史,我想不外乎兩個字:抽象。

  從最早的匯編語言中使用的子例程到結構化編程,然后到面向對象、面向組件以及面向服務。我覺得都是不斷地提升抽象的層次。所以編程方法沒有好壞,只有適合不適合。在匯編時代問題規模都很小,所以我們需要的抽象能力不需要太強。而現代的軟件項目,問題的規模非常龐大,需要考慮的事情非常多(雖然純粹的技術含量不一定有匯編時代的高),我們就必須使用抽象層次更高的方法來匹配我們的問題規模。

  面向對象編程方法的出現也不外乎如此,所以我們在使用面向對象方法開發的時候一個目的就是要提升抽象層次(比如現在由有人提出面向對象已經不足以匹配并行軟計算的抽象層次,所以不再教授面向對象,轉而教授函數編程)。

  而我覺得提升抽象層次的一個好方法就是用代碼與人交談,用代碼來表達你的思想,在代碼中形成一個個“概念”,或者說代碼就是用來傳遞知識的。我將概念二字加上引號并加粗是有特別強調的意思,這個在后文我會談談什么是這里所說的概念。我不想在表面文字上談論太多,我們來實踐吧。

  注意,本文代碼僅僅為了說明一些問題或現象,并不考慮業務上的合理性,讀者可以自行分辨然后拿自己的業務代碼進行思考。

  方法的參數

  不知道你寫過或見過下面的代碼沒有:

 
bool IsValid(string userName, string password, string email, int status);

  如果你見過然后還放任不管,那么你就喪失了一次提升抽象層次的機會。Robert.Martin在《Clean Code》里談到,方法的參數不宜過多,如果有過多的參數我們就要特別審視一番。當我們審視上面的方法參數時,我們發現其實這些參數都應該屬于同一個東西,而現在我們沒有。類似字符串、整型這些類型,抽象層次太低了,沒有任何的領域含義。而且我們發現,上面方法的參數和方法名字IsValid也不在同一個抽象的層次上,我們閱讀到IsValid這個方法時,我們甚至不能一下子了解其目的:哦,這個方法是檢查用戶名、密碼、郵件以及狀態的有效性么?哎,多么啰嗦,還不一定對。如果我們多看兩眼這些參數我們或許會寫出這樣的代碼:

 
bool IsValid(User user);

public class User
{

public string UserName{get;set;}
public string Password{get;set;}
public string Email{get;set;}

//這里僅僅為了方便使用整型代替枚舉,其實可以新建一個枚舉來提升這里的抽象層次-_-
public int Status{get;set;}
}

  哦,看到這個方法后我就知道這個方法是用來檢查用戶的合法性的,除此之外我們還創建了一個概念“用戶”,我們將一堆零散的數據聚合成一個新對象,向你傳遞了一個知識。

  園子里的沙沙對這一點寫了另外一篇文章,給我提了個醒。我這里所說的可能有所誤導:我并不是說如果參數多我們就硬生生的將這幾個參數合并到一個類里去,我們首先要考察的是這幾個參數之間的關系,如果沒有任何關系就這樣硬湊其實是不合理的。

  但是我想說如果你的一個方法參數很多,比如三四個,而且這幾個參數之間居然還沒啥關系,你自己想想到底發生了什么事?

  不要迷戀哥

  然后我們再進入到IsValid方法內部看看:

 
bool IsValid(User user)
{

if(user.UserName.Length > 0 && user.Email.Contains("@")){
//....
}
//...
}

  我們發現這個方法內部干的事兒就是不斷的詢問User對象,探尋User對象的內部狀態然后做出一些判斷。探尋別人的隱私是不好的,這么強的依賴別人的內部狀態違反了面向對象封裝的原則。如果我們的一段代碼總是不斷的探尋另外一個對象的內部狀態,然后做出一些判斷,我們就應該思索:這個被詢問的對象是不是缺少一個概念?或者說這段代碼應該屬于被詢問的那個對象而不是現在的這個對象:

 
public class User
{

public bool IsValid()
{

if(userName.Length > 0 && email.Contains("@"))
{

//....
}
//...
}
}

  這里抱成一團

  有的時候我們發現,方法內部的某部分代碼圍繞著一個中心點在糾結。跟方法內的其他代碼有些間隙,最重要的是這段代碼嚴重的影響了整個方法的可讀性,因為有了這段代碼,方法體變長,方法更難讀懂。這個時候我們應該對方法內部的代碼排排序,檢查一下這些代碼,是不是有的代碼是為了干一件事兒(方法剛寫的時候可能是這樣分的,為了同一個目的的代碼都放到一塊兒,但隨著時間流逝,新代碼不斷的加入可能違反了這個原則)。比如上面的IsValid方法或許如下面這樣實現:

 
public class User
{

public bool IsValid()
{

if(userName.Length > 0 && (email.Contains("@") && (email.EndsWith(".com") || email.EndsWith(".biz")...)))
{

//....
}
//...
}
}

  那一長串&&和||不就是為了驗證Email的合法性么?因為它的存在搞得這里一團糟,如果我們能進一步提升抽象層次:將一團代碼提取到一個方法,用方法名來描述方法本身:

 
public class User
{

public bool IsValid()
{

if(userName.Length > 0 && IsEmailValid())
{

//....
}
//...
}

bool IsEmailValid()
{

return email.Contains("@") && (email.EndsWith(".com") || email.EndsWith(".biz")...));
}
}

  不僅這團代碼的抽象層次提高了,IsValid頓時也高貴起來了,好懂多了(不過那團代碼依然存在,不過隱藏在抽象的背后,忘記是不是有這么一個名言:每個漂亮的接口后面都有一個骯臟的實現<玩笑話>)。

  人格分裂

  碰到過這樣的代碼沒有,一個類里五個方法,三個方法訪問a,b,c屬性,另外兩個方法訪問d,e,f屬性。好像有一條隱約可見的分界線將這個類一分為二。有的時候這條分界線并不十分明顯,可能還有一些交叉。這實際上和上面說的提取一個方法類似。我們只是比方法更高一個層次:缺少一個類型將這部分代碼獨立出去。還是看上面的User類,我們發現有那么幾個方法總是圍繞著email在打轉,對User類其他的東西倒不是很關心:

 
public class User
{

public bool IsValid()
{

if(userName.Length > 0 && IsEmailValid())
{

//....
}
//...
}

bool IsEmailValid()
{

return email.Contains("@") && (email.EndsWith(".com") || email.EndsWith(".biz")...));
}


public string EmailAddress()
{

return string.Format("\"{0}\"<1>",userName,email);
}


private string Convert()
{

if(email.IndexOf('#') != -1)
{

return email.Replace('#','@');
}
}
}

  我想或許我們缺少一個Email的概念,這樣就可以將這幾個方法以及其要使用的屬性封裝起來:

 
public class User
{

private Email email;

public bool IsValid()
{

if(userName.Length > 0 && email.IsEmailValid())
{

//....
}
//...
}
}


public class Email
{

private string address;

bool IsEmailValid()
{

return address.Contains("@") && (address.EndsWith(".com") || address.EndsWith(".biz")...));
}


public string EmailAddress(string userName)
{

return string.Format("\"{0}\"<1>",userName,email);
}


private string Convert()
{

if(email.IndexOf('#') != -1)
{

return address.Replace('#','@');
}
}
}

  好了,就談這么多吧。類似的提升抽象層次的例子還有很多很多,無非就是通過重組織代碼,形成一些概念,向閱讀代碼的人傳遞領域的知識。然后我想說的是,面向對象設計或許真的很難,需要豐富的經驗,但是面向對象編程并不難,只需要我們有一顆精益求精的心就可以了。代碼不是寫出來然后就放到那里,然后就不去管了,我們需要時不時的去照顧我們的代碼,然后觀察,然后不斷的去雕刻。

  面向對象很像路邊攤的大媽攤雞蛋餅。不斷的把餅攤薄,餅的每個小塊都很薄,然后就很容易熟。

  面向對象實踐之路其他文章:

  寬接口、窄接口和訪問方法

  或許你需要一些可操作性更強的實踐

0
0
 
標簽:面向對象
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()