憋了很久,終於弄懂什么是IOC(控制反轉)


導航

  • 共享充電寶
  • IOC思想
    • 復雜的依賴關系
    • IOC定義
  • Spring中的IOC
    • IOC與工廠模式
    • IOC容器的技術剖析
  • 結語
  • 參考

  本節是摘自《Spring Boot 實戰紀實》的第13篇,感謝您的閱讀,預計閱讀時長3min。

將對象自身從其依賴關系的管理中解放出來,將這個管理工作的責任交給第三方來完成。

共享充電寶



尷尬往事

手機早已成為我們生活中不可或缺的一部分,但是伴隨而來的便是手機的充電問題。

大概在2011年,筆者和同學買好了回學校的火車票。因為是晚上6點半的火車票,所以筆者就想時間還早,正好自己也要去商場買點東西,便和同學約定晚上六點在火車站候車室會合,並將車票交給了同學。

下午五點半的時候,筆者便早早地出發前往火車站,大概二十分鍾左右便到了車站。到站之后正好趕上排隊檢票(一般都是提前一個小時檢票),但是尷尬地一幕發生了——手機沒電了。眼看就要輪到我檢票了,可車票還在我同學那里,我同學已經進站,在二樓候車室。我禮貌地請檢票員想跳過我,先去檢查后面的票,同時也在想辦法聯系我的同學。

我也想起來借一個電話或者打公用電話,但無奈沒有記住同學的手機號碼...眼看着排隊檢票的隊伍都進站了,檢查通道也開始准備關閉了。

這時多么希望自己帶了充電線或者充電寶啊...

就在這時,我那同學突然從樓上沖下來,把車票給我,化解了這場尷尬。

因為手機沒電且未帶手機充電線出現的糗事其實不止這一件,我想生活中很多人都有過這樣的經歷。

時隔多年,可能很多同學會覺得這個很荒唐,為啥不用共享充電寶呢。因為,那個時候沒有。

共享充電寶

盡管這樣的事情屢見不鮮,但是依然沒有引起手機廠商的重視(直到今天手機電池的續航能力依然是個問題),通常我們在出門前會做一些准備避免這種事情的發生:

  • 多帶一個手機
  • 換一個大容量電池
  • 帶上電話本(以備不時之需)
  • 帶上充電寶

但是以上幾種方式依然是成本較高的,所以通常手機沒電你大概率只能通過以下但是充電:

  • 找路人借充電寶
  • 在飯店吃飯時,讓店家幫忙充電
  • 去住酒店充電
    ...

另外,因為手機廠商不同,充電線接口不一致,你可能還需要再去買一根充電線...

而以上這些不僅你的增加時間和金錢成本,還會增加新溝通成本。

所以,共享充電寶應用而生,他為用戶提供了各種型號的充電線和電源,用戶只掃碼支付即可使用。

共享充電寶的模式就是把充電過程中的所有設備和過程打包成一個盒子(類似於容器),這一點和軟件架構的IOC思想不謀而合。

IOC思想

IOC(Inversion of Control) 控制反轉是一種面對對象編程的設計原則,用於降低代碼之間的耦合度。其基本思想是借助第三方實現具有依賴關系的對象之間的解耦。

對象之間復雜的依賴關系

在面向對象方法設計的軟件系統中,它的底層實現都是由N個對象組成的,所有的對象通過彼此的合作,最終實現系統的業務邏輯。

Note: 關於面向對象請查看《類和實例通俗理解》



上圖中的齒輪組,它擁有多個獨立的齒輪,這些齒輪相互嚙合在一起,協同工作,共同完成某項任務。我們可以看到,在這樣的齒輪組中,如果有一個齒輪出了問題,就可能會影響到整個齒輪組的正常運轉。

齒輪組中齒輪之間的嚙合關系,與軟件系統中對象之間的耦合關系非常相似。對象之間的耦合關系是無法避免的,也是必要的,這是協同工作的基礎。

但是隨着軟件系統的規模越來越龐大,對象之間的依賴關系也越來越復雜,經常會出現對象之間的多重依賴性關系。



