由淺入深理解 IOC 和 DI

踏雪彡尋梅發表於2020-08-31

由淺入深理解 IOC 和 DI

開閉原則 OCP(Open Closed Principle)

  • 對擴充套件開放,對修改封閉。
    • 修改一處程式碼可能會引起其他地方的 bug,最好的方式就是新增業務模組/類代替原來的業務模組/類,使出現 bug 的機率變小。
  • 必須滿足此原則的程式碼才能算作好的可維護的程式碼。

面向抽象程式設計

  • 只有面向抽象程式設計,才能夠逐步實現開閉原則。
    • 面臨的兩個問題:
      • 統一方法的呼叫。
      • 統一物件的例項化。
  • 可實現面向抽象程式設計的語法:
    • 介面(interface)
    • 抽象類(abstract)
  • 只有有了介面和抽象類的概念,多型性才能夠得到很好的支援。
  • 面向抽象程式設計的目的: 實現可維護的程式碼,實現開閉原則。
    • 面向抽象 -> OCP -> 可維護的程式碼

逐步理解實現 IOC 和 DI 的過程(LOL Demo 示例)

比較尷尬的編寫程式新增需求/更改需求的做法

  • 程式示例:

    • 各英雄類

      /**
      * <p>
      * Camille 英雄
      * </p>
      *
      * @author 踏雪彡尋梅
      * @version 1.0
      * @date 2020/7/28 - 10:21
      * @since JDK1.8
      */
      public class Camille {
          public void q() {
              System.out.println("Camille Q");
          }
      
          public void w() {
              System.out.println("Camille W");
          }
      
          public void e() {
              System.out.println("Camille E");
          }
      
          public void r() {
              System.out.println("Camille R");
          }
      }
      
      /**
       * <p>
       * Diana 英雄
       * </p>
       *
       * @author 踏雪彡尋梅
       * @version 1.0
       * @date 2020/7/28 - 10:00
       * @since JDK1.8
       */
      public class Diana {
          public void q() {
              System.out.println("Diana Q");
          }
      
          public void w() {
              System.out.println("Diana W");
          }
      
          public void e() {
              System.out.println("Diana E");
          }
      
          public void r() {
              System.out.println("Diana R");
          }
      }
      
      /**
       * <p>
       * Irelia 英雄
       * </p>
       *
       * @author 踏雪彡尋梅
       * @version 1.0
       * @date 2020/7/28 - 10:16
       * @since JDK1.8
       */
      public class Irelia {
          public void q() {
              System.out.println("Irelia Q");
          }
      
          public void w() {
              System.out.println("Irelia W");
          }
      
          public void e() {
              System.out.println("Irelia E");
          }
      
          public void r() {
              System.out.println("Irelia R");
          }
      }
      
    • 選擇英雄釋放技能 main 函式

      /**
       * <p>
       * 傳統編寫程式新增需求/更改需求的做法
       * </p>
       *
       * @author 踏雪彡尋梅
       * @version 1.0
       * @date 2020/7/28 - 10:01
       * @since JDK1.8
       */
      public class Main {
          public static void main(String[] args) {
              // 選擇英雄
              String name = Main.getPlayerInput();
              // 新增英雄時需要改此處程式碼
              switch (name) {
                  case "Diana":
                      Diana diana = new Diana();
                      diana.r();
                      break;
                  case "Irelia":
                      Irelia irelia = new Irelia();
                      irelia.r();
                      break;
                  case "Camille":
                      Camille camille = new Camille();
                      camille.r();
                      break;
                  default:
                      break;
              }
          }
      
          private static String getPlayerInput() {
              Scanner scanner = new Scanner(System.in);
              System.out.println("請輸入一個英雄的名稱: ");
              return scanner.nextLine();
          }
      }
      
  • 從上面的程式碼,可以看出以下幾點:

    • 當增加新的英雄時,需要修改 switch 處的程式碼,增加新的 case
    • 各個 case 中的程式碼都存在著 new 一個某某英雄,並且呼叫了釋放技能的方法。
    • 在真實專案中,大量存在著這樣的 new 是不好的,因為真實專案中類和類的依賴是非常之多的,這個類依賴那個類,那個類又依賴了另一個類。
    • 如果大量存在著這樣的 new 操作,程式碼間的耦合度將變得非常高,當某個類的需求產生變化的時候,一旦修改程式碼,其他依賴這個類的地方就很有可能引起很多 bug,同時依賴的地方也可能需要修改大量的程式碼。
    • 通過上面例子也可以看出,在建立例項物件之後,會呼叫這個物件的方法,上面的例子只是簡單地呼叫了一個方法,而在真實專案中,依賴的類可能需要呼叫它的很多方法。
    • 所以一旦依賴的這個類的程式碼產生了變化,比如某某方法不用了,依賴的地方就需要刪除這個呼叫,而依賴這個類的類很可能有許多個,就需要更改很多地方的程式碼,可見耦合度之高,這也就是為什麼這種程式碼一旦修改,就很可能出現多個 bug 的原因。
    • 所以,對於這種程式碼,是需要優化和改良的,不能依賴的太過具體,而是要依賴抽象,即面向抽象程式設計,下面就一步步演進這個過程,達到逐步理解 IOCDI 的目的。

