博客 / 詳情

返回

淺談:從面向抽象編程再到IOC控制反轉

1.WHAT-面向抽象編程的例子

1.1 面向對象

一般來説,面向對象編程是我們比較常見的,即將一個實際的項目分成多個類(對象),賦予這些對象屬性和方法,從而實現編程。

比如,我們要編寫一個校園管理平台,分別管理老師和學生,非常直觀地我們就能把老師類和同學類給歸納出來,老師有教學的職能(方法)有性別年齡(屬性),學生則有唸書的職能(方法),也有性別年齡(屬性)。

1.2 面向對象進階到面向抽象

在我看來,面向對象其實就是:歸納總結。我們程序猿將產品經理提出的直觀需求進行歸納總結,可以得到歸納的類。如果我們能把歸納的類再進一步地歸納(或者説抽象),我們就得到了一個抽象類。

同一個例子,除了老師類,學生類,我們還可以歸納一個“人”類,這個“人”類有着一個人應具備的屬性(如性別,年齡,身高,體重)和能力(如吃飯,睡覺)。
(P.S:當然,歸納出一個抽象類只是面向抽象的一部分,並不是面向抽象的全部。)

2.WHY-為什麼要面向抽象編程

2.1 代碼編寫

在代碼編寫層面,有過編程經驗的我相信都會或多或少經歷過:將公用的部分抽離出來,這樣就不用重複編寫代碼。例如組件、包,我們不需要去編寫重複性極強的代碼。

2.2 代碼維護

在代碼編寫層面,更多還是面向對象的思維在起作用。針對一個項目,我們正常的思維往往是自上而下的,因此先歸納出一個抽象類,再得到具體類,這是很正常的思維邏輯,這樣寫出來的代碼,層次分明,但還是遠遠不夠。

一個大型項目,往往難的不是它的開發,而是它的維護。大型項目裏,即使我們用抽象類的方式將類進行了歸納總結,但類與類之間仍然不可避免地有着非常強的依賴關係。

舉個栗子:我們A類抽象成IA類,但我們仍然要去實例化類A,而實例化類A的啓動類B,依然引用了A,啓動類B和A的依賴性仍然存在。(java的多態)

2.2.1 開閉原則(OCP)

首先,要想更好地實現代碼維護,我們就要遵循開閉原則:允許拓展,禁止修改。
image.png
見上圖,上圖的四個齒輪是我們的四個類,它們互相依賴從而推動程序的運行。但產品經理有一天突然提出了一個新需求,這個需求可能我們直接修改類A就實現了,然而類A一旦發生變動,我們的類B,類D也得跟着動,然後連帶着C也可能要跟着動,這樣的變動是災難性的。

由此可見,要遵循開閉原則,並不是一件容易的事情,這必須是在設計代碼框架時就得思考清楚。那麼到底怎麼才能做到開閉原則呢?

2.2.2 面向抽象編程

我們需要明白:變化(產品經理)是固然存在的,是不可磨滅的。代碼想要做到只拓展,完全不修改是不可能的。因此,遵循OCP原則,我們應該做到的是:
1.將代碼的變化隔離至一處。
2.降低類與類之間的關聯性,讓每個類可以獨立變化而不影響到其他類的使用。

而面向抽象編程正是將這些變化抽象,將類與類關聯性降低。它有三個方面:
1.用變量抽象化“值”的變化;
2.用Interface接口抽象化“類”的變化;
3.用一個容器來封裝類的實例化,再用反射機制來抽象化類的實例化。

2.2.3 工廠模式

面向抽象編程的第一個方面在日常編程中其實是非常常用的,像寫配置文件,此處就不再多做贅述。

而第二點,上文已經提到過了,用一個抽象類來實現多個不同的具體類。具體在java中,就是用多態的形式引用類。

Fu fu = new Zi1()// new Zi2()

最主要還是第三點:用容器封裝類的實例化。在類已經被抽象化的前提下,類的實例化成了類與類之間相互依賴的關鍵所在,正是因為一個類直接在另一個類中進行了實例化,兩個類才產生耦合。

image.png
見上圖,還是四個齒輪,但我這次在四個齒輪之間加入了一個大齒輪,今後不再是小齒輪互相推動運轉,而是大齒輪帶動四個小齒輪進行運轉。顯而易見,小齒輪之間的關聯性已經被降低非常多了,之後不管是修改小齒輪的內部,或者是要更換小齒輪,都比原來的風險要小的多。

