用汽車比喻理解OOP - Jonathan Kuhl

banq發表於2019-01-20

站在任何街角,觀看交通一段時間,來來往往都是汽車。它們具有相同的基本結構:四個輪子,一個發動機,一個方向盤,用汽油或柴油執行。然而,它們在顏色,馬力,形狀,特徵甚至可能使用的汽油型別方面差異很大。每條繁忙的街道都是不同車型的雜音,但我們看到的大多數車輛,每個人都會同意,是一輛汽車。是的,根據你的定義,你的“道奇”可以被認為是“一輛車”。
這是物件導向程式設計(OOP)的意義所在。

物件是基於某些基本模板的單個屬性的捆綁。

每輛汽車都是基於一些基本模板基礎上的一系列獨立功能;當然,一些模板是模板的模板。沒有“基本車”這個概念,因為它是一個抽象的概念,每個衍生車是必須實現其屬性和方法的集合,但每個衍生車可以在抽象的範圍內自由地實現。
我們對汽車的概念有一個概括,然後擴充套件到不同型別的汽車。斯巴魯是一輛旅行車;GMC Yukon是一款SUV;福特Fusion是一款轎車。從這些型別,旅行車,SUV,轎車等,你可以找到更具體的想法,就像我列舉的車輛一樣。最終,你會得到足夠的抽象來購買你的個人汽車。

Car
    旅行車Sports Utility Vehicle
        GMC Yukon
    SUVStation Wagon
        斯巴魯Outback
    轎車Sedan
        福特Fusion


這些被稱為類Class。

什麼是類
如果你走進一個斯巴魯工廠,你找不到一群瘋狂的工程師不管三七二十一鼓搗一輛汽車,如果存在這種現象會是很荒謬的、低效的,並導致不均勻的汽車製造。(banq注:汽車修理廠和汽車製造廠的區別,修理廠是圍繞著某個具體汽車修理,而製造廠是根據磨具類或圖紙藍圖進行製造每個具體汽車)。
從藝術上講,非均勻性通常是有價值的,但在程式設計世界中,它會引起問題。物件不符合他們所需的角色。測試幾乎是不可能的,因此質量保證就被扔到窗外,批次生產會放緩。
相反,你發現的是一個長的程式流水線工人(大多數現在是機器人)遵循一套精確的指令,根據之前繪製的原理圖來製造汽車。
這個原理圖正是一個類,類本身不是它們建立的物件,而是用於建立物件的模板。如果我要了解2017款斯巴魯的原理圖,它會詳細告訴我每個螺母和螺栓的位置以及每個功能的工作原理和實施方式。

建構函式
當然,個別汽車的建立方式也各不相同。雖然,由於某種原因,橙色斯巴魯Crosstreks似乎在高速公路上占主導地位,顯然,人們有很多顏色選擇餘地和許多其他小功能的選擇。
建構函式設定屬性並初始化它們。它允許我們為物件的單個例項設定單獨的詳細資訊,然後,它建立物件。
建構函式和初始化程式有許多不同的版本。在我繼續之前,建構函式和初始化程式之間存在明顯的區別。建構函式在記憶體中建立物件(Subaru工廠丟擲一個新的Outback)並且初始化程式設定其屬性(新的Outback被繪製了插入的可選功能。)一些語言同時執行此操作,其他語言單獨執行。

// Java
class MyObject {
    String param;
    MyObject(String param) {
        this.param = param;
    }
}


// JavaScript
class MyObject {
    constructor(param) {
        this.param = param;
    }
}


# Python
class MyObject:
    self.__init__(self, param):
        self.param = param


想想斯巴魯工廠的裝配線等構造商。你透過告訴機器人你想要什麼顏色和功能來啟動,然後,基於他們給出的類别範本,用您想要的引數製造汽車。

介面和抽象類
但是,並非每個物件都需要構建。正如我之前所說,基本的汽車理念並不代表具體的物件,而是一個抽象的想法;基本車的蓋世只是每輛車必須實現的功能列表。如果您不實施這些功能,那麼您的製造出來的成品就不是汽車;也許它有輪子和發動機,但它不是汽車。

介面是合同,它保證另一邊出來的都是汽車。