使用 interface 介面統一方法的呼叫

  • 程式示例:

    • 英雄技能介面類

      /**
       * <p>
       * 英雄技能介面類
       * </p>
       *
       * @author 踏雪彡尋梅
       * @version 2.0
       * @date 2020/7/28 - 10:31
       * @since JDK1.8
       */
      public interface ISkill {
          void q();
      
          void w();
      
          void e();
      
          void r();
      }
      
    • 各英雄類

      /**
       * <p>
       * Camille 英雄
       * </p>
       *
       * @author 踏雪彡尋梅
       * @version 2.0
       * @date 2020/7/28 - 10:21
       * @since JDK1.8
       */
      public class Camille implements ISkill {
          @Override
          public void q() {
              System.out.println("Camille Q");
          }
      
          @Override
          public void w() {
              System.out.println("Camille W");
          }
      
          @Override
          public void e() {
              System.out.println("Camille E");
          }
      
          @Override
          public void r() {
              System.out.println("Camille R");
          }
      }
      
      /**
       * <p>
       * Diana 英雄
       * </p>
       *
       * @author 踏雪彡尋梅
       * @version 2.0
       * @date 2020/7/28 - 10:00
       * @since JDK1.8
       */
      public class Diana implements ISkill {
          @Override
          public void q() {
              System.out.println("Diana Q");
          }
      
          @Override
          public void w() {
              System.out.println("Diana W");
          }
      
          @Override
          public void e() {
              System.out.println("Diana E");
          }
      
          @Override
          public void r() {
              System.out.println("Diana R");
          }
      }
      
      /**
       * <p>
       * Irelia 英雄
       * </p>
       *
       * @author 踏雪彡尋梅
       * @version 2.0
       * @date 2020/7/28 - 10:16
       * @since JDK1.8
       */
      public class Irelia implements ISkill {
          @Override
          public void q() {
              System.out.println("Irelia Q");
          }
      
          @Override
          public void w() {
              System.out.println("Irelia W");
          }
      
          @Override
          public void e() {
              System.out.println("Irelia E");
          }
      
          @Override
          public void r() {
              System.out.println("Irelia R");
          }
      }
      
    • 選擇英雄釋放技能 main 函式

      /**
       * <p>
       * 使用 interface 統一方法的呼叫
       * </p>
       *
       * @author 踏雪彡尋梅
       * @version 2.0
       * @date 2020/7/28 - 10:29
       * @since JDK1.8
       */
      public class Main {
          public static void main(String[] args) throws Exception {
              ISkill iSkill;
      
              // 選擇英雄
              String name = Main.getPlayerInput();
              // 新增英雄時也需要改此處程式碼
              // 這個 switch 提取成一個方法(例如工廠模式)之後,這裡的程式碼就會變得簡單
              // 只有一段程式碼不負責物件的例項化,即沒有 new 的出現,才能保持程式碼的相對穩定,才能逐步實現 OCP
              switch (name) {
                  case "Diana":
                      iSkill = new Diana();
                      break;
                  case "Irelia":
                      iSkill = new Irelia();
                      break;
                  case "Camille":
                      iSkill = new Camille();
                      break;
                  default:
                      throw new Exception();
              }
              // 呼叫技能,現在這個版本使用介面統一了方法的呼叫,但還不能統一物件的例項化
              // 統一了方法的呼叫是意義非常重大的
              // 真實專案中,方法的呼叫可能非常的多或者複雜,這種情況下把方法的呼叫統一起來,集中在一個介面的方法上面,這個意義非常重大
              iSkill.r();
          }
      
          private static String getPlayerInput() {
              Scanner scanner = new Scanner(System.in);
              System.out.println("請輸入一個英雄的名稱: ");
              return scanner.nextLine();
          }
      }
      
  • 從以上程式碼示例可得出以下幾點:

    • 單純的 interface 可以統一方法的呼叫,但是它不能統一物件的例項化。
      • 統一了方法的呼叫是意義非常重大的。
        • 真實專案中,方法的呼叫可能非常的多或者複雜,這種情況下把方法的呼叫統一起來,集中在一個介面的方法上面,這個意義非常重大。
      • 抽象的難點在於將 new 物件這個操作變得更加的抽象,而不是具體。
    • 物件導向很多時候都是在做兩件事情: 例項化物件,呼叫方法(完成業務邏輯)。
      • 所以僅僅達到統一方法的呼叫還不足夠,還需要達到統一物件的例項化。
    • 由以上幾點可得出只有一段程式碼不負責物件的例項化,即沒有 new 的出現,才能保持程式碼的相對穩定,才能逐步實現 OCP。(表象)
      • 實質: 一段程式碼如果要保持穩定,就不應該負責物件的例項化。
      • 如果各個類中有大量的例項化物件的過程,那麼一旦產生變化,影響將非常大。
    • 當然,物件例項化是不可能消除的,我們需要把物件例項化的過程轉移到其他的程式碼片段裡,即把所有這些物件例項化的過程全部隔離到一個地方,這樣子除了這個地方外的其他地方的程式碼就會變得非常穩定(最簡單的方式為使用工廠模式,接下來的版本將演示這個過程)。

