物件導向程式設計入門 - Janos Pasztor

banq發表於2019-01-09

你已經程式設計了一段時間,你仍然難以接受物件導向程式設計的實際情況?那麼這可能是你的指南。我們將偏離傳統的解釋,並尋找一種解釋OOP的新方法。
我們馬上就會從一些程式碼開始。請記住,本文中的示例是用Java-esque表示法編寫的,但是所有內容都可以輕鬆應用於任何OOP程式語言,無論是PHP,Python還是Go。

那麼,讓我們來看一個類:

class Student {
  string name;
}

一個類就像一個藍圖(banq注:blueprint是藍圖 原圖 計劃大綱,做事之前的計劃和主觀觀念)。如果您想獲取此藍圖並建立實際例項,請按以下步驟操作:

Student myStudent = new Student();
myStudent.name = "Janos";


myStudent變數現在包含一個Student類的副本,其name變數設定為我的名字。您也可以輕鬆建立第二個副本:

Student myStudent = new Student();
myStudent.name = "Janos";

Student yourStudent = new Student();
yourStudent.name = "Your name";


如果執行此程式碼,這兩個例項將存在於不同的記憶體空間中; 你可以獨立修改它們。
到現在為止還挺好?好吧,那就讓我們做一些更先進的事情吧。到目前為止我們所做的是建立一個資料結構。您可以根據需要新增任意數量的變數,但這不僅僅是一種組織資料的方法。讓我們透過向我們的類新增方法來改變它。

class Student {
  string name;
  void setName(string name) {
    this.name = name;
  }
}


如您所見,該setName方法設定名稱變數。你問為什麼要這麼做?讓我們假設您想要檢查名稱是否為空的情況:

class Student {
  string name;
  
  void setName(string name) {
    if (name == "") {
      throw new InvalidArgumentException("The name must not be empty!");
    }
    this.name = name;
  }
}

這很不錯,但正如你所看到的,這個name領域仍然可以解開。我們需要一些方法來強制執行name欄位的初始化。這就是建構函式發揮作用的地方。

建構函式是一個特殊函式,在例項化類時執行。通常,它與類共享名稱,但在某些語言(如PHP)中,它具有不同的名稱。建立建構函式就像建立方法一樣簡單:

class Student {
  string name;
  
  Student(string name) {
    this.setName(name);
  }
  
  void setName(string name) {
    if (name == "") {
      throw new InvalidArgumentException("The name must not be empty!");
    }
    this.name = name;
  }
}

Student myStudent = new Student("Janos");


換句話說,您在例項建立時輸入的引數自動最終作為建構函式的引數。

保衛資料!(封裝)
如您所見,我們將資料繫結到功能。一個好的類可以讓你使用它,而不必知道它是如何實現的或如何在內部儲存資料。但是,我們有一個小問題。透過當前設定,我們可以透過name直接設定變數來輕鬆繞過驗證邏輯:

Student myStudent = new Student("Janos");
myStudent.name    = "";


幸運的是,大多數OOP語言為我們提供了禁用對成員變數和方法的外部訪問的工具,稱為 可見性關鍵字。通常,我們會區分這些級別的可見性:
  • public:每個人都可以訪問使用此關鍵字標記的方法。在大多數OOP語言中,這是預設語言。
  • protected:只有子類可以訪問方法或變數(我們稍後會討論這個)。
  • private:只有當前類可以訪問方法或變數。(注意,同一類的其他例項通常也可以訪問它!)


使用這些關鍵字,我們可以使程式碼更安全:

class Student {
  private string name;
  
  public void setName(string name) {
    if (name == "") {
      throw new InvalidArgumentException("The name must not be empty!");
    }
    this.name = name;
  }
}


如果我們現在嘗試name直接訪問成員變數,我們將從編譯器中得到錯誤。除此之外,具有明確的可見性標記使我們的程式碼更具描述性並且更易於閱讀。
作為一般規則,您不應該將類用作函式容器; 它應該用於儲存與類相關的資料。