暫時放下汽車類比,假設你有一個函式將一個物件作為其引數之一,並且在該函式中,它呼叫該物件的一個​​方法。

class Person {
    String name;
    Person(String name) {
        this.name = name;
    }
    public void greet() {
        System.out.printf("%s says 'Hello'", this.name);
    }
}

// assume we're in class main
public static void personGreet(Person person) {
    person.greet();
}
public static void main(String ...args) {
    personGreet(new Person("Jim"));
}


在上面的例子中,我們有一個方法,它接受Person並呼叫它的greet方法。這當然很好,但是有一個問題。如果我有多個“人”型別,繼承是一個模型對我不利,怎麼辦?
Employee並且Dog所有可能都有問候,但可能不作為Person的子類,該怎麼辦?Employee可能會是Person的子類,但Dog不會!Employee可以更好地透過組合而不是繼承來實現。透過personGreet()介面確保這兩個類在方法中工作:

interface Greetable {
    public void greet();
}

class Employee implements Greetable {
    // ...
    public void greet() {
        System.out.printf(
            "%s says 'Hello', I work for %s as %s", 
            this.name, this.job, this.jobTitle
        );
    }
}

class Dog implements Greetable {
    // ...
    public void greet() {
        System.out.printf("%s barks 'Arf Arf!', this.name);
    }
}

// and back to the Main class
public static void personGreet(Greetable greeter) {
    greeter.greet();
}


現在我們的greet類可以接受任何實現的物件,Greetable因為我們可以確信它實現了所需的方法。

它對任何現代IDE都非常有幫助。如果IDE知道您的變數實現Greetable,它將有助於在Greetable介面中建議方法,因此您不必記住拼寫方法或方法引數的順序。

像JavaScript這樣的語言沒有靜態型別,對於那些語言介面沒有意義。如果我想將一個物件傳遞給該personGreet()方法,那麼沒有什麼可以確保personGreet()有一個greet方法,除了我實際程式設計這樣的檢查。介面是靜態型別語言的亮點。如果您是JavaScript的粉絲,但也想要靜態型別,請嘗試使用Typescript。


在考駕駛執照場景中,你沒有被教導如何駕駛特定的汽車。你被教導如何以非常通用的方式駕駛。我們都知道汽車實現了特定的介面:方向盤位於左側(除非您的英國人),加速器位於右側並且可以使用它;將轉向開關向上輕拂一下右轉,然後向下按以指示左轉。汽車都實現了相同的介面,如果你可以開其中一輛車,你幾乎可以開任何一輛車。
而任何需要特殊許可的車輛,他們可能會實現轎車介面,但他們也會實現使用者需要注意的其他一些介面。一半可能會實現一些與汽車相同的細節,這一半就是以相同的基本方式操作,但也會實現一些其他介面,因為它的尺寸,重量,輪子數量都將影響其處理並使其成為不同的物件。事實上,將一些想法從汽車中取出(如基本駕駛)並將其移至車輛介面中可能會更好。
也許如果汽車是一個抽象類而不是一個實現車輛介面的實現類會更好,它也有自己的屬性:有四隻輪子以及汽車獨有的其他東西。

屬性和方法
我們駕駛過所有那些車,包括那個可怕的橙色斯巴魯Crosstrek。(為什麼它們總是橙色?)雖然它們都繼承了車輛和汽車,但它們看起來都非常獨特。人們在形狀,顏色,效能,收音機,駕駛室控制等方面有廣泛的選擇。構造物件時,它是儲存在記憶體中的唯一資料結構。你可以擁有任意數量的它們(直到記憶體耗盡)並且它們可能具有相同的基本形狀,但它們都是獨一無二的。
在我繼續之前,屬性在不同語言中定義為不同的東西。在JavaScript中,它們是屬性。在Python和Ruby中,它們是屬性。在Java中,它們是欄位。為了清楚起見,當我說“propperty”時,它適用於所有這些事情。
屬性只是應用於單個物件的某種狀態。例如,汽車的顏色或馬力。
方法是單個物件可能執行的某些行為。當你在汽車中加油門時,你可以呼叫accelerate()方法。即使每個物件都採用相同的accelerate()方法,但並不是每輛車都會立刻加速!只有自己的汽車accelerate()才會開始加速並增加其速度屬性值,如果兩輛車同時加速,他們只會修改自己的內部狀態。我踩下汽車裡的油門,只能讓我自己的車開走。

靜態與成員
當人們談論靜態方法和屬性時,他們經常說一些模糊的東西,比如“靜態方法屬於類,而成員方法屬於一個物件”。一旦你掌握了OOP,那麼理解這個概念並不是非常困難。但對於一些全新的程式設計人員來說,可能會有點令人困惑。畢竟,不是所有的方法和屬性都屬於這個類嗎?一個新的程式設計師可能還沒有意識到一個類和一個物件是不一樣的。正如我之前所說,一個類是原理圖,一個物件是原理圖設計的具體內容。
任何標記為“靜態”的東西都是適用於整個類的東西,適用於單個物件是沒有意義的,也不適合與類分離。
為了解釋它,我將繼續用汽車比喻。
想象一下,你開車去公路旅行。因為你非常富裕,所以你可以把車從美國運到大西洋到歐洲。當你到達那裡時,你意識到歐洲人,他們喜歡簡單易用,使用公制系統。
幸運的是,您的汽車具有將每加侖英里顯示器換成每升公里數的功能。這樣的配方對任何一輛車都很有用,但是如果你駕駛的是斯巴魯WRX或福特Fusion或笨重的Yugo用膠帶和絕望裝在一起,它就不會改變。無論你駕駛什麼,MPG到KPL都是同樣的功能。即使您駕駛高油耗車,單位轉換也不會改變。那麼為什麼要使這種方法針對個別車?讓它靜態,以便所有汽車都可以共享它。
MPG到KPL的轉換不屬於汽車。當然,個人可能會建立一種方法來自行顯示這種轉換,但數學的實際實現並不屬於汽車。相反,它可以作為常識儲存在所有汽車製造商和司機那裡。

class Car {
    final static double MPG_TO_KPL = 0.425144;
    public static double mpgToKpl(double mpg) {
        return mpg * MPG_TO_KPL;
    }
}

class Outback extends Car {
    public void displayMetricFuelConsumption() {
        double fuelConsumption = Car.mpgToKpl(this.mpg);
        System.out.println("Current fuel consumption in kpl: " + fuelConsumption);
    }
}

如您所見,單個物件本身可以呼叫自己的方法來顯示轉換後的單元,但單位轉換本身是靜態的並保持在類級別。此外,Outback不必Car為靜態方法擴充套件可用,任何物件都可以從Car類中呼叫它。

繼承
繼承是子型別繼承其父類的屬性和方法。
旅行車是一輛汽車,因此它有四個輪子,意在供一般人使用。汽車又是車輛,因此它有方向盤,加速器和制動踏板,某種活塞發動機等等。
斯巴魯傲虎是一輛旅行車,因此它是一輛帶掀背車的車,後備箱中有額外的空間。因為它是一輛旅行車,所以它是一輛汽車,因為它是一輛汽車,所以它是一輛汽車。

多型性
但並非每個子型別都會以與父級相同的方式實現方法!斯巴魯WRX和斯巴魯傲虎肯定有不同的加速方法!在任何OOP語言中,子項都可以覆蓋父項的方法和屬性。這是透過覆蓋和過載完成的,但不是每種語言都支援過載。例如,JavaScript沒有。
覆蓋是指我們更改實現時,但保持方法簽名相同。在斯巴魯WRX中,也許在我們的加速方法中有一種渦輪增壓方法,這是你通常在Outback找不到的東西。
過載是指方法簽名相同但引數及其型別不同。只要使用不同的引數和不同的引數型別過載方法,就可以多次覆蓋方法。但同樣,並非每種語言都支援這一點。JavaScript沒有。
您可以使用typeof或instanceof關鍵字模擬JavaScript中的過載,並以程式設計方式確定引數是什麼以及實現的功能取決於給出的引數,但是您不能擁有具有相同名稱和不同引數的多個函式。
你可以在Java中:

public static String add(String a, String b) {
  return a.concat(b);
}

public static int add(int a, int b) {
  return a + b;
}

兩種方法都具有相同的名稱,但簽名不同。過載只是根據輸入的內容改變實現。

應用程式介面
在繼續之前,我確實想花一點時間來解釋API(應用程式程式設計介面)是什麼。API是使用者可以使用的面向公眾的屬性和方法的集合。
你的車有API:它有一個加油口。有一個踏板可以讓它走,另一個讓它停下來。有一個輪子可以讓我轉向。這些東西允許您與汽車介面互動,並以受控和可預測的方式改變其狀態和行為。
任何物件的API可以概括為其公共屬性和使用者要使用的方法。

封裝
當我開車行駛時,當我的腳在制動器或汽油上時,它會影響我車的內部狀態。我正在放慢速度或加快速度。它不會影響我周圍的其他車輛。我汽車的內部狀態是封裝的。作為使用者,我不需要知道引擎在做什麼,我周圍也沒有人知道。我只需要知道公共API是什麼。當我與該API介面時,我不需要知道內部發生了什麼。如果我加速,我不需要特別知道哪些活塞必須上升,哪些活塞必須在任何給定時刻降低; 引擎為我處理。如果我被允許在發動機運轉時進入並修改它,我會破壞一些東西。
這是封裝。類的內部狀態僅透過明確設計用於外部使用者的特定方法暴露給外部世界。例如,單例使用封裝來確保其自身只有一個例項。建構函式只能透過特定的公共方法訪問,以確保只能呼叫一次。通常,它會檢查例項是否已建立並建立例項,或返回對該例項的引用,但無法從外部呼叫建構函式本身(設定為private)。

抽象化
現在讓我們再次回到司機考駕照場景:請記住,你不是在學習如何駕駛一輛特定的汽車,而是學會通用的汽車駕駛方法。我們知道你駕駛的任何汽車都將繼承汽車和汽車,所以我們知道它會有加速器,剎車踏板,方向盤,方向燈等所有其他東西或多或少相同的地方。
這些方法是如何實現的並不重要,只是它們是以可預測的方式實現的。抽象與封裝有關,因為你不瞭解或不關心內部。你知道的是,如果你呼叫一個方法,就應該發生可預測的行為。如果我踩油門,汽車應該向前移動。如果我踩了油門,轉向閃光燈卻亮了。。。那會很奇怪。
你知道如何駕駛任何一輛汽車,因為你對汽車是什麼以及它的方法有了一個抽象的概念。您知道每輛車都可以在您可以使用的類似API中使用這些方法。
油門踏板或方向燈的實際實施或任何黑盒裝。如果你想加速,你只需踩踏板;沒有必要了解踏板是如何按壓導致氣體流入發動機的,這使得活塞上升和下降並允許它們迴圈透過進氣,壓縮,燃燒和排氣系統。當然,這對於維護來說可能很方便,但你可以輕鬆駕駛汽車而不知道引擎蓋下那個大笨重的東西究竟在做什麼。
抽象隱藏了不必要的實現細節。它允許您採用該實現並將其捆綁為更有意義和可重用的內容。這降低了使用者端的複雜性。以JavaScript的Array方法庫為例。

const squares = [1,2,3].map(x => x**2);
//[1, 4, 9]


雖然JavaScript是一種OOP語言,但它基於原型繼承方案而不是經典方案。這是要記住的事情。話雖如此,我在這裡所說的大部分內容也適用於JavaScript,但也許並不完全正確。

我是否關心map()引擎蓋下的內容?不,我所關心的是它對每個元素都執行某些功能的知識。我不在乎怎麼樣。每次我想要更改陣列的元素時,我都不想實現對映函式。為什麼不把它抽象成一個方法,以便我可以根據需要重用它?
這是抽象。

結論
物件導向程式設計可能有點令人困惑,但我發現如果將物件視為日常物件,就能更好地理解它。汽車是大規模生產的物體,具有類似的基本形狀,即使有各種各樣的型別,從笨重的SUV到旅行車,到卡車,轎車和跑車。如果您考慮汽車在現實世界中如何相互關聯,您可以更好地瞭解物件在程式設計世界中的工作方式。
我希望這有助於更好地理解OOP是什麼以及OOP中涉及的基本概念和術語是什麼。我並不打算真正進入OOP的實際語言實現,但希望當你讀到不同語言如何實現OOP時,進行某種類比將有助於使概念更清晰。

 

相關文章