為了解決對象之間的耦合度過高的問題,軟件專家提出了IOC理論,用來實現對象之間的“解耦”,目前這個理論已經被成功地應用到實踐當中。

IOC的定義

控制反轉(Inversion of Control,縮寫為IoC),是面向對象編程中的一種設計原則,可以用來減低計算機代碼之間的耦合度。(百度百科)

既然名字叫做控制反轉,我們來看看,控制什么,反轉什么。

早在2004年,Martin Fowler就提出了“哪些方面的控制被反轉了?”這個問題。他總結出是依賴對象的獲得被反轉了,因為大多數應用程序都是由兩個或是更多的類通過彼此的合作來實現企業邏輯,這使得每個對象都需要獲取與其合作的對象(也就是它所依賴的對象)的引用。如果這個獲取過程要靠自身實現,那么這將導致代碼高度耦合並且難以維護和調試

  • 控制什么:控制對象的創建和銷毀,指的是控制對象的生命周期。

  • 反轉什么:之前我們創建一個對象都是new,現在有了IOC了,指的是把對象的控制權交給了IOC容器。

IOC借助於“第三方”實現具有依賴關系的對象之間的解耦,如下圖:



由於引進了中間位置的“第三方”,也就是IOC容器,使得A、B、C、D這4個對象沒有了耦合關系,齒輪之間的傳動全部依靠“第三方”了,全部對象的控制權全部上繳給“第三方”IOC容器,所以,IOC容器成了整個系統的關鍵核心,它起到了一種類似“粘合劑”的作用,把系統中的所有對象粘合在一起發揮作用,如果沒有這個“粘合劑”,對象與對象之間會彼此失去聯系,這就是有人把IOC容器比喻成“粘合劑”的由來。

為了更加直觀的理解,我們可以把IOC拿掉,這時候,A、B、C、D這4個對象之間已經沒有了耦合關系,彼此毫無聯系,這樣的話,當你在實現A的時候,根本無須再去考慮B、C和D了,對象之間的依賴關系已經降低到了最低程度。



最后,我們用一張圖把IOC引入的過程串起來。



Note: IoC可以認為是一種全新的設計模式,但是理論和時間成熟相對較晚,並沒有包含在GoF中。詳細查看百度百科-控制反轉

Spring中的IOC

IOC與工廠模式

IOC的實現主要用到了3種技術:工廠模式、XML解析、反射。

工廠模式在Java/C#中開發中應用廣泛。

在工廠模式中,我們不會將對象創建邏輯暴露給客戶端,使用一個通用的接口引用新創建的對象。



工廠模式的實現比較簡單

  • 客戶端(client)需要一個product對象,無須通過new關鍵字直接創建,而是向工廠(factory)發起一個獲取新對象請求。這個過程中,客戶端(client)只需要提供自己需要的對象的類型相關信息即可。
  • 工廠(factory) 實例化一個具體的product對象,然后返回給到客戶端(client)新的product對象(轉換為抽象類類型)。
  • 客戶端使用product對象而不用了解具體的實現細節。

按照慣例,這里還是給個簡單demo

步驟1

創建一個接口 Shape.java

public interface Shape {
   void draw();
}

步驟2

創建實現相同接口的具體類。如下所示幾個類

Rectangle.java

public class Rectangle implements Shape {

   @Override
   public void draw() {
      System.out.println("Inside Rectangle::draw() method.");
   }
}

Square.java

public class Square implements Shape {

   @Override
   public void draw() {
      System.out.println("Inside Square::draw() method.");
   }
}

Circle.java

public class Circle implements Shape {

   @Override
   public void draw() {
      System.out.println("Inside Circle::draw() method.");
   }
}

步驟3

創建工廠根據給定的信息生成具體類的對象 ShapeFactory.java

public class ShapeFactory {