類協作
要使OOP有用,您將需要多個類。如果您正在編寫顯示文章的網站,您可能希望擁有一個可以從資料庫中獲取原始資料的類。您還需要一個將原始資料(例如markdown或LaTeX)轉換為輸出格式的類,例如HTML或PDF。
作為一種自然的方法,我們可以做這樣的事情:

class HTMLOutputConverter {
  private MySQLArticleDatabaseConnector db;
  
  public HTMLOutputConverter() {
    this.db = new MySQLArticleDatabaseConnector();
  }
}


如您所見,HTMLOutputConverter依賴於MySQLArticleDatabaseConnector我們在輸出轉換器的建構函式中建立資料庫聯結器的例項。為什麼這麼糟糕?
  • 你不能用不同的類替換MySQLArticleDatabaseConnector。
  • 由於MySQLArticleDatabaseConnector建立了例項HTMLOutputConverter,該類需要知道需要傳遞給的所有引數MySQLArticleDatabaseConnector。
  • 在檢視類定義時,依賴性不會立即顯現出來。你必須檢視程式碼才能發現存在依賴關係。

讓我們看看我們是否可以做得更好。我們不是建立例項MySQLArticleDatabaseConnector,而是將其作為引數請求。像這樣的東西:

class HTMLOutputConverter {
  private MySQLArticleDatabaseConnector db;
  
  public HTMLOutputConverter(MySQLArticleDatabaseConnector db) {
    this.db = db;
  }
}


此構造稱為依賴注入。您依賴的類是注入的,而不是直接建立的。必須手動建立依賴項似乎很麻煩,但有一些工具可以幫助您; 它們被稱為依賴注入容器。值得注意的例子包括Google Guice, AurynPython DIC
依賴注入解決了第二和第三個問題,但沒有解決第一個問題。所以讓我們建立一個新的語言結構並將其稱為介面。介面將描述類需要實現的方法,而不實際指定它們的程式碼。像這樣的東西:

interface ArticleDatabaseConnector {
  public Article getArticleBySlug(string slug);
}


因此,介面將描述類可以實現的功能。在這種情況下,我們將描述一個接受slug引數並返回Article一個響應的方法。您可以像這樣編寫實現類:

class MySQLArticleDatabaseConnector implements ArticleDatabaseConnector {
  public Article getArticleBySlug(string slug) {
    //Query data from the MySQL database and return the article object.
  }
}


如您所見,MySQLArticleDatabaseConnector實現ArticleDatabaseConnector,需要實現其行為。這使我們能夠修改HTMLOutputConverter依賴於介面,而不是實際的實現:

class HTMLOutputConverter {
  private ArticleDatabaseConnector db;
  
  public HTMLOutputConverter(ArticleDatabaseConnector db) {
    this.db = db;
  }
}

由於HTMLOutputConverter依賴於介面,我們可以自由地為我們喜歡的介面建立任何實現,無論是MySQL,Cassandra還是Google Cloud SQL。當一個抽象可以有許多形式,許多實際實現時,我們稱之為多型。
當我們以這樣的方式使用多型時,你的類依賴於抽象而不是具體,我們也將它稱為依賴性反轉,以突出我們已經顛倒了依賴性的事實。
但這只是花哨的極客 - 說的是你不應該把你的類焊接在一起。您可以將介面想象為實現它的類與使用它的類之間的契約。此契約描述了實現類必須提供的功能,並且使用類可以依賴於它。

讓我們概括介面!(抽象)
您可能已經猜到,介面並不是建立抽象的唯一工具。事實上,介面被發明為Java中的一種解決方法,用於稱為多重繼承。這帶來了一個顯而易見的問題:什麼是繼承?

讓我們想象一下,當強制具體指定某個特定行為是不夠的時候,你真的想要將“一些程式碼”傳遞給你現有的抽象。實際上,您的抽象必須為您提供實現“某些方法”的可能性,這些方法介面顯然無法實現。

這就是繼承發揮作用的地方。從本質上講,繼承意味著如果一個類Foo擴充套件了類Bar,它將繼承它的所有方法和變數。換句話說,您可以編寫如下程式碼:

class Bar {
  protected string baz;
}

class Foo extends Bar {
  public void setBaz(string baz) {
    this.baz = baz;
  }
}


如您所見,子類宣告瞭一個方法,該方法設定從父類繼承的變數,這是可能的,因為可見性rules(protected)允許它。如果我們將變數設定baz為private,則此程式碼將不起作用。
有趣的是,在上面的例子中,你可以例項化Bar和Foo。如果你想限制它,你必須宣告Bar該類abstract。除此之外,您還可以新增沒有正文的抽象方法,並且必須由子類實現:

abstract class Bar {
  protected string baz;
  
  abstract void setBaz(string baz);
}

class Foo extends Bar {
  public void setBaz(string baz) {
    this.baz = baz;
  }
}

那麼一個類可以有多少父類?一?二?五?答案是:這取決於。有些語言,比如C ++,已經解決了多重繼承的問題。因此,介面語言構造甚至不存在於C ++中。其他語言,如Java或PHP,決定不處理這個問題,而是發明介面。換句話說,介面只是抽象類,只有抽象方法,沒有變數來規避必須解決多重繼承。
謹防錯誤的抽象!許多OOP教程都帶有從矩形繼承的正方形的例子。這僅在數學意義上是正確的。在程式設計中,您希望子類的行為與它們的父類相同,因為矩形具有兩個獨立的邊,而正方形則沒有。

避免全域性狀態
某些語言(如Java)引入了一個名為的特殊關鍵字static。它顛倒了這樣一個事實,即每個例項都有自己的記憶體空間,並在所有例項中建立共享記憶體空間。有很多種方法可以使用它。

一個值得注意的例子是單例模式:

class DatabaseConnection {
  private static DatabaseConnection instance;

  public static DatabaseConnection getInstance() {
    if (!self::instance) {
      self::instance = new DatabaseConnection();
    }
    return self::instance;
  }
}

DatabaseConnection db = DatabaseConnection::getInstance();


第一次呼叫時getInstance,將建立一個例項。任何進一步的呼叫都將返回該初始例項。

使用static的問題在於它建立了一個有時隱藏的全域性狀態。您無法建立一個真正獨立的類例項,這使得測試和其他操作變得棘手。
通常,您應該儘可能避免全域性狀態。雖然靜態不是建立全域性狀態的唯一方法,但它是最相關的方式之一。如果可能的話,最好避免使用靜態,並且如上所述進行依賴注入。
提示: static確實有一些合法的用途,但一般來說,應始終考慮替代方案。

類責任
擁有狀態的物件與經典的基於函式的程式設計不同,您只需傳遞資料。學習OOP時,儘量避免使物件成為純函式容器,並將資料與功能整合。
但是,在建立類時,請始終考慮其責任。雖然很容易將與一項任務相關的所有內容都放入類中,但這樣做可能並不明智。如果你有學生管理軟體,你可能會想做這樣的事情:

class Student {
  private string id;
  private string name;
  
  public void setId(string id) { ... }
  public void setName(string name) { ... }
  
  public void save() { ... }
}

正如您在此場景中所看到的那樣,處理學生資料並將其儲存到某種型別的資料庫將屬於同一個類。實際上,這些是完全獨立的兩個任務,並且沒有任何業務存在。雖然我們不會在本文中詳細介紹,但建議您將課程簡潔明瞭並專注於單個任務。

未來的步驟
這些只是最基本的OOP概念。實際上,人們可以遵循許多想法和設計模式,但很少有程式設計師可以用心命名。不要害怕嘗試,更重要的是,不要害怕失敗。OOP和編寫可維護程式碼一般都很難,所以在對結果感到滿意之前,您可能需要嘗試幾次。在以後的文章中,我們將詳細介紹可以幫助您編寫更好,更易維護的程式碼的概念和想法,因此請務必保持關注。
 

相關文章