使用工廠模式把物件例項化的過程隔離

  • 三種子模式:

    • 簡單工廠模式
      • 對生產的物件的一種抽象。
    • 普通工廠模式
    • 抽象工廠模式
      • 對工廠的一種抽象。
  • 使用簡單工廠模式把物件例項化的過程轉移到其他的程式碼片段裡:

    • 程式示例:
      • 英雄技能介面類

        /**
         * <p>
         * 英雄技能介面類
         * </p>
         *
         * @author 踏雪彡尋梅
         * @version 3.0
         * @date 2020/7/28 - 10:31
         * @since JDK1.8
         */
        public interface ISkill {
            void q();
        
            void w();
        
            void e();
        
            void r();
        }
        
      • 各英雄類

        /**
         * <p>
         * Camille 英雄
         * </p>
         *
         * @author 踏雪彡尋梅
         * @version 3.0
         * @date 2020/7/28 - 10:21
         * @since JDK1.8
         */
        public class Camille implements ISkill {
            @Override
            public void q() {
                System.out.println("Camille Q");
            }
        
            @Override
            public void w() {
                System.out.println("Camille W");
            }
        
            @Override
            public void e() {
                System.out.println("Camille E");
            }
        
            @Override
            public void r() {
                System.out.println("Camille R");
            }
        }
        
        /**
         * <p>
         * Diana 英雄
         * </p>
         *
         * @author 踏雪彡尋梅
         * @version 3.0
         * @date 2020/7/28 - 10:00
         * @since JDK1.8
         */
        public class Diana implements ISkill {
            @Override
            public void q() {
                System.out.println("Diana Q");
            }
        
            @Override
            public void w() {
                System.out.println("Diana W");
            }
        
            @Override
            public void e() {
                System.out.println("Diana E");
            }
        
            @Override
            public void r() {
                System.out.println("Diana R");
            }
        }
        
        /**
         * <p>
         * Irelia 英雄
         * </p>
         *
         * @author 踏雪彡尋梅
         * @version 3.0
         * @date 2020/7/28 - 10:16
         * @since JDK1.8
         */
        public class Irelia implements ISkill {
            @Override
            public void q() {
                System.out.println("Irelia Q");
            }
        
            @Override
            public void w() {
                System.out.println("Irelia W");
            }
        
            @Override
            public void e() {
                System.out.println("Irelia E");
            }
        
            @Override
            public void r() {
                System.out.println("Irelia R");
            }
        }
        
      • 生產英雄的工廠類

        /**
         * <p>
         * 英雄工廠類,生產或例項化英雄類,把物件例項化的過程隔離
         * </p>
         *
         * @author 踏雪彡尋梅
         * @version 3.0
         * @date 2020/7/28 - 21:11
         * @since JDK1.8
         */
        public class HeroFactory {
            /**
             * 簡單工廠例項化英雄類
             *
             * @param name 英雄名稱
             * @return 返回英雄名稱對應的例項
             */
            public static ISkill getHero(String name) throws Exception {
                ISkill iSkill;
        
                // 變化是導致程式碼不穩定的本質原因
                // 所有的變化最終其實都要交給不同的物件去處理,當業務或使用者的輸入有了變化的時候,必須要建立不同的物件去響應這些變化
                // 這裡的變化: 使用者的輸入,選擇英雄導致的不穩定,根據使用者的輸入例項化不同的物件
                // 也例如改動程式使用的資料庫,從 MySQL 更改為 Oracle
                // 如何消除這個變化?
                // 思考:
                // 1. 這裡是使用者只能夠輸入一個字串,把輸入的字串轉換為一個物件
                // 2. 但是如果使用者能夠直接輸入一個物件傳給程式,這個 switch 就可以被幹掉(使用反射解決,把輸入的字串轉換為一個物件)
                switch (name) {
                    case "Diana":
                        iSkill = new Diana();
                        break;
                    case "Irelia":
                        iSkill = new Irelia();
                        break;
                    case "Camille":
                        iSkill = new Camille();
                        break;
                    default:
                        throw new Exception();
                }
        
                return iSkill;
            }
        }
        
      • 選擇英雄釋放技能 main 函式

        /**
         * <p>
         * 使用簡單工廠模式把物件例項化的過程轉移到其他的程式碼片段裡(IOC 的雛形)
         * </p>
         *
         * @author 踏雪彡尋梅
         * @version 3.0
         * @date 2020/7/28 - 21:06
         * @since JDK1.8
         */
        public class Main {
            public static void main(String[] args) throws Exception {
                // 選擇英雄
                String name = Main.getPlayerInput();
                // 呼叫工廠方法,這裡把 new 的操作幹掉了,這裡的程式碼已經相對穩定了,新增英雄時這裡的程式碼不需要再更改,只需要更改工廠方法的程式碼
                // 對於 main 方法而言,它實現了 OCP,而工廠方法中的程式碼還沒有實現 OCP
                // 雖然這裡的程式碼已經相對穩定了,但是還引用著 HeroFactory 工廠類,對於這行程式碼,還不是非常穩定,還存在著可能更換修改的可能
                // 例如說: HeroFactory 的 getHero 方法是個例項方法,那麼 HeroFactory 也需要 new 出來,這種情況下會存在著修改程式碼的可能
                // 如果業務邏輯足夠複雜,可能存在很多這種工廠類,這樣看起來對於工廠類而言,需求變更時還是需要改動很多程式碼
                // 當然也可以使用抽象工廠將工廠抽象化使這裡變得穩定起來,但這裡不演示了,這裡只是演示一個如何隔離變化的過程,所以假設這裡是穩定的,是一個超級工廠,能夠生產專案的各種物件
                // 當假設有一個超級的工廠之後,這個工廠可以相容整個專案的工廠,把整個專案的所有的變動都封裝到一起,從而保證除了這個超級工廠之外的程式碼都是穩定的,這樣這個工廠就有了意義
                // 其實 IOC 也就是相當於一個非常大的容器一樣,把所有的變化都集中到了一個地方,寫其他的程式碼就會變得非常容易,不再需要在整個專案中到處更改程式碼,如果出現了變化,只需要讓容器去負責改變即可
                // spring ioc 中的 ApplicationContext 就類似於這個超級工廠,通過 ApplicationContext 可以獲取各種各樣的物件,不過 ApplicationContext 在 spring 中給的是一個介面,即抽象工廠模式
                // 需要注意的是: 生產物件只是 IOC 的一部分,不是 IOC 的全部
                ISkill iSkill = HeroFactory.getHero(name);
                // 呼叫技能
                iSkill.r();
            }
        
            private static String getPlayerInput() {
                Scanner scanner = new Scanner(System.in);
                System.out.println("請輸入一個英雄的名稱: ");
                return scanner.nextLine();
            }
        }
        
  • 從以上例子可得出以下幾點:

    • 程式碼中總是會存在不穩定,要儘可能地隔離這些不穩定,保證其他的程式碼是穩定的。隔離不穩定其實就是在隔離變化。
      • 其實 IOC 就是將這些不穩定(變化)給封裝、隔離到了一塊,保證其他地方的程式碼是穩定的。
      • 變化是導致程式碼不穩定的本質原因。
        • 變化有這麼兩大類變化:
          • 使用者的輸入、使用者的選擇、使用者的操作造成的變化。
          • 軟體自身的業務需求或技術選擇有了變化。
            • 注意事項:
              • 對於技術選擇的改變,例如從使用 MySQL 更換到 Oracle,如果將這個變化提取到配置檔案中,那麼配置檔案的變化是允許的,並不違反 OCP
                • 配置檔案是屬於系統外部的,而不屬於程式碼本身。(這裡的配置檔案也可以理解為使用者的輸入,把需求的變化隔離到了配置檔案中)
        • 所有的變化最終其實都要交給不同的物件去處理,當業務或使用者的輸入有了變化的時候,必須要建立不同的物件去響應這些變化。
          • 那麼如何消除這些變化呢?
            • 在上面的示例中,使用者只能輸入一個字串,然後在工廠方法內去判斷使用者的輸入的變化,建立不同的物件去響應這些變化。
            • 同時,如果有新增的英雄,勢必要工廠方法中的 switch 程式碼。
            • 那麼如果有這麼一個機制,可以實現使用者的輸入輸入進來就是一個物件,然後就建立這個物件進行響應,而不是像上面的判斷字串,那麼就可以幹掉 switch,使這裡的程式碼變得更加簡單,更加穩定。
            • 對於這種機制,也就是反射機制,下面的版本將演示這個過程。

