1. 面向對象

一.封裝、抽象、繼承、多態分別可以解決哪些編程問題?

封裝
  1. 封裝也叫作信息隱藏或者數據訪問保護。類通過暴露有限的訪問接口,授權外部僅能通過類提供的方式(或者叫函數)來訪問內部信息或者數據
抽象
  1. 封裝主要講的是如何隱藏信息、保護數據,而抽象講的是如何隱藏方法的具體實現
  2. 抽象這個概念是一個非常通用的設計思想,並不單單用在面向對象編程中,也可以用來指導架構設計等。而且這個特性也並不需要編程語言提供特殊的語法機制來支持,只需要提供“函數”這一非常基礎的語法機制,就可以實現抽象特性、所以,它沒有很強的“特異性”,有時候並不被看作面向對象編程的特性之一
繼承
  1. 繼承最大的一個好處就是代碼複用
多態
  1. 多態。多態是指,子類可以替換父類
  2. 利用繼承實現多態
  3. 利用接口實現多態
  4. 多態特性能提高代碼的可擴展性和複用性:僅用一個 print() 函數就可以實現遍歷打印不同類型(Array、LinkedList)集合的數據

二.面向對象相比面向過程有哪些優勢?面向過程真的過時了嗎?

面向過程編程與面向過程編程語言
  1. 面向過程編程

面向過程編程也是一種編程範式或編程風格。它以過程(可以理解爲方法、函數、操作)作爲組織代碼的基本單元,以數據(可以理解爲成員變量、屬性)與方法相分離爲最主要的特點。面向過程風格是一種流程化的編程風格,通過拼接一組順序執行的方法來操作數據完成一項功能。

  1. 面向對象與面向過程區別例子:

例:假設我們有一個記錄了用戶信息的文本文件 users.txt,每行文本的格式是 name&age&gender(比如,小王 &28& 男)。我們希望寫一個程序,從 users.txt 文件中逐行讀取用戶信息,然後格式化成 name\tage\tgender(其中,\t 是分隔符)這種文本格式,並且按照 age 從小到大排序之後,重新寫入到另一個文本文件 formatted_users.txt 中

  1. 面向過程實現:

struct User {
  char name[64];
  int age;
  char gender[16];
};

struct User parse_to_user(char* text) {
  // 將text(“小王&28&男”)解析成結構體struct User
}

char* format_to_text(struct User user) {
  // 將結構體struct User格式化成文本("小王\t28\t男")
}

void sort_users_by_age(struct User users[]) {
  // 按照年齡從小到大排序users
}

void format_user_file(char* origin_file_path, char* new_file_path) {
  // open files...
  struct User users[1024]; // 假設最大1024個用戶
  int count = 0;
  while(1) { // read until the file is empty
    struct User user = parse_to_user(line);
    users[count++] = user;
  }
  
  sort_users_by_age(users);
  
  for (int i = 0; i < count; ++i) {
    char* formatted_user_text = format_to_text(users[i]);
    // write to new file...
  }
  // close files...
}

int main(char** args, int argv) {
  format_user_file("/home/zheng/user.txt", "/home/zheng/formatted_users.txt");
}
  1. 面向對象實現:

 public class User {
  private String name;
  private int age;
  private String gender;
  
  public User(String name, int age, String gender) {
    this.name = name;
    this.age = age;
    this.gender = gender;
  }
  
  public static User praseFrom(String userInfoText) {
    // 將text(“小王&28&男”)解析成類User
  }
  
  public String formatToText() {
    // 將類User格式化成文本("小王\t28\t男")
  }
}

public class UserFileFormatter {
  public void format(String userFile, String formattedUserFile) {
    // Open files...
    List users = new ArrayList<>();
    while (1) { // read until file is empty 
      // read from file into userText...
      User user = User.parseFrom(userText);
      users.add(user);
    }
    // sort users by age...
    for (int i = 0; i < users.size(); ++i) {
      String formattedUserText = user.formatToText();
      // write to new file...
    }
    // close files...
  }
}

public class MainApplication {
  public static void main(String[] args) {
    UserFileFormatter userFileFormatter = new UserFileFormatter();
    userFileFormatter.format("/home/zheng/users.txt", "/home/zheng/formatted_users.txt");
  }
}
  1. 區別: 面向過程風格的代碼被組織成了一組方法集合及其數據結構(struct User),方法和數據結構的定義是分開的。面向對象風格的代碼被組織成一組類,方法和數據結構被綁定一起,定義在類中。
