淺談迴圈依賴

漫游云巅發表於2024-03-29

說明

  • 迴圈依賴是一個大家討論很多的話題,它更多是一個工程上的問題而不是技術問題,我們需要首先有一定的認知:

    • 如同兩個人相互幫忙,兩個類之間你呼叫我的,我呼叫你的是很正常也很自然的需求模型。
    • 單一依賴確實有好處,改動一個最頂層類時不需要在意對底部類的影響,但是從本來就自然的模型非要理順的話就需要額外付出代價,例如額外的拆分類。
  • 迴圈依賴可以分這幾種:

    • 從小的來說是類之間的相互引用。
    • 再大一點的來說是同一個專案下不同模組之間的引用。
    • 再再大一點的來說涉及到微服務或不同類庫之間引用。

    對於微服務級別或是模組級別的引用來說解決迴圈依賴是有必要的,因為這可能牽扯到不同人分工協作的問題,而類之間尤其是同一模組下的類之間是否禁止迴圈依賴實際上是有爭議的,本文只討論同一模組下的類之間的迴圈引用。

  • 一些解決迴圈依賴的方法類似@LazygetBean等實際上解決的不是迴圈依賴,而是解決的springboot啟動時的迴圈依賴檢測,但本質上它們還是相互引用,所以這裡不討論這些方法,只討論拆分類的方法。

  • 個人目前比較認同的是同一個人寫的同一個模組下的功能是可以迴圈依賴的,增加額外的拆分類反而會增加複雜度及影響效率,但springboot 2.6版本之後預設禁止了迴圈依賴,所以個人也在思考,如果想要拆分的化要怎麼拆分,目前總結了如下兩種不同型別的迴圈引用拆分示例。

示例

情形一
  • 最常見的老師學生這種或是主表子表相互關聯的:

    @Component
    public class Teacher {
        @Autowired
        private Student student;
        
        public void method() {
            //獲取某教師下學生類別
            List<String> students = student.getStudentsByTeacher("xxx");
            System.out.println(students);
        }
    }
    
    @Component
    public class Student {
        @Autowired
        private Teacher teacher;
        
        public void method() {
            //獲取學生歸屬的教師
            String teacherStr = teacher.getTeacherByStudent("xxx");
            System.out.println(teacherStr);
        }
    }
    
  • 這種拆分比較簡單,類似資料庫多對多的中間表,我們也建立一箇中間類,然後TeacherStudent類不要依賴彼此,直接抽取方法到中間類中或是都引用中間類:

    @Component
    public class TeacherStudent {
        @Autowired
        private Teacher teacher;
        @Autowired
        private Student student;
        
         public void method1() {
            //獲取某教師下學生類別
            List<String> students = student.getStudentsByTeacher("xxx");
            System.out.println(students);
        }
        
        public void method2() {
            //獲取學生歸屬的教師
            String teacherStr = teacher.getTeacherByStudent("xxx");
            System.out.println(teacherStr);
        }
    }
    
情形二
  • 另一種常用的場景是引用第三方類庫A,然後在配置類B中用@Bean來例項化,而類A是透過讀取資料庫中的配置(透過類C)來組裝引數,而當資料庫配置變更時(類C中更新),由於引數變化同時也要重置類A例項,我們在整合微信、釘釘等SDK時會經常遇到此情況,如果直接按照此邏輯寫的話,就是下述的程式碼:

    //B本身是個配置類
    @Configuration
    class B { 
        @Autowired
        private C c;
    
        @Bean 
        public A init(){
            A a = new A();
            //引用c的資料庫中資料來組裝成A例項
            a.setProp(c.getProp());
            return a;
        }
    }
    
    @Component
    class C {
        @Autowired
        private A a;
    
        public void update(){
            //修改資料庫相關後,又來重置A例項
            a.reset();
        }
    }
    
  • 此情況下最主要的耦合就是類A需要類C的資料來作為配置項,所以把這個耦合獨立出來,而類B中去除類C的引用,僅僅是生成類Abean

    //用@PostConstruct
    @Component
    public class D {
        @Autowired
        private A a;
        @Autowired
        private C c;
        
        @PostConstruct
        public void init(){
            a.setProp(c.getProp());
        }
    }
    //或是@Autowired註解到方法上
    @Component
    public class D {
        @Autowired
        public void init(A a, C c) {
            a.setProp(c.getProp());
        }
    }
    
  • 雖然從需求上類A依賴類C,但本質上類A並不需要依賴任何類,和第一種情況不同的是類A是一個第三方的類庫,我們無法修改其引用及方法,而其本身並不是個bean,需要我們額外去操作。

結果

  • 上述的情況都是額外增加一個拆分類來處理,這樣無形中增加了程式碼量,尤其是第一種情形太常見了,除非是專案初始時就規定好禁止service層互相呼叫,而是單獨再劃分一層來處理(類似阿里的manager層),否則的話個人寧願用@Lazy來解決掉迴圈依賴的報錯。
  • 解決迴圈依賴上述同模組內的相對簡單些,只是增加程式碼量而已,當涉及到模組或微服務時,則完全不一樣,要考慮業務邏輯及架構等一系列問題,感覺很是麻煩。
  • 以上只是個人見解,有更好的觀點可發到評論區一起討論下。

相關文章