使用反射隔離工廠中的變化,讓使用者直接輸入一個物件

  • 對於這個版本,只有工廠類發生了變動,所以只展示工廠類的程式碼和 main 函式的程式碼

  • 程式示例:

    • 生產英雄的工廠類

      /**
       * <p>
       * 英雄工廠類,生產或例項化英雄類,把物件例項化的過程隔離
       * </p>
       *
       * @author 踏雪彡尋梅
       * @version 4.0
       * @date 2020/7/28 - 21:11
       * @since JDK1.8
       */
      public class HeroFactory {
          /**
           * 簡單工廠例項化英雄類
           *
           * @param name 英雄名稱
           * @return 返回英雄名稱對應的例項
           */
          public static ISkill getHero(String name) throws Exception {
              // 使用反射隔離工廠中的變化,讓使用者直接輸入一個物件,即把使用者輸入的字串轉換為一個物件
              // 反射的作用: 動態的建立物件
              // 根據輸入的英雄名稱獲取元類
              // 類是物件的抽象,描述物件
              // 元類是類的抽象,是對類的描述
              // 需要注意在名稱前加上包路徑 cn.xilikeli.lol.v4.hero.
              name = "cn.xilikeli.lol.v4.hero." + name;
              Class<?> classA = Class.forName(name);
              // 通過元類例項化對應的例項物件
              // 注意點: java8 之後 newInstance 已經廢棄
              // 新版本中使用 classA.getDeclaredConstructor().newInstance()
              Object obj = classA.newInstance();
              // 強制轉型返回
              return (ISkill) obj;
          }
      }
      
    • 選擇英雄釋放技能 main 函式

      /**
       * <p>
       * 使用反射隔離工廠中的變化,讓使用者直接輸入一個物件,即把使用者輸入的字串轉換為一個物件
       * </p>
       *
       * @author 踏雪彡尋梅
       * @version 4.0
       * @date 2020/7/28 - 22:26
       * @since JDK1.8
       */
      public class Main {
          public static void main(String[] args) throws Exception {
              // 選擇英雄
              String name = Main.getPlayerInput();
              ISkill iSkill = HeroFactory.getHero(name);
              // 呼叫技能
              iSkill.r();
          }
      
          private static String getPlayerInput() {
              Scanner scanner = new Scanner(System.in);
              System.out.println("請輸入一個英雄的名稱: ");
              return scanner.nextLine();
          }
      }
      
  • 從以上示例可得出以下幾點:

    • 在使用了反射機制之後,已經消除了所有的變化,不管輸入的是什麼,程式碼都不需要再做更改了,程式碼已經變得非常穩定了。
    • Spring 內部其實也是使用了類似現在這種工廠模式 + 反射的機制,但是要比現在實現的這種形式更加地完善更加地聰明。
      • 現在這種形式,每次輸入都進行一次反射是效能比較低的,因為頻繁地反射會使效能變低。
      • Spring 中取到或例項化一個物件之後,會把這個物件放到它的快取中去,下次要再取或建立相同的物件的時候,不會進行反射,而是從快取中拿(和 DI 有關係)。
    • 需要注意的是: 現在這種工廠模式 + 反射的機制還不是 IOCDI
      • 現在這個版本並沒有運用到任何 IOCDI 的原理,只是讓程式碼變得非常穩定。
      • 現在這種形式是正向思維,雖然現在是實現了需要什麼就可以返回什麼,但是現在拿到物件的方式依然是需要什麼然後去呼叫什麼類下面的什麼方法得到什麼的方式。
      • IOC 是控制反轉,現在這裡並沒有反轉,同時也沒有注入,現在只是實現了 OCP
    • 那麼,問題來了,現在已經實現了 OCP,那麼還需要 IOCDI 幹嘛?
      • 因為現在的實現使用起來還不方便,是一個正向的思維。每次建立一個物件都需要引入這個工廠類呼叫其方法。也就是說工廠的方式在實現的邏輯中是正向的建立物件,而 IOC 是反向的,是容器根據需求主動注入的。
      • 那麼有什麼方法可以讓工廠類不出現,直接可以拿到需要的物件?
        • 這就是 IOCDI 需要做的事情。IOCDI 的雛形至此也就出來了。
        • 如果需要的物件可以由一個什麼東西例如某個容器中將其傳進來,這個需要的物件就可以不需要使用工廠類建立了,此時程式碼變得更加簡單、更加穩定。
        • 這種形式就是 IOCDI 的體現,把物件的控制權交給了容器,即控制反轉;獲取物件時只需要直接使用它即可,容器會自動把這個物件建立好給傳入進來,即依賴注入。
        • 在前面也談到了配置檔案,其實 IOC 簡單理解就是 工廠模式 + 反射機制 + 配置檔案 組成了一個大的容器,我們需要什麼就配置什麼,容器就會建立物件,把建立好的物件提供給我們,這個過程中我們沒有感受到建立物件的正向過程,而是感受到使用的物件是容器給我們的,這是一個反向的過程,也就是控制權由我們反轉到了容器中,容器掌控著建立物件的權利,即控制反轉。
    • 到了此處,也可以隱隱約約地明白了 IOCDI 到底是個什麼東西了,接下來再對這兩個東西解釋一下,以便理解地更加深刻。