面向對象編程相比面向過程編程有哪些優勢?
  1. 對於大規模複雜程序的開發,程序的處理流程並非單一的一條主線,而是錯綜複雜的網狀結構。面向對象編程比起面向過程編程,更能應對這種複雜類型的程序開發。
  2. 面向對象編程相比面向過程編程,具有更加豐富的特性(封裝、抽象、繼承、多態)。利用這些特性編寫出來的代碼,更加易擴展、易複用、易維護。
  3. 從編程語言跟機器打交道的方式的演進規律中,我們可以總結出:面向對象編程語言比起面向過程編程語言,更加人性化、更加高級、更加智能。

三.哪些代碼設計看似是面向對象,實際是面向過程的?

濫用 getter、setter 方法
  1. 它違反了面向對象編程的封裝特性,相當於將面向對象編程風格退化成了面向過程編程風格。數據沒有訪問權限控制,任何代碼都可以隨意修改它,代碼就退化成了面向過程編程風格的了

  2. 處理:

在設計實現類的時候,除非真的需要,否則,儘量不要給屬性定義 setter 方法。除此之外,儘管 getter 方法相對 setter 方法要安全些,但是如果返回的是集合容器(比如例子中的 List 容器),也要防範集合內部數據被修改的危險。

濫用全局變量和全局方法
  1. 只包含靜態方法不包含任何屬性的 Utils 類,是徹徹底底的面向過程的編程風格。
  2. 即便在面向對象編程中,我們也並不是完全排斥面向過程風格的代碼。只要它能爲我們寫出好的代碼貢獻力量,我們就可以適度地去使用。
  3. 除此之外,類比 Constants 類的設計,我們設計 Utils 類的時候,最好也能細化一下,針對不同的功能,設計不同的 Utils 類,比如 FileUtils、IOUtils、StringUtils、UrlUtils 等,不要設計一個過於大而全的 Utils 類。
定義數據和方法分離的類
  1. 所有操作這些數據的業務邏輯都定義在對應的 Controller 類、Service 類、Repository 類中。這就是典型的面向過程的編程風格。

四.接口vs抽象類的區別?如何用普通的類模擬抽象類和接口?

抽象類
  1. 抽象類特性
  1. 抽象類不允許被實例化,只能被繼承。也就是說,你不能 new 一個抽象類的對象出來(Logger logger = new Logger(…); 會報編譯錯誤)。
  2. 抽象類可以包含屬性和方法。方法既可以包含代碼實現(比如 Logger 中的 log() 方法),也可以不包含代碼實現(比如 Logger 中的 doLog() 方法)。不包含代碼實現的方法叫作抽象方法。
  3. 子類繼承抽象類,必須實現抽象類中的所有抽象方法。對應到例子代碼中就是,所有繼承 Logger 抽象類的子類,都必須重寫 doLog() 方法。
  1. 代碼複用,不可實例化,強制實現某種方法
接口
  1. 接口特性
  1. 接口不能包含屬性(也就是成員變量)
  2. 接口只能聲明方法,方法不能包含代碼實現。
  3. 類實現接口的時候,必須實現接口中聲明的所有方法
  1. 抽象類更多的是爲了代碼複用,而接口就更側重於解耦,提高代碼的可擴展性。
如何決定該用抽象類還是接口?
  1. 實際上,判斷的標準很簡單。如果我們要表示一種 is-a 的關係,並且是爲了解決代碼複用的問題,我們就用抽象類;如果我們要表示一種 has-a 關係,並且是爲了解決抽象而非代碼複用的問題,那我們就可以使用接口。

五.爲什麼基於接口而非實現編程?有必要爲每個類都定義接口嗎?

  1. 接口意識、抽象思維
  2. 在編寫代碼的時候,要遵從“基於接口而非實現編程”的原則
  1. 函數的命名不能暴露任何實現細節。比如,前面提到的 uploadToAliyun() 就不符合要求,應該改爲去掉 aliyun 這樣的字眼,改爲更加抽象的命名方式,比如:upload()。
  2. 封裝具體的實現細節。比如,跟阿里雲相關的特殊上傳(或下載)流程不應該暴露給調用者。我們對上傳(或下載)流程進行封裝,對外提供一個包裹所有上傳(或下載)細節的方法,給調用者使用。
  3. 爲實現類定義抽象的接口。具體的實現類都依賴統一的接口定義,遵從一致的上傳功能協議。使用者依賴接口,而不是具體的實現類來編程

3.例子:上傳下載(重構前)


