1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        復(fù)雜系統(tǒng)設(shè)計原則與案例

        共 9957字,需瀏覽 20分鐘

         ·

        2023-08-23 05:47

        一、復(fù)雜是軟件的本質(zhì)屬性

        1.1 復(fù)雜是軟件的本質(zhì)屬性

              互聯(lián)網(wǎng)經(jīng)歷了十多年的高速發(fā)展,各個領(lǐng)域方向的系統(tǒng)都已經(jīng)歷了多次升級迭代,大家在經(jīng)手這些軟件系統(tǒng)時,不免感嘆現(xiàn)在軟件系統(tǒng)的復(fù)雜度,其實件復(fù)雜性是軟件固有的屬性,這種固有的復(fù)雜性主要由4個方面的原因造成的:

        • 問題域的復(fù)雜性

        • 管理開發(fā)過程的復(fù)雜性

        • 隨處可變的靈活性

        • 描繪離散系統(tǒng)行為的問題

               上面每一個方面都有極大的挑戰(zhàn),以「問題域的復(fù)雜性」為例,現(xiàn)以微服務(wù)架構(gòu)設(shè)計思路下的大型系統(tǒng)中,動不動就幾十個應(yīng)用,組合在一起就是一個復(fù)雜的系統(tǒng),而每個人只負責其中一小部分,想要了解系統(tǒng)全部的運行狀況是很難的,哪怕一個子系統(tǒng),它包含的業(yè)務(wù)規(guī)則就巨多,因此說軟件復(fù)雜是它的本質(zhì)屬性。

        1.2 對業(yè)務(wù)認知復(fù)雜度是影響軟件復(fù)雜性的重要因素

                影響軟件復(fù)雜度的因素有很多,其中「認知復(fù)雜度」占據(jù)著很重要的因素。一提到復(fù)雜性,我們腦海里會浮出各種各樣的印象:應(yīng)用數(shù)多、代碼行數(shù)超過百萬級、業(yè)務(wù)規(guī)則復(fù)雜等,這些復(fù)雜度從本質(zhì)上來看是認知復(fù)雜度超過了正常人的認知范圍,比如看百萬行級的代碼與看100行代碼相比,維護10個應(yīng)用與維護1個應(yīng)用相比,兩個復(fù)雜度不是在同一個數(shù)量級上,有可能是指數(shù)級提升。認知復(fù)雜度是軟件的本質(zhì)復(fù)雜度,從根本上規(guī)避不了,只能去理解、消化吸收,我們能做的是在理解的基礎(chǔ)上去發(fā)現(xiàn)共性的「規(guī)律」,將這些「規(guī)律」抽象出來,讓應(yīng)用層開發(fā)變得簡單。

               舉當前的例子,目前負責的是電商板塊的物流資金結(jié)算業(yè)務(wù)系統(tǒng),最開始面對的業(yè)務(wù)認知復(fù)雜度非常高,它關(guān)聯(lián)電商交易、支付、營銷、結(jié)算、資金等領(lǐng)域,依賴業(yè)務(wù)將近100張離線表,除了要理解電商業(yè)務(wù)鏈路外,還要站在物流,財務(wù),風控等視角把這些數(shù)據(jù)有序地組織起來,復(fù)雜度一下子就上升上來了,新人至少要花3個月的時間去消化這些業(yè)務(wù)知識。 當進來做了一些需求開發(fā)后,慢慢發(fā)現(xiàn)了一些規(guī)律,利用發(fā)現(xiàn)的這些規(guī)律有助于提升需求溝通、開發(fā)的效率。

        二、應(yīng)對復(fù)雜性的設(shè)計方法

        2.1 把握套路是應(yīng)對復(fù)雜性的根本方法

              「規(guī)律」是日常開發(fā)中發(fā)現(xiàn)有共性的地方,往后再遇到可以同樣的問題可加速解決的效率。軟件復(fù)雜度伴隨著軟件研發(fā)開始就產(chǎn)生的問題,「設(shè)計原則」就是應(yīng)對復(fù)雜性過程中總結(jié)出來的規(guī)律。常見的設(shè)計原則有SOLID、GRASP、KISS、分層等,這些設(shè)計原則指導(dǎo)我們在面對復(fù)雜系統(tǒng)時應(yīng)該如何去設(shè)計。原則的東西,個人經(jīng)驗是建立自己的認知體系,需要有實事求是,學以致用的實踐態(tài)度。

        6d5190a5a53d35afd69f2ced17bb2265.webp

        2.2 通識規(guī)律

               在經(jīng)典的設(shè)計原則之上,最終將設(shè)計原則歸類成三個方面:「職責分解」、「層次抽象」和「變化擴展」。

        2.2.1 職責分離

                 對職責分離有兩點體會:一個是「你擁有什么信息就應(yīng)該承擔怎樣的職責」;另一個是「一個類只做一件事」。當我們在討論是否是貧血模型時,你可以用這個原則去檢驗,如果一類中的成員屬性操作放在另外一類中,大概率是不符合信息專家原則,舉一個簡單的例子,比如要計算物流訂單的運費金額,那么這個計算方法應(yīng)該是在訂單類中,而不是放在另外一個類中,因為訂單類中有訂單的單價和數(shù)量。

                另一點是出自于SOLID的單一職責,它的原意是一個類只有一個變化的原因,一個類專注于做一件事的好處是可提升復(fù)用性和減少依賴,反之一個類耦合了不同的操作,修改的頻次就會變多,盡量少改動穩(wěn)定的部分,在系統(tǒng)穩(wěn)定性中有一個共性認知:故障的發(fā)生大概率與最近的發(fā)布有關(guān)。

              職責分解最大的挑戰(zhàn)是一個職責到底要劃分到多細或多粗,只能說只做一件事或者只有一個變化這樣大的指導(dǎo)原則,更多是我們在實踐中總結(jié)出來的經(jīng)驗,比如「變與不變分離」、「讀寫分離」、「配置域與執(zhí)行域分離」。

        2.2.2 層次抽象

                 層次抽象是利用已發(fā)現(xiàn)的規(guī)律,讓往后的開發(fā)變得簡單,當我們在一線開發(fā)中,你會發(fā)現(xiàn)有一些規(guī)律,比如在日常開發(fā)中,發(fā)現(xiàn)開發(fā)主要涉及到與前端交互、業(yè)務(wù)邏輯處理和數(shù)據(jù)存儲,這樣就可以分成三層:「視圖層」、「業(yè)務(wù)邏輯層」和「數(shù)據(jù)訪問層」。

               高層次依賴低層次,最高層次越具象,也會越簡單,舉一個例子,在傳統(tǒng)Servlet開發(fā)中,一般的步驟是獲取參數(shù)信息并轉(zhuǎn)成業(yè)務(wù)層的對象,再進行業(yè)務(wù)處理,雖然不同的業(yè)務(wù)處理邏輯是不一樣的,但參數(shù)獲取是具有共性的操作,在SpringMVC中,我們可以直接定義POJO去映射參數(shù),可以不用使用HttpServlet底層的操作去獲取參數(shù),這就是一種典型的層次抽象。

            「層次特性」是復(fù)雜系統(tǒng)的固有屬性,需要我們不斷去探索,分層的確能極大地降低認知復(fù)雜度,相當是站在巨人的肩膀上看問題,利用已發(fā)現(xiàn)的規(guī)律辦事效率會高很多,如上文提到的財務(wù)核算,做多了就會發(fā)現(xiàn)就那幾種模式,當你沒有摸清里面的規(guī)律時,會覺得顯得很零散。

        2.2.3 變化擴展

                軟件如果沒有變化,也就不需要所謂的設(shè)計原則,一次性工程怎么快就怎么來,而現(xiàn)實中遇到最多的現(xiàn)象是需求不斷變化。變化擴展的挑戰(zhàn)不在于技術(shù),而是在于「怎么認知到哪里有變化」。常見變化擴展的技術(shù)有:配置項、接口、抽象類、攔截器、SPI、插件等,這些都是具體的解決手段,它們并不復(fù)雜,復(fù)雜在于哪里會有變化,這個是最難的。

               認識到多少變化,它取決于認識的寬度,看到多少內(nèi)容會影響到系統(tǒng)設(shè)計,比如在SpringMVC中,我們最高常操作的是定義一個Controller,再在方法上寫一個RequestMapping注解,但在實際中,它還有另外的寫法,如實現(xiàn)Controller接口,正是有不同的場景和類型,處理上還有差別,此時就會有變化擴展的訴求。

        2.3 軟件設(shè)計的6條經(jīng)驗

                在經(jīng)典的設(shè)計原則之上,結(jié)合實踐過程中的得與失,總結(jié)了以下6條設(shè)計經(jīng)驗,為了更容易理解,下面的案例選用常用的開源框架剖析設(shè)計思想,方便與大家產(chǎn)生共鳴。

        2.3.1 模板方法-在多變中找不變

                當一個業(yè)務(wù)有多個場景,并且不同的場景處理既有共性的地方,也有差異性的地方時,此時最容易想到的方法是用「模板方法」固定共性的邏輯,差異性的邏輯放到子類中實現(xiàn)。在開源框架中,我們經(jīng)常見到這樣的設(shè)計思想,比如在SpringMVC中查找Handler的過程,不同的場景查找邏輯不一樣,最常見的是RequestMapping方式查找,它是在HandMapping接口類中定義getHandler方法。

            
              public interface HandlerMapping {

        HandlerExecutionChain getHandler(HttpServletRequest request)

                throws Exception;

        }

              然后在抽象類AbstractHandlerMapping中定義模板方法,抽象方法又交由子類去實現(xiàn)。

            

        public final HandlerExecutionChain getHandler(HttpServletRequest request)

        throws Exception {

        // 抽象方法,交由具體的子類實現(xiàn)
        Object handler = getHandlerInternal(request);
        if (handler == null) {
        handler = getDefaultHandler();
        }
        if (handler == null) {
        return null;
        }

        // 省略部分代碼
        HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);

        // 省略部分代碼
        return executionChain;
        }

                在MyBatis框架中,Executor定義了增刪改查等方法,具體實現(xiàn)有如單條命令執(zhí)行、批量命令執(zhí)行等,模板方法定義在BaseExecutor類中,類結(jié)構(gòu)繼承關(guān)系如下所示,這也是一種最簡單的三層設(shè)計結(jié)構(gòu):接口類、抽象類、子類。

        9f239655990802ab933a5cbe96d9081f.webp

        2.3.2 命令職責-業(yè)務(wù)鏈路查詢和復(fù)雜組裝

                有一類業(yè)務(wù),它涉及「查詢」與「組裝」兩個操作,比如Spring中有Bean查詢操作,與之對應(yīng)的有Bean創(chuàng)建操作,這兩個職責是不一樣的,也有的稱之為「讀寫分離」或者「查詢與命令分離」,從本質(zhì)上講,它也遵循了接口單一職責。

        4dfe8420cd8fd8100416c3acf162c671.webp

        2.3.3 配置域與執(zhí)行域分離-有面向用戶配置

               有些業(yè)務(wù)前臺用戶能夠直接配置操作的,比如在SpringMVC中,我們配置一個Controller的請求可以配置不同的屬性,其中RequestMapping是直接面向用戶視角的配置操作,在配置域的內(nèi)容,是與現(xiàn)實操作一一映射的,RequestMapping對應(yīng)有一個類叫RequestMappingInfo,然而在執(zhí)行域,此時它就不需要配置域中的那么多信息,執(zhí)行過程只要對象和方法的信息即可,對應(yīng)有一個類中HandlerMethod,由此可見,配置域和執(zhí)行域兩個抽象的視角是不一樣的,一個是現(xiàn)實世界的直接映射,一個是偏底層執(zhí)行。

            @RestController
        public class UserController {

        @RequestMapping(value = "/acquire", method = RequestMethod.GET)
        public User getUser(@RequestParam("name") String name, @RequestParam("age") Integer age) {

        return null;
        }
        }

        RequestMappingHandlerMapping類結(jié)構(gòu)繼承關(guān)系如下圖所示。

        c2bfa793a496dea38fbadee375136e8e.webp

                再比如在Spring中,允許用戶配置自定義的編輯器、BeanPostProcessor處理器,也是由一個單獨的接口類ConfigurableBeanFactory表達的。a01fd40d490cf917d7a001e49de50a4e.webp

               這樣的例子還有很多,比如BeanDefinition是面向配置域的,Bean是執(zhí)行域的,我們在定義Bean有很多的屬性,這些屬性信息在BeanDefinition類中定義,而在執(zhí)行過程中會生成一個對象,本質(zhì)上是一個Object。

        2.3.4 封裝變化-業(yè)務(wù)有多樣變化

              應(yīng)對變化的方法有很多,難的是要感知到變化并且封裝好變化,比如Spring Bean實例化后進行初始化,在此期間就有很多操作,如常見的Bean依賴注入、AOP代理等,Spring抽象出BeanPostProcessor擴展類,在Bean初始化前后做一些額外的擴展工作。

            public interface BeanPostProcessor {
        // 初始化前的操作
        Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
        }

        // 初始化后的操作
        Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
        }
        }

               設(shè)計擴展點時一定要把握好度,粒度過細則擴展點數(shù)量非常多,在Spring中設(shè)計就比較好,對于開發(fā)而言,有兩個時機有明顯的擴展訴求,一個是在Bean掃描時,可以允許用戶自定義Bean,此時有BeanFactoryPostProcessor擴展接口;另一個是在Bean初始化時的擴展,對應(yīng)有BeanPostProcessor擴展接口。不管是Spring內(nèi)部使用,還是外部開發(fā),都是使用同樣的擴展。

        2.3.5 責任鏈-業(yè)務(wù)流程型操作

                業(yè)務(wù)型操作,有明顯的流程痕跡,比如前置檢查、協(xié)議組裝、接口調(diào)用等,節(jié)點與節(jié)點之間就構(gòu)成了一條鏈條,只不過平時寫代碼時我們是放在一個大的流程中實現(xiàn)的。在HttpClient中,對于請求,我們有不同的操作流程,比如重試、緩存、重定向、調(diào)用socket等操作,HttpClient使用責任鏈的模式。

        8cf25c1840c7a8c309344ac205cd459f.webp

                鏈條上的每個節(jié)點都是獨立操作的,方便擴展,責任鏈核心是鏈的構(gòu)建和節(jié)點設(shè)計,這給平時寫流程型業(yè)務(wù)代碼提供了一種新的思路,大型系統(tǒng)中,有流程引擎,本質(zhì)來講它也是一條鏈,一個節(jié)點做完之后下一個節(jié)點繼續(xù)做,思想上大同小異。

        2.3.6 合理抽象-復(fù)雜系統(tǒng)場景

               抽象是應(yīng)對復(fù)雜場景的重要方法,這一點我們并不懷疑,最難的是要抽象什么去刻畫業(yè)務(wù),比如AOP切面編程,站在用戶視角,就是告訴他哪些類、哪些方法需要被增強什么共性業(yè)務(wù)邏輯,比如日志切面類、權(quán)限切面類等,AOP對它的抽象是「對指定的類和方法以某種方式織入特定的共性邏輯」。其中指定的類和方法抽象成切點,以某種方式抽象成通知。此時,你會發(fā)現(xiàn)它抽象出了一些概念出來,如切面、切點、通知。因此,對復(fù)雜業(yè)務(wù)場景,一定要有一套抽象的元數(shù)據(jù)去表征它,也即是領(lǐng)域模型,最高明的建模方法是下定義的方法,用一句簡明的話講清楚業(yè)務(wù)的結(jié)構(gòu)和功能。

        67ac2fa97f22d9b3f6ae5f106b0f78b6.webp

                系統(tǒng)是元素和元素間以某種關(guān)聯(lián)關(guān)系構(gòu)成的一種結(jié)構(gòu),復(fù)雜系統(tǒng)是構(gòu)成元素更多、關(guān)聯(lián)關(guān)系更復(fù)雜,核心還是要找到「結(jié)構(gòu)」,這種結(jié)構(gòu)也即是領(lǐng)域模型,好的領(lǐng)域模型可遇而不可求,是要花大量的時間去探尋它,突然有一天在你腦海里靈光一現(xiàn)就出來了,這種感覺很奇妙,因此,領(lǐng)域建模是非常依賴經(jīng)驗而非方法。

        三、框架設(shè)計案例分析

                有了上面的分析基礎(chǔ),再以SpringMVC DispatcherServlet為例,分析它的設(shè)計思想,它的結(jié)構(gòu)如下圖所示。

        292939a6b520f943f2effa281f5bd63f.webp

                SpringMVC核心是對HttpServlet的封裝,在HttpServlet中有兩個重要的方法,一個是init()方法,一個是service()方法,init()方法是Servlet初始化時回調(diào)的方法,service()是處理請求時回調(diào)的方法。

                在HttpServletBean類中,它重寫了HttpServlet init()方法,主要完成SpringMVC子容器初始化的過程。FrameworkServlet類主要重寫了service()方法,處理實際的如GET、POST請求,但它只是定義了一個抽象的doService()方法,實際處理過程是在DispatcherServlet類中,分發(fā)的Servlet是攔截所有的請求,然后匹配到目標Handler執(zhí)行。

               在DispatcherServlet類的設(shè)計中,體現(xiàn)出了「職責分離」和「變化擴展」的設(shè)計思想,init初始化與service執(zhí)行分離,攔截器支持變化擴展。上面列舉的幾個框架,它們都是解決了一些平常的問題,但不影響它們優(yōu)秀的設(shè)計,如MyBatis、Spring、SpringMVC、HttpClient,它們并沒有在一個大類中實現(xiàn)各種各樣的功能,而是切分放在不同的類中,并且通過多層繼承關(guān)系組合在一起,不管是可讀性上,還是可擴展性上都非常不錯。

        四、認知是解決復(fù)雜性的基石

                在認知面前,所有的方法和工具都是蒼白的,就像一個人想不勞而獲一樣,總想找一種萬能的方法解決所有的問題,而事實并沒有,還得靠在實踐中解決問題。復(fù)雜性也是同樣的問題,沒有萬能的方法解決它,只有原則作為指導(dǎo),而具體要怎么去做,還是得身體力行。當我們不理解框架為什么要設(shè)計得這么復(fù)雜時,大概率是我們對應(yīng)用的場景了解還不夠全面。

        4.1 業(yè)務(wù)認知

                當大家第一次去看Spring Bean掃描的邏輯時,它的邏輯是很復(fù)雜的,如果讓我們自己去實現(xiàn)一個,你可能會很簡單的設(shè)計出來,根據(jù)指定的路徑掃描所有的類,如果有@Component的注解時就存放到BeanDefinnitionMap中,那為什么Spring要設(shè)計得這么復(fù)雜呢,原因是現(xiàn)實場景中Bean定義有多種方法,比如嵌套定義Bean,再比如先掃描出一部分Bean,此時這些Bean中有定義@CompentScan,又可以加載其它的Bean,所以你看這么多你不曾考慮的場景疊加在一起,實現(xiàn)起來的復(fù)雜度自然就高了。

                還比如SpringMVC在查找Handler時,它的邏輯也挺復(fù)雜的,與我們?nèi)粘Mㄟ^一個URL映射到一個Handler不一樣,在現(xiàn)實中完全有一種可能是相同的URL對應(yīng)不同的請求方法,此時就不是一個簡單映射的就能完成,還有一大堆的匹配邏輯,所以你會看到,當我們的業(yè)務(wù)認知了解得越來越多時,在設(shè)計中就會考慮更多的因素。

                提升業(yè)務(wù)認知,除了溝通交流外,還得踏踏實實去工作一段時間,真正地了解里面的問題是什么,即使是踩坑,也是修正自己的認知。

        4.2 技術(shù)認知

                 除了業(yè)務(wù)認知外,技術(shù)也是在不斷發(fā)展的,如果你不了解某個技術(shù)或技術(shù)點,此時你也不會想到好的設(shè)計方法。比如讓你設(shè)計一個事件通知框架,本來這個功能倒不是那么復(fù)雜,它最難的點是在于如何找到事件對應(yīng)的事件處理器,此時就有不同的解決方案,一種最簡單的方法是在定義事件處理器時讓用戶指定事件類型,這似乎是一種解決方案,但站在用戶使用的角度看,它并不是一種好的解決方案,把復(fù)雜留給用戶而不是自己。為了提升用戶使用體驗,這里就要使用到泛型類型解析的方面的知識了,核心代碼如下:

            /**
        * 事件分發(fā)器
        *
        * @author fulai.gfl
        */
        public class EventDispatcher {

        /**
        * 事件列表
        */
        private static List<Event> events = new ArrayList<>();

        /**
        * 事件處理器列表
        */
        private static List<Handler> handlers = new ArrayList<>();

        /**
        * 添加事件
        */
        public static void addEvent(Event event) {
        events.add(event);
        }

        /**
        * 添加事件處理器
        */
        public static void addHandler(Handler handler) {
        handlers.add(handler);
        }

        /**
        * 觸發(fā)事件
        */
        public Object fire(Event event) throws Exception {

        Handler handler = getHandler(event);
        if(Objects.isNull(handler)){
        throw new Exception("event_name =" + event.getEventName());
        }

        return handler.handle(event);
        }

        /**
        * 根據(jù)事件找到對應(yīng)的Handler
        */
        private Handler getHandler(Event event) throws Exception {

        Handler handler = null;
        for (Handler h : handlers) {

        Type[] argumentsTypes =

                    ((ParameterizedTypeImpl)h.getClass()

                        .getGenericInterfaces()[0])

        .getActualTypeArguments();

        if (Class.forName(((Class)argumentsTypes[0])

                        .getName()).equals(event.getClass())) {

        handler = h;
        }

        }

        return handler;
        }
        }

        五、小結(jié)

                本文主要講述了應(yīng)對復(fù)雜性的一些原則和經(jīng)驗,通過實際案例解構(gòu)設(shè)計思想,個人認為好的設(shè)計是體現(xiàn)在「職責分離」、「抽象分層」和「變化擴展」上,在類的結(jié)構(gòu)設(shè)計上尤其要花心思去想,如「變與不變分離」、「配置域與執(zhí)行域分離」、「查詢與命令分離」。歸根到底,認知是解決復(fù)雜性的基石,如果要更好地發(fā)揮技術(shù)的作用,對業(yè)務(wù)的理解需要更好的認識。






        瀏覽 38
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            国产熟女一区二区三区十视频 | 91精品高潮| 无码一区二区三区四区五区 | chinese粉嫩露出vide | 中文字幕无码乱伦 | 我要看一级毛片 | 我被继夫添我阳道舒服口述视频 | 成人黄色大片 | 欧美精品一区二区三区中文字幕 | 三级小说|