回到代碼上來,這個大齒輪,其實就是一個容器,用這個容器實現類與類之間的關聯。而要實現這個容器,我們可以採用工廠模式+反射機制

用具體的代碼來實現一下工廠模式+反射機制:
假設我們需要做一個英雄聯盟的選英雄,並釋放R技能的簡單demo。
單純的工廠模式:

// 主控類
public class Main {
  public static void main(String[] args) throws Exception {
      String name = Main.getPlayerInput();
      Hero hero = HeroFactory.getHero(name);
      hero.r();
  }

  private static String getPlayerInput(){
      System.out.println("Enter a Hero's Name");
      Scanner scanner = new Scanner(System.in);
      String name = scanner.nextLine();
      return name;
  }
}
// 工廠類
public class HeroFactory {
  public static Hero getHero(String name) throws Exception {
      Hero hero;
      switch (name){
          case "Diana":
              hero = new Diana();
              break;
          case "Irelia":
              hero = new Irelia();
              break;
          case "Camile":
              hero = new Camile();
              break;
          default:
              throw new Exception();
      }
      return hero;
  }
}

乍看上去,好像我們通過抽象所有英雄有了一個Hero類,同時用工廠來生成它,已經很好地封裝了,但是,我們每一次添加英雄,都需要去變動這個工廠,這個工廠作為我們的“大齒輪”,顯然還不夠資格,它連結了各個英雄類,直接修改它依然有影響連結的風險。
(感覺這個例子可能説服力不太強,因為各個英雄類之間關聯性本身就不大,看起來修改工廠也問題不大,我只能直接從齒輪直觀來解釋了,不知道大家有無更好的例子- -。)。

工廠模式+反射機制:

public static Hero getHero(String name) throws Exception {
    String classStr = "reflect.hero." + name;
    Class<?> cla = Class.forName(classStr);
    Object obj = cla.newInstance();
    return (Hero) obj;
}

單純的工廠模式,每一次都需要的變動是因為實例化過程還未被抽象,若是加上了java的反射機制,那就不同了,類的實例化完全取決於用户輸入的英雄名(或者説配置文件中的英雄名)。無論我們新增任何英雄還是要調整任何英雄,該工廠都無需發生變化,

顯而易見,這個工廠基本實現了我們圖中的“大齒輪”,同時,我們可以看到,這樣的一個大齒輪,很好地把我們控制代碼運行的類和實現業務邏輯的類實現了分離————控制代碼:MAIN+FACTORY;業務代碼:各個英雄類DIANA+IRELIA+CAMILE。

因此,我們能將OCP原則換一種方式理解:儘量不要修改負責控制的代碼,只修改業務邏輯的代碼

3.IOC控制反轉

假設有一個A類需要引用B類,那麼一般來説,我們會在A類中實例化B類;而引入一個容器,將A類,B類都事先引入容器中,再由容器把B類注入到A類中,這就實現了IOC控制反轉。

從這個概念來看,工廠模式+反射機制,這種模式看似已經實現了IOC控制反轉(將英雄類注入到控制類之中)。但是實際情況中,生成類的工廠並非上面的例子那麼簡單,它可能需要根據某些特定的條件來判斷注入哪一個類,還需要考慮如何實例化類:是否帶參數,參數還可能發生變動。換句話説,我們的工廠不可能是一成不變的。

既然工廠還存在如此繁瑣的業務邏輯變化,我們自然不能直接將工廠放入控制的代碼中(即直接引用HEROFACTORY)。

同時,工廠模式+反射雖然隔離了類的實例化,但仍然沒脱離正向思維:控制類(MAIN)需要英雄類(HERO),才去工廠中生成我們要的英雄。而我們希望的是:容器一開始就將英雄類注入到控制類之中,這才是IOC控制反轉。

因此,我們需要Springboot框架,來幫我們掃描類生成bean,然後將bean注入到對應的控制類中。此處不過多做展開,待下次再記錄一下對Springboot框架的相關理解。

自己的疑問:

1.工廠模式+反射已經解決了控制類不發生變動了,那麼Spring框架相對於它的優勢到底在哪呢?因為Spring框架還需要先將類生成bean加入到IOC容器之中,還多了這麼一步,所以感覺較之工廠模式+反射來説還更加繁瑣。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.