   //use getShape method to get object of type shape 
   public Shape getShape(String shapeType){
      if(shapeType == null){
         return null;
      }        
      if(shapeType.equalsIgnoreCase("CIRCLE")){
         return new Circle();

      } else if(shapeType.equalsIgnoreCase("RECTANGLE")){
         return new Rectangle();

      } else if(shapeType.equalsIgnoreCase("SQUARE")){
         return new Square();
      }

      return null;
   }
}

步驟4

使用工廠通過傳遞類型等信息來獲取具體類的對象。
FactoryPatternDemo.java

public class FactoryPatternDemo {

   public static void main(String[] args) {
      ShapeFactory shapeFactory = new ShapeFactory();

      //get an object of Circle and call its draw method.
      Shape shape1 = shapeFactory.getShape("CIRCLE");

      //call draw method of Circle
      shape1.draw();

      //get an object of Rectangle and call its draw method.
      Shape shape2 = shapeFactory.getShape("RECTANGLE");

      //call draw method of Rectangle
      shape2.draw();

      //get an object of Square and call its draw method.
      Shape shape3 = shapeFactory.getShape("SQUARE");

      //call draw method of circle
      shape3.draw();
   }
}

引入工廠模式的優勢很明顯: 增加新的shape(如 triangle 三角形),我們也不用修改現有的架構,而只需要在ShapeFactory中通過(if else/switch)進行擴展。

上面這種方式工廠實現的方式原理是根據傳入的某個參數獲取一個對象,一旦我們新增一個shape類型,就修改ShapeFactory 類。這種方式不夠靈活,並違背了軟件設計的開閉原則。

利用反射,每當新增接口子類,無需去修改工廠類代碼就可以很方便的進行接口子類擴容。

Note: Java的反射(reflection)機制是指在程序的運行狀態中,可以構造任意一個類的對象,可以了解任意一個對象所屬的類,可以了解任意一個類的成員變量和方法,可以調用任意一個對象的屬性和方法。這種動態獲取程序信息以及動態調用對象的功能稱為Java語言的反射機制。(百度百科-JAVA反射機制)

我們只需要對ShapeFactory進行改造,如下:

public class ShapeFactory {

