OOP思維導論

來源:互聯網
上載者:User

資料是什嗎?

程式員每天都在和資料打交道,但我們能否清晰地回答這樣一個問題呢:到底資料是什麼?毫不誇張地講,這個看似簡單的問題在理解OOP思想中處於至關重要的地位。我們先從整數的例子來探討資料的本質:整數..., -1,0, 1, 2,...的本質是什麼呢?首先,我們對資料的直觀認識來源於其符號表示"0", "1","2"等,但我們很容易意識到孤立地把數1看作一個符號"1"是沒有意義的,符號的意義無法脫離它所處的環境而單獨存在。這裡的環境即整數運算,我們必須從運算中考察其性質。作為整數的1比符號"1"有更多地內涵(註:這裡的符號不是電腦中的字串,只是一種記號)。資料中蘊含的東西除了看得見的直觀的符號,還有與之相關的運算和運算規則,它們是有機聯絡的整體。我們給這個整體取個名字:類型(Type)。用數學的語言,類型定義了符號集和運算規則集,其中,符號集包括運算子(Operator)和運算元(Operand),記為:Type=(SymbolSet, RuleSet)。對於整數類型integer,SymbolSet={..., "-1", "0", "1", "2,", ...}∪{"=", "<", ">", "+", "-", "*", "/", "%", "^"},RuleSet=整數運算規則集(加法結合律,交換律,加減逆運算,乘法與加法關係等)。


為了與無類型的符號區分,強調資料的類型性,我們把資料又稱為某種類型的對象(Object),比如:1是整數類型的對象。這裡的對象並非OOP語言的類的執行個體,而是一種數學概念。


類型不變性

一個有趣的現象是,在類型定義中,符號集的選擇並不是那麼重要。如果我們不用阿拉伯數字,而是採用羅馬數字來表示整數,整數的性質並未發生變化。著名的丘奇計數(Church Numberals)用“把任何其他函數f映射到它的n重函數複合”的lambda函數形式表示自然數n:

0 ≡ λf.λx. x

1 ≡ λf.λx. f x

2 ≡ λf.λx. f (f x)

3 ≡ λf.λx. f (f (f x))

...

n ≡ λf.λx. fn x

加法函數 plus(m,n) = m + n 利用了恒等式 f(m + n)(x) = fm(fn(x))。

plus ≡ λm.λn.λf.λx. m f (n f x)

乘法函數 times(m,n) = m * n 利用了恒等式 f(m * n) = (fm)n

mult ≡ λm.λn.λf. n (m f)

指數函數 exp(m,n) = mn 由Church數定義直接給出。

exp ≡ λm.λn. n m


Church用函數定義自然數的方式看似艱深晦澀,其實只要理解了資料的本質就不難理解函數也是資料。甚至函數標記法比用阿拉伯數字更加接近資料的本質,因為函數符號是有規律的,符號中蘊含了運算,更體現了資料與運算的有機聯絡。


上面的例子是為了說明,符號集的選擇並不那麼重要,在採用不同符號集的情況下,符號間的關係保持不變,這種變化中的不變才是類型的本質,我們稱之為類型規範或類型不變性(Type Invariant)。比如,先入後出(FILO)就是堆棧類型stack的不變性,它是對於stack對象push和pop兩種操作的一種約束性。stack的本質不在於壓棧彈棧如何?名字是不是叫push/pop,而在於壓棧和彈棧是否保持了FILO關係。FILO不太容易像整數類型的四則運算一樣進行形式化表達,但等價於下面的形式化表示:

{ push(S,x); V ← pop(S) } is equivalent to { Vx }

即連續的兩次操作“1.將x壓入堆棧S;2.彈棧並賦值給V”其效果等價於“將x賦值給V”。這種形式化的表述與FILO的規範性表述是等價的。


封裝和資料抽象

接著stack的例子引出了另外一個話題。如果是用C語言實現一個stack,我們通常會定義一個結構體Stack儲存資料,並分別實現了push和pop兩個函數操作資料以實現壓棧和彈棧。我們的問題是:應該如何來為這個stack編寫單元測試用例呢?一種思路是,為push和pop分別編寫測試案例:


測試方案1:

/*C語言*/