IOC/DI/DIP

  • DIP(Dependency Inversion Principle,依賴倒置)

    • 高層模組不應該依賴低層模組,兩者都應該依賴抽象。
      • 高層: 抽象,抽象也就是站在更高的角度概括具體。
      • 低層: 具體的實現。
    • 抽象不應該依賴細節。
    • 細節應該依賴抽象。
    • 倒置: 正常編寫程式碼時可能會 new 一個物件,即依賴了一個具體;而倒置就是不依賴具體而是反過來依賴一個介面(抽象)。
  • DI

    • 物件與物件之間的相互作用必定是要產生依賴的,這個是不可避免的,關鍵是產生依賴的方式是多種多樣的,比如 new 的方式,不過這個方法不好,因為是一個具體例項化的過程,如果依賴的類的程式碼改變了,使用這個依賴的類的地方就會變得不穩定。
    • 更好的方式: 不再使用 new 的方式,而是讓容器(容器可以理解為在系統的最上層)把需要依賴的類的抽象(例如介面)的實現給注入進來,雖然也產生了依賴,但是依賴的形式是不同的,是注入進來的,並且注入進來的類是抽象的實現,依賴只是依賴抽象的介面,相比 new 來說產生的依賴不這麼具體。
    • 依賴注入的幾種形式:
      • 屬性注入

        public class A {
          private IC ic;
        
          // 屬性注入,容器在例項化 A 的時候會 set 一個 ic 進來
          public void setIc(IC ic) {
            this.ic = ic;
          }
        }
        
      • 構造注入

        public class A {
          private IC ic;
        
          // 構造注入,容器在例項化 A 的時候會給建構函式傳一個 ic 進來
          public A(IC ic) {
            this.ic = ic;
          }
        }
        
      • 介面注入(用的較少)

    • 依賴注入的原理
      • 容器在建立一個物件例項的時候可以把這個物件依賴的物件例項注入進去。相當於我們自己寫程式碼的時候 new 物件時把一個物件傳進去或賦值以及可以呼叫 set 方法傳入一個物件或賦值。
    • 依賴注入在更高角度的意義
      • 容器其實是在裝配這一個個的物件。
        • 即各個類依賴了各個類,他們彼此之間的裝配不是由他們在程式碼中 new 出來的。而是全部交給容器,由容器來裝配。
        • 當把裝配的過程交給了容器了之後,我們在編寫類時的只需要負責類的編寫就行了,而不需要關心如何裝配。由容器來決定某個類依賴的物件到底是哪一個物件,由它把物件注入到類裡。
        • 同時在編寫類的時候,也不需要關心依賴的物件,因為依賴的不是一個具體的物件,而是一個抽象,例如介面。最終例項化的是這個介面的哪一個實現類,編寫類的時候是不需要關心的,只需要關心介面即可,對於例項化哪個實現類則由容器來決定。這就保證了我們在編寫一個個類的時候類是獨立的,保證了程式碼的穩定性。保證了系統的耦合度是非常低的。即面向抽象的重要性,面向介面去程式設計。
  • IOC

    • IOC 本身概念非常抽象和模糊,只是展示了一種思想,並沒有給出具體的實現。
    • DI 可以看做 IOC 的一個具體的實現。
    • DI 的角度理解 IOC
      • 當在一個類(這裡用 A 表示)中使用 new 來建立一個需要的物件時,主控類為 A
      • 如果應用了 DI 之後,有了容器的概念,此時主控方為容器,主控的地方不再是 A 類了,這個其實就是實現了控制反轉,全部交給了容器去控制。
    • IOC 的奧義
      • 整個程式執行的控制權是誰的?
        • 其實本質上還是由程式設計師決定的。
          • 如果需求固定不變,就沒有什麼問題。
          • 但是如果存在著變化,就會有問題。一旦產生了變化,程式設計師就要去更改控制程式碼(變化指的不是新增的業務程式碼,而是指控制程式碼,控制程式碼: 例如原來 new 了一個什麼,要改成 new 一個新的什麼)。
          • 如果此時反轉過來,程式設計師不再控制這些控制程式碼,而是交給別人控制,所有除了控制程式碼之外的程式碼都是非常穩定的。也就是反過來是使用者在控制程式碼,也可以理解為產品經理來控制整個應用程式。
            • 例如: 原來用的 MySQL,產品經理改成了 Oracle,程式設計師肯定要負責新增程式碼實現 Oracle 的功能,但是至於應用程式用的是原來的 MySQL 還是新加的 Oralce,現在不再由程式設計師去控制,而是產品經理去控制,控制權由產品經理決定,即控制反轉。
      • IOC 舉例
        • 積木生產廠家(程式設計師)
          • 只負責生產一個個積木,不再負責積木的搭建。
          • 由玩具/使用者負責使用生產的這些一個個積木,搭建出各種各樣的形狀。
          • 原來的話可能是直接把積木給組裝好,如果使用者說不想要這個組裝好的積木,就需要廠家來改(控制);但現在不一樣了,因為生產的只是一個個的積木(可以理解為類),至於怎麼去組裝它,則是玩家和使用者來構建了(用哪些類交給產品經理或其他人去決定)。

相關文章