1. 程式人生 > 其它 >重新認識面向物件(持續更新)

重新認識面向物件(持續更新)

技術標籤:程式語言java設計模式多型封裝

重新認識面向物件

重新認識面向物件

1.面向物件程式設計和麵向物件程式語言

面向物件程式設計有兩個非常重要,非常基礎的概念,那就是類(class)和物件(object).

  • 面向物件程式設計是一種程式設計正規化或程式設計風格.它以類或物件作為組織程式碼的基本單元,並將封裝,抽象,繼承,多型四個特性,作為程式碼設計和實現的基石;
  • 面向物件程式語言是支援類或物件的語法機制,並有執行緒的語法機制,能方便地實現面向物件程式設計四大特性(封裝,抽象,繼承,多型)的程式語言;

1.1.封裝,抽象,繼承,多型解決的問題

1.1.1.封裝

封裝叫做資訊隱藏或者資料訪問保護.類通過暴露有限的訪問介面,授權外部僅能通過類提供的方法來訪問內部資訊或資料.對於封裝這個特性,我們需要程式語言本身提供一定的語法機制來支援.這個語法機制就是訪問許可權控制.

####1.1.2.抽象

抽象這個特性是非常容易實現的,並不需要非得依靠介面類或者抽象類這些特殊語法機制來支援。換句話說,並不是說一定要為實現類抽象出介面類,才叫作抽象。即便不編寫介面類,單純的實現類本身就滿足抽象特性。之所以這麼說,那是因為,類的方法是通過程式語言中的“函式”這一語法機制來實現的。通過函式包裹具體的實現邏輯,這本身就是一種抽象。呼叫者在使用函式的時候,並不需要去研究函式內部的實現邏輯,只需要通過函式的命名、註釋或者文件,瞭解其提供了什麼功能,就可以直接使用了。

####1.1.3.繼承

繼承最大的一個好處就是程式碼複用。假如兩個類有一些相同的屬性和方法,我們就可以將這些相同的部分,抽取到父類中,讓兩個子類繼承父類。這樣,兩個子類就可以重用父類中的程式碼,避免程式碼重複寫多遍。不過,這一點也並不是繼承所獨有的,我們也可以通過其他方式來解決這個程式碼複用的問題,比如利用組合關係而不是繼承關係.

####1.1.4.多型

多型是指子類可以替換父類,在實際的程式碼執行過程中,呼叫子類的方法實現.

多型這種特性也需要程式語言提供特殊的語法機制來實現.

  • 第一個語法機制是程式語言要支援父類物件可以引用子類物件;
  • 第二個語法機制是程式語言需要支援繼承;
  • 第三個語法機制是程式語言要支援子類可以重寫父類中的方法;

2.程式碼設計看似是面向物件,實際是面向過程

2.1. 濫用getter,setter方法

這種做法實際上違反了面向物件程式設計的封裝特性,相當於將面向物件程式設計風格退化成面向過程程式設計風格.

public class ShoppingCart {
  private int itemsCount;
  private double totalPrice;
  private List<ShoppingCartItem> items = new ArrayList<>();
  
  public int getItemsCount() {
    return this.itemsCount;
  }
  
  public void setItemsCount(int itemsCount) {
    this.itemsCount = itemsCount;
  }
  
  public double getTotalPrice() {
    return this.totalPrice;
  }
  
  public void setTotalPrice(double totalPrice) {
    this.totalPrice = totalPrice;
  }

  public List<ShoppingCartItem> getItems() {
    return this.items;
  }
  
  public void addItem(ShoppingCartItem item) {
    items.add(item);
    itemsCount++;
    totalPrice += item.getPrice();
  }
  // ...省略其他方法...
}

任何程式碼都可以隨意呼叫setter方法,來重新設定屬性的值.

面向物件封裝的定義是:**通過訪問許可權控制,隱藏內部資料,外部僅能通過類提供的有限的介面訪問,修改內部資料.**暴露不應該暴露的setter方法,明顯違反了面向物件的封裝特性,資料沒有訪問許可權控制,任何程式碼都可以隨意修改它,程式碼就退化成了面向過程風格的.

對於items屬性沒有設定setter方法,看似是沒有問題,但是getter方法,返回的是一個List集合容器.外部呼叫者在拿到這個容器之後,是可以操作容器內部資料的.

ShoppingCart cart = new ShoppCart();
...
cart.getItems().clear(); // 清空購物車