void test_push(){

    Stack *pStack = create_stack();//建立結構體stack

    push(pStack, 1);

    ASSERT_EQUAL(1, pStack->items[0]); /*檢查狀態*/

    push(pStack, 2);

    ASSERT_EQUAL(2, pStack->items[1]); /*檢查狀態*/

}

void test_pop(){

    Stack *pStack = create_stack();/*建立結構體stack*/

    pStack->size = 2;

    pStack->items[0] = 1;

    pStack->items[1] = 2;/*資料準備*/

 

    int item2 = pop(pStack));

    ASSERT_EQUAL(2, item2);

    int item2 = pop(pStack));

    ASSERT_EQUAL(2, item2);

}

 與之相對的是另一種思路,把push和pop聯合起來測試FILO:

測試方案2:

/*C語言*/

void test_FILO(){

    Stack *pStack = create_stack();

    push(pStack, 1);

    push(pStack, 2);
    int item2 = pop(pStack));
    ASSERT_EQUAL(2, item2); /*檢查FILO*/
    int item1 = pop(pStack);

    ASSERT_EQUAL(1, item2); /*檢查FILO*/

}

雖然這是C語言,但過程思維和OO思維的差別已經完全體現出來了。如果你寫出的是類似方案1的測試案例,那麼你是過程思維;反之,如果你寫出的是方案2,雖然使用的是C語言,但你已經具備了一些OO思維!過程式與對象式作為兩種程式設計範式(Programming Paradigm)體現了兩種不同的程式設計思維。過程式以過程抽象為主,把要解決的問題抽象為一個過程,再分解為若干子過程分而治之。過程式程式的磚塊是過程,所以我們說以單個函數為基本單元的測試方案1體現了過程思維。而對象式以資料抽象為主,把要解決的問題映射到類型定義,對象是程式的磚塊。測試方案2注意到了push和pop是在stack類型不變性下功能內聚的,所以我們說它體現了OO思維。如同看到整數1就自然聯想到它可以參與加減運算,並且加減運算是逆運算一樣;看到stack類型對象我們就應該聯想到可以對它進行push和pop操作,且push和pop具有FILO的關係。可見,OO思維並不是體現在用不用OOP語言,有沒有定義類,而在於是否有類型意識。


類型(Type)是數學概念更強調語義,類(Class, Struct)是OOP語言為定義類型所提供的文法機制。從語義上講,類定義了抽象資料類型(Abstract Data Type),這個過程稱為資料抽象(Data Abstraction);從文法上講,類定義了對外介面,隱藏了內部實現,這個過程稱為封裝(Encapsulation)。註:Class通常用於定義參考型別(Reference Type),Struct通常用於定義實值型別(Value Type),二者的區別在此不詳細介紹,本文的“類”包括了Class和Struct。


為了定義stack類型,我們可以用C++寫出Stack類聲明:

//C++

//Stack.h

class Stack{

public:

    Stack();

    ~Stack();

public:

    push(int i);

    int pop();

private:

    std::vector<int> items;

}

Stack.h是關於Stack類的聲明,它定義了Stack類對外介面的文法特徵,使用者會直接和Stack.h打交道而不會看到Stack.cpp的實現。所以,類聲明和標頭檔也是一種抽象機制。但是,除了文法上的定義類型相關的操作外,還必須滿足類型的語義。我們可以用一個單元測試用例代表客戶來表達對Stack類滿足FILO的期待:

//C++

void test_FILO(){

    Stack stack;


    push(stack, 1);

    push(stack, 2);


   
    int item2 = stack.pop();
    ASSERT_EQUAL(2, item2); /* 檢查FILO*/
    int item1 = stack.pop();

    ASSERT_EQUAL(1, item2); /*檢查FILO*/

}

Stack類聲明和單元測試結合起來就從文法和語義完成對stack類型的資料抽象。至於具體的實現則被隱藏起來,我們可以有多種不同的實現,只要文法上符合Stack類聲明,語義上符合FILO。但是嚴格地講,C++這種標頭檔的類聲明機制還不夠完美。標頭檔的問題在於雖然它是面向使用者的,使用者不需要知道private的東西,但類聲明卻要求包含private的內容,相當於“走光”。比如:使用者拿到上面的Stack.h,看到private的std::vector<int>items就知道Stack內部是基於vector實現的,比如:

//C++

//Stack.cpp

class Stack{

public:

    Stack() {}

    ~Stack() {}

public:

    push(int i) {items.push_back(i);}

    int pop(

        int i = items.back();

        items.pop_back();

        return i;

    );

private:

    std::vector<int> items;


}

 

C++的Pimp慣用法的作用之一便是解決類聲明暴露內部實現的問題,讓類聲明屏蔽掉使用者不該也不需要看到的東西:

//C++

//Stack.h

class Stack{

public:

    Stack();

    ~Stack();

public:

    push(int i);

    int pop();

private:

    class StackImpl;//StackImpl類聲明

    StackImpl *pStackImpl;

}
資料抽象的過程包括了變與不變兩個部分:不變的是類的介面文法和語義規範,變化的是類的具體實現。於是,類具有陰陽兩面:陽面面向使用者是相對固定的,陰面面向實現者是相對易變的。易變的陰面被局部化(Locality)在類內部,而穩定的陽面則被放心地到處使用。陽面的設計屬於軟體設計範疇;陰面的實現屬於演算法和資料結構範疇。這種變與不變中體現了一種資訊隱藏(Information Hiding)原則,不過資訊隱藏原則是普遍原則,凡是抽象都屬於資訊隱藏,對於資料抽象特別需要重視的是類型的整體性不變性。

TDD

上面先類聲明和測試案例,後實現的方式暗合了當前流行的測試驅動開發方法(TDD)。TDD從一定程度上促使程式員從陽面思考類的行為,自然地獲得易測性和高程式碼涵蓋範圍。一般來講,TDD可以促使做出好的設計,但我也曾見過(誤)用TDD把設計搞得更糟糕的情況。我們來看什麼時候TDD把設計搞得更糟糕,比如:對於上面的stack需求,有人用TDD小步迭代寫出用例:

//C++

void test_push(){

   Stack stack;

   stack.push(1);

}

這時,這位程式員發現無法驗證push方法是否成功,於是乎想到“通過依賴注入(Dependency Injection)把vector與stack解耦,檢查vector狀態”,於是測試驅動成為:

//C++

void test_push(){

   std::vector<int> items;

   Stack stack(items); //建構函式依賴注入

   stack.push(1);

   ASSERT_EQUAL(1, items[0]);//檢查狀態

}

依賴注入、TDD,這些流行的軟體思想都用上了,不過好像在哪裡見過這樣的代碼?沒錯,和最初C語言的那個每個函數一個測試案例本質上不是一樣的嗎?哈哈!如果說前面那位用C語言寫出結合push和pop測試FILO的朋友是“大智若愚”;我們只能給眼前這位兄弟一個“大愚若智”了。一個用上依賴注入、解耦,等“先進”軟體思想的設計居然本質上是面向過程程式的。我們並沒有說面向過程並就一定不好,只是恐怕這位喜歡用先進軟體思想的朋友並未意識到自己原來是在寫面向過程程式吧?!可見,設計的關鍵不在於我們是否在用某一門語言,甚至不在於你是不是用了什麼聽起來時髦的思想,而在於是否對編程的本質理解得深入。


“高內聚與低耦合”是軟體中的兩個方面。抽象資料類型首先體現了一種自身的高內聚性,如:push和pop屬於FILO的功能內聚(Functional Cohesion)。耦合是指對象間的關係,UML主要區分依賴、關聯、彙總、組合4種橫向關係,一般只有前3種關係適合採用依賴注入達到低耦合(可參考我之前的這篇文章)。目前開發社區似乎過分地強調了“低耦合”,忽略了“高內聚”,導致很多人“言必稱解耦”,這是對OOP非常大的誤解。歸根結底,只有抓住事物的本質,做到該高內聚的高內聚,該低耦合的低耦合,才能算是高品質的軟體設計。


小結

本文是面向OOP初學者的一個思維引導,也是我自己的一個階段性學習總結。OOP和程式設計博大精深,需要不斷地學習,思考和探索。熱情歡迎朋友們批評指正本文的錯誤和不足,願與大家共同進步!


參考文獻

鄭暉,《冒號課堂——編程範式與OOP思想》
Babara Liskov, Data Abstraction and Hierarchy

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.