public class AliyunImageStore {
  //...省略屬性、構造函數等...
  
  public void createBucketIfNotExisting(String bucketName) {
    // ...創建bucket代碼邏輯...
    // ...失敗會拋出異常..
  }
  
  public String generateAccessToken() {
    // ...根據accesskey/secrectkey等生成access token
  }
  
  public String uploadToAliyun(Image image, String bucketName, String accessToken) {
    //...上傳圖片到阿里雲...
    //...返回圖片存儲在阿里雲上的地址(url)...
  }
  
  public Image downloadFromAliyun(String url, String accessToken) {
    //...從阿里雲下載圖片...
  }
}

// AliyunImageStore類的使用舉例
public class ImageProcessingJob {
  private static final String BUCKET_NAME = "ai_images_bucket";
  //...省略其他無關代碼...
  
  public void process() {
    Image image = ...; //處理圖片,並封裝爲Image對象
    AliyunImageStore imageStore = new AliyunImageStore(/*省略參數*/);
    imageStore.createBucketIfNotExisting(BUCKET_NAME);
    String accessToken = imageStore.generateAccessToken();
    imagestore.uploadToAliyun(image, BUCKET_NAME, accessToken);
  }
  
}

問題:假如修改上傳方式

  1. 重構後

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);
  }
}

總結一下,我們在做軟件開發的時候,一定要有抽象意識、封裝意識、接口意識。在定義接口的時候,不要暴露任何實現細節。接口的定義只表明做什麼,而不是怎麼做。而且,在設計接口的時候,我們要多思考一下,這樣的接口設計是否足夠通用,是否能夠做到在替換具體的接口實現的時候,不需要任何接口定義的改動。

是否需要爲每個類定義接口?
  1. 前面我們也提到,這條原則的設計初衷是,將接口和實現相分離,封裝不穩定的實現,暴露穩定的接口。上游系統面向接口而非實現編程,不依賴不穩定的實現細節,這樣當實現發生變化的時候,上游系統的代碼基本上不需要做改動,以此來降低代碼間的耦合性,提高代碼的擴展性
  2. 從這個設計初衷上來看,如果在我們的業務場景中,某個功能只有一種實現方式,未來也不可能被其他實現方式替換,那我們就沒有必要爲其設計接口,也沒有必要基於接口編程,直接使用實現類就可以了。

六. 爲何說要多用組合少用繼承?如何決定該用組合還是繼承?

爲什麼不推薦使用繼承?

繼承最大的問題就在於:繼承層次過深、繼承關係過於複雜會影響到代碼的可讀性和可維護性。

組合相比繼承有哪些優勢?
  1. 接口表示具有某種行爲特性。針對“會飛”這樣一個行爲特性,我們可以定義一個 Flyable 接口,只讓會飛的鳥去實現這個接口。對於會叫、會下蛋這些行爲特性,我們可以類似地定義 Tweetable 接口、EggLayable 接口
  2. 例如:

public interface Flyable {
  void fly();
}
public interface Tweetable {
  void tweet();
}
public interface EggLayable {
  void layEgg();
}
public class Ostrich implements Tweetable, EggLayable {//鴕鳥
  //... 省略其他屬性和方法...
  @Override
  public void tweet() { //... }
  @Override
  public void layEgg() { //... }
}
public class Sparrow impelents Flayable, Tweetable, EggLayable {//麻雀
  //... 省略其他屬性和方法...
  @Override
  public void fly() { //... }
  @Override
  public void tweet() { //... }
  @Override
  public void layEgg() { //... }
}
  1. 問題:接口只聲明方法,不定義實現。也就是說,每個會下蛋的鳥都要實現一遍 layEgg() 方法,並且實現邏輯是一樣的,這就會導致代碼重複的問題。那這個問題又該如何解決呢?
  2. 們可以針對三個接口再定義三個實現類,它們分別是:實現了 fly() 方法的 FlyAbility 類、實現了 tweet() 方法的 TweetAbility 類、實現了 layEgg() 方法的 EggLayAbility 類。然後,通過組合和委託技術來消除代碼重複
  3. 例子:

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(); // 委託
  }
}
如何判斷該用組合還是繼承
  1. 如果類之間的繼承結構穩定(不會輕易改變),繼承層次比較淺(比如,最多有兩層繼承關係),繼承關係不復雜,我們就可以大膽地使用繼承。反之,系統越不穩定,繼承層次很深,繼承關係複雜,我們就儘量使用組合來替代繼承
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章