清空購物車這樣的功能雖然看起來合情合理,但是這樣子的程式碼寫法,會導致itemsCount,totalPrice,items三者資料不一致,我們不應該將清空購物車的業務邏輯暴露給上層程式碼.正確的做法應該是,在ShoppingCart類中定義一個clear()方法,將清空購物車的業務邏輯封裝在裡面.

如果想要看購物車中購買了什麼,那麼必須要提供items屬性的getter方法,我們可以通過Java提供的Collections.unmodifiableList()方法,讓getter方法返回一個不可被修改的UnmodifiableList集合.

public class ShoppingCart {
  // ...省略其他程式碼...
  public List<ShoppingCartItem> getItems() {
    return Collections.unmodifiableList(this.items);
  }
}

public class UnmodifiableList<E> extends UnmodifiableCollection<E>
                          implements List<E> {
  public boolean add(E e) {
    throw new UnsupportedOperationException();
  }
  public void clear() {
    throw new UnsupportedOperationException();
  }
  // ...省略其他程式碼...
}

ShoppingCart cart = new ShoppingCart();
List<ShoppingCartItem> items = cart.getItems();
items.clear();//丟擲UnsupportedOperationException異常

在設計實現類的時候,除非真的需要,否則,儘量不要給屬性定義setter方法.除此之外,儘管getter方法相對於setter方法要安全些,但是如果返回的是集合容器,也要防範集合內部資料被修改的危險;

2.2.濫用全域性變數和全域性方法

2.3.定義資料和方法分離的類

傳統的 MVC 結構分為 Model 層、Controller 層、View 層這三層。不過,在做前後端分離之後,三層結構在後端開發中,會稍微有些調整,被分為 Controller 層、Service 層、Repository 層。Controller 層負責暴露介面給前端呼叫,Service 層負責核心業務邏輯,Repository 層負責資料讀寫。而在每一層中,我們又會定義相應的 VO(View Object)、BO(Business Object)、Entity。一般情況下,VO、BO、Entity 中只會定義資料,不會定義方法,所有操作這些資料的業務邏輯都定義在對應的 Controller 類、Service 類、Repository 類中。這就是典型的面向過程的程式設計風格

3.抽象與介面

抽象類的比較典型的使用場景就是(模板設計模式).

Logger 是一個記錄日誌的抽象類,FileLogger 和 MessageQueueLogger 繼承 Logger,分別實現兩種不同的日誌記錄方式:記錄日誌到檔案中和記錄日誌到訊息佇列中。FileLogger 和 MessageQueueLogger 兩個子類複用了父類 Logger 中的 name、enabled、minPermittedLevel 屬性和 log() 方法,但因為這兩個子類寫日誌的方式不同,它們又各自重寫了父類中的 doLog() 方法。

// 抽象類
public abstract class Logger {
  private String name;
  private boolean enabled;
  private Level minPermittedLevel;

  public Logger(String name, boolean enabled, Level minPermittedLevel) {
    this.name = name;
    this.enabled = enabled;
    this.minPermittedLevel = minPermittedLevel;
  }
  
  public void log(Level level, String message) {
    boolean loggable = enabled && (minPermittedLevel.intValue() <= level.intValue());
    if (!loggable) return;
    doLog(level, message);
  }
  
  protected abstract void doLog(Level level, String message);
}
// 抽象類的子類:輸出日誌到檔案
public class FileLogger extends Logger {
  private Writer fileWriter;

  public FileLogger(String name, boolean enabled,
    Level minPermittedLevel, String filepath) {
    super(name, enabled, minPermittedLevel);
    this.fileWriter = new FileWriter(filepath); 
  }
  
  @Override
  public void doLog(Level level, String mesage) {
    // 格式化level和message,輸出到日誌檔案
    fileWriter.write(...);
  }
}
// 抽象類的子類: 輸出日誌到訊息中介軟體(比如kafka)
public class MessageQueueLogger extends Logger {
  private MessageQueueClient msgQueueClient;
  
  public MessageQueueLogger(String name, boolean enabled,
    Level minPermittedLevel, MessageQueueClient msgQueueClient) {
    super(name, enabled, minPermittedLevel);
    this.msgQueueClient = msgQueueClient;
  }
  
  @Override
  protected void doLog(Level level, String mesage) {
    // 格式化level和message,輸出到訊息中介軟體
    msgQueueClient.send(...);
  }
}

介面的定義如下

// 介面
public interface Filter {
  void doFilter(RpcRequest req) throws RpcException;
}
// 介面實現類:鑑權過濾器
public class AuthencationFilter implements Filter {
  @Override
  public void doFilter(RpcRequest req) throws RpcException {
    //...鑑權邏輯..
  }
}
// 介面實現類:限流過濾器
public class RateLimitFilter implements Filter {
  @Override
  public void doFilter(RpcRequest req) throws RpcException {
    //...限流邏輯...
  }
}
// 過濾器使用Demo
public class Application {
  // filters.add(new AuthencationFilter());
  // filters.add(new RateLimitFilter());
  private List<Filter> filters = new ArrayList<>();
  
  public void handleRpcRequest(RpcRequest req) {
    try {
      for (Filter filter : filters) {
        filter.doFilter(req);
      }
    } catch(RpcException e) {
      // ...處理過濾結果...
    }
    // ...省略其他處理邏輯...
  }
}

繼承關係是一種 is-a 的關係,那抽象類既然屬於類,也表示一種 is-a 的關係。相對於抽象類的 is-a 關係來說,介面表示一種 has-a 關係,表示具有某些功能。對於介面,有一個更加形象的叫法,那就是協議(contract)。

4.基於介面而非實現程式設計

越抽象,越頂層,越脫離具體某一實現的設計,越能提高程式碼的靈活性,越能應對未來需求變化.好的程式碼設計,不僅能應對當下的需求,而且在將來需求發生變化的時候,仍然能夠在不破壞原有程式碼設計的情況下靈活應對.

展示一個上傳下載的基於介面程式設計的正規化如下:

public interface ImageStore {
  String upload(Image image, String bucketName);
  Image download(String url);
}

public class AliyunImageStore implements ImageStore {
  //...省略屬性、建構函式等...

  public String upload(Image image, String bucketName) {
    createBucketIfNotExisting(bucketName);
    String accessToken = generateAccessToken();
    //...上傳圖片到阿里雲...
    //...返回圖片在阿里雲上的地址(url)...
  }

  public Image download(String url) {
    String accessToken = generateAccessToken();
    //...從阿里雲下載圖片...
  }

  private void createBucketIfNotExisting(String bucketName) {
    // ...建立bucket...
    // ...失敗會丟擲異常..
  }

  private String generateAccessToken() {
    // ...根據accesskey/secrectkey等生成access token
  }
}

// 上傳下載流程改變:私有云不需要支援access token
public class PrivateImageStore implements ImageStore  {
  public String upload(Image image, String bucketName) {
    createBucketIfNotExisting(bucketName);
    //...上傳圖片到私有云...
    //...返回圖片的url...
  }

  public Image download(String url) {
    //...從私有云下載圖片...
  }

  private void createBucketIfNotExisting(String bucketName) {
    // ...建立bucket...
    // ...失敗會丟擲異常..
  }
}

// ImageStore的使用舉例
public class ImageProcessingJob {
  private static final String BUCKET_NAME = "ai_images_bucket";
  //...省略其他無關程式碼...
  
  public void process() {
    Image image = ...;//處理圖片,並封裝為Image物件
    ImageStore imageStore = new PrivateImageStore(...);
    imagestore.upload(image, BUCKET_NAME);
  }
}

很多人在定義介面的時候,希望通過實現類來反推介面的定義.先把實現類寫好,然後看實現類中有哪些方法,如果這種方式,就有可能導致介面定義不夠抽象,依賴具體的實現.這樣的介面設計就沒有意義了.

5.多用組合少用繼承

在面向物件程式設計中,有一條非常經典的設計原則:組合優於繼承,多用組合少用繼承.

5.1.為什麼不推薦使用繼承?

繼承是面向物件的四大特性之一,用來表示類之間的is-a關係,可以解決程式碼複用的問題.雖然繼承有諸多作用,但繼承層次過深,過複雜,也會影響到程式碼的可維護性.

繼承最大的問題:繼承層次過深,繼承關係過於複雜會影響到程式碼的可讀性和可維護性.

5.2.組合相比繼承有哪些優勢?

我們可以利用組合,介面,委託三個技術手段來解決繼承存在的問題

public interface Flyable {
  void fly()}
public class FlyAbility implements Flyable {
  @Override
  public void fly() { //... }
}
//省略Tweetable/TweetAbility/EggLayable/EggLayAbility

public class Ostrich implements Tweetable, EggLayable {//鴕鳥
  private TweetAbility tweetAbility = new TweetAbility(); //組合
  private EggLayAbility eggLayAbility = new EggLayAbility(); //組合
  //... 省略其他屬性和方法...
  @Override
  public void tweet() {
    tweetAbility.tweet(); // 委託
  }
  @Override
  public void layEgg() {
    eggLayAbility.layEgg(); // 委託
  }
}