面向對象編程入門 – Janos Pasztor

收藏待读

面向對象編程入門 – Janos Pasztor

你已經編程了一段時間,你仍然難以接受面向對象編程的實際情況?那麼這可能是你的指南。我們將偏離傳統的解釋,並尋找一種解釋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和編寫可維護代碼一般都很難,所以在對結果感到滿意之前,您可能需要嘗試幾次。在以後的文章中,我們將詳細介紹可以幫助您編寫更好,更易維護的代碼的概念和想法,因此請務必保持關注。

原文 : 解道

相關閱讀

免责声明:本文内容来源于解道,已注明原文出处和链接,文章观点不代表立场,如若侵犯到您的权益,或涉不实谣言,敬请向我们提出检举。