    private ShapeFactory(){}
    public static Shape getInstance(String className){
        Shape shape = null;
        try {
            shape = (Shape) Class.forName(className).newInstance();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
        return shape;
    }
}

這里我們將類名作為參數傳遞給工廠,工廠利用反射機制找到對應的對象,並創建實例。

什么?? 你說沒有看到反射的影子。那就進到Class.forName去看看吧。

@CallerSensitive
public static Class<?> forName(String className)
                throws ClassNotFoundException {
     Class<?> caller = Reflection.getCallerClass();
     return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

然后,我們再來個測試用例

@Test
	void testReflectFactory()
	{
		/**
		 * get circle instance
		 * */
		Shape shapeCircle = ShapeFactory
				.getInstance("com.zhike.blogmanager.Shape.Circle");
		shapeCircle.draw();

		/**
		 * get rectangle instance
		 * */
		Shape shapeRectangle = ShapeFactory
				.getInstance("com.zhike.blogmanager.Shape.Rectangle");
		shapeRectangle.draw();

		/**
		 * get square instance
		 * */
		Shape shapeSquare = ShapeFactory
				.getInstance("com.zhike.blogmanager.Shape.Square");
		shapeSquare.draw();
	}

看看執行結果

2021-10-04 22:41:50.514 === [main] INFO  com.zhike.blogwebapi.BlogWebapiApplicationTests - Started BlogWebapiApplicationTests in 6.359 seconds (JVM running for 8.133)
Inside Circle::draw() method.
Inside Rectangle::draw() method.
Inside Square::draw() method.

Process finished with exit code 0

從結果來看,進一步驗證了我們設想。

到了這里,有讀者就會問了。你講的工廠和IOC有啥關系呢?
還記得前面我提過:IOC的實現主要用到了3種技術:工廠模式、XML解析、反射

Spring IOC 技術剖析

IOC容器其實就是一個大工廠,它用來管理我們所有的對象以及依賴關系。

  • 原理就是通過 Java 的反射技術來實現的!通過反射我們可以獲取類的所有信息(成員變量、類名等等等)!

  • 再通過配置文件(xml)或者注解來描述類與類之間的關系

我們就可以通過這些配置信息和反射技術來構建出對應的對象和依賴關系了!

我們簡單來看看實際Spring IOC容器是怎么實現對象的創建和依賴的:



  • 根據Bean配置信息在容器內部創建Bean定義注冊表
  • 根據注冊表加載、實例化bean、建立Bean與Bean之間的依賴關系
  • 將這些准備就緒的Bean放到Map緩存池中,等待應用程序調用

(1) BeanFactory

Spring Bean 的創建是典型的工廠模式,這一系列的 Bean 工廠,也即 IOC 容器為開發者管理對象間的依賴關系提供了很多便利和基礎服務,在 Spring 中有許多的 IOC 容器的實現供用戶選擇和使用,
其相互關系如下:



BeanFactory 作為最頂層的一個接口類,它定義了IOC容器的基本功能規范。

最基本的IOC容器接口BeanFactory

public interface BeanFactory {

	/**
	 對 FactoryBean 的轉義定義,因為如果使用 bean 的名字檢索 FactoryBean 得到的對象是工廠生成的對象,
    如果需要得到工廠本身,需要轉義
	 */
	String FACTORY_BEAN_PREFIX = "&";


	/**
	 *根據 bean 的名字,在 IOC 容器中獲取 bean 實例
	 */
	Object getBean(String name) throws BeansException;

	/**
	 * 根據 bean 的名字和 Class 類型來得到 bean 實例,增加了類型安全驗證機制。
	 */
	<T> T getBean(String name, Class<T> requiredType) throws BeansException;

	/**
	 * 根據名字和參數 在IOC容器中獲取bean的實例
	 */
	Object getBean(String name, Object... args) throws BeansException;

	/**
	 * 根據名字和參數 在IOC容器中獲取bean的實例
	 */
	<T> T getBean(Class<T> requiredType) throws BeansException;

	<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;

	<T> ObjectProvider<T> getBeanProvider(Class<T> requiredType);

	<T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType);

	/**
	 *提供對 bean 的檢索,看看是否在 IOC 容器有這個名字的 bean
	 */
	boolean containsBean(String name);

	/**
	 *根據 bean 名字得到 bean 實例,並同時判斷這個 bean 是不是單例
	 */
	boolean isSingleton(String name) throws NoSuchBeanDefinitionException;

	boolean isPrototype(String name) throws NoSuchBeanDefinitionException;

	boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;

	boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException;

	/**
	 * 得到 bean 實例的 Class 類型
	 */
	@Nullable
	Class<?> getType(String name) throws NoSuchBeanDefinitionException;

	@Nullable
	Class<?> getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException;

	/**
	 *得到 bean 的別名,如果根據別名檢索,那么其原名也會被檢索出來
	 */
	String[] getAliases(String name);

}

在 BeanFactory 里只對 IOC 容器的基本行為作了定義,根本不關心你的 Bean 是如何定義怎樣加載的。
正如我們只關心工廠里得到什么的產品對象,至於工廠是怎么生產這些對象的,這個基本的接口不關心。

而要知道工廠是如何產生對象的,我們需要看具體的IOC容器實現,Spring 提供了許多 IOC 容器的實現。比如XmlBeanFactory,ClasspathXmlApplicationContext等。

(2) BeanDefinition

SpringIOC 容器管理了我們定義的各種 Bean 對象及其相互的關系,Bean 對象在 Spring 實現中是以 BeanDefinition來描述的,其繼承體系如下:



Spring IOC的實現過程比較復雜,相關的源碼可以研究一下。感興趣的同學可以下載源碼查閱spring-framework源碼

結語

IOC不是什么技術,而是一種設計思想。

在Spring 開發中,由IOC容器控制對象的創建、初始化、銷毀等。這也就實現了對象控制權的反轉,由我們對對象的控制轉變成了Spring IOC 對對象的控制。

以上只是筆者個人對Spring IOC的一點看法和思考,歡迎大家共同探討和文明交流。

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM