<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          淺析前端 DDD 框架 Remesh

          共 20478字,需瀏覽 41分鐘

           ·

          2023-03-11 10:50

          大廠技術(shù)  高級前端  Node進階

          點擊上方 程序員成長指北,關(guān)注公眾號

          回復1,加入高級Node交流群

          1. 什么是 DDD

          DDD(Domain-Driven Design):領(lǐng)域驅(qū)動設(shè)計。首先需要了解,所謂的「領(lǐng)域」,其實不僅僅在于程序表現(xiàn)形式,更適合說是對特定業(yè)務的描述,通常由該業(yè)務的垂直協(xié)作方共同確定,比如產(chǎn)品需求、系統(tǒng)架構(gòu)、程序代碼,由一群“專業(yè)的”人承接,這意味著其中的每一個人,可能都是該「領(lǐng)域」內(nèi)的專家,而「領(lǐng)域模型」成了他們之間的「通用語言」,或者說,「領(lǐng)域知識」讓彼此能夠坐在一起討論問題,再換句話說,產(chǎn)品也可以使用此通用語言來“組織代碼”。這也是 DDD 的戰(zhàn)略意義。

          MVC 與 DDD

          這里有必要談及一下后端傳統(tǒng)的 MVC 架構(gòu),通常會采用一種「貧血模型」,即將「行為」和「狀態(tài)」拆分至不同的對象中,也就是我們常說的 POJO 和 Service,這樣做的好處是,在開發(fā)業(yè)務代碼時,心智負擔較小,對于簡單業(yè)務效率很高。往往開發(fā)人員會形成一種慣性思維,接到需求時,先三下五除二,把實體和服務先定義好。可是我們仔細想想,這種方式其實違背了面向?qū)ο蟮谋举|(zhì),將一個對象的狀態(tài)和行為強行拆分,變成了面向過程開發(fā),Service 中的邏輯隨著業(yè)務復雜度提升,變得失控。當業(yè)務復雜度膨脹至一定程度,甚至會產(chǎn)生牽一發(fā)而動全身的風險。

          而 DDD 帶來的「充血模型」,完全規(guī)避了這個問題,POJO 和 Service 變成 Domain,可以理解為高內(nèi)聚(對于 Domian 的設(shè)計不是本文導論的重點),Domain 的 State 僅由其 Action 完成,禁止任何外部的直接修改,將業(yè)務風險收斂至領(lǐng)域內(nèi)部,一切將變得井然有序。

          DDD 優(yōu)勢

          • 業(yè)務層面:基于領(lǐng)域知識的通用語言,快速交付,極低的協(xié)作成本
          • 架構(gòu)層面:利于結(jié)構(gòu)布局,利于資源聚焦
          • 代碼層面:復雜度治理

          DDD 弊端

          主要在于戰(zhàn)術(shù)層面

          • 心智負擔大,對現(xiàn)有業(yè)務拆解成本大,對團隊要求較高
          • 缺少戰(zhàn)術(shù)意義上的最佳實踐,從頭造輪子難度較大
          • 簡單業(yè)務下,效率很低(缺少開箱即用的框架)

          對前端的思考

          DDD 近幾年在后端的落地頗有成效,社區(qū)也產(chǎn)出了較多的相關(guān)文章,如微軟的《Tackle Business Complexity in a Microservice with DDD and CQRS Patterns》(https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/) 。DDD 帶來的戰(zhàn)略意義,前端往往也能深受啟發(fā)。DDD 概念早在 2003 年就已提出,也是近幾年隨著技術(shù)和架構(gòu)的演進重新回到人們視野,并且發(fā)光發(fā)熱。那么,我該什么時候使用 DDD 呢?

          • 存在復雜的業(yè)務及領(lǐng)域概念(如:商品、訂單、履約等),代碼復雜度與業(yè)務復雜度成正比,好的領(lǐng)域模型可以極大地降低代碼復雜度,避免牽一發(fā)而動全身。
          • 想復用業(yè)務邏輯(如:多端實現(xiàn)),將邏輯抽離至領(lǐng)域?qū)樱梢詭椭鷺I(yè)務快速實現(xiàn)。

          2. DDD 示例

          通常一個商品有以下幾種場景:創(chuàng)建、編輯、上架、審核、撤回,其對應的狀態(tài)有,草稿、審核中、已上架,如果我們用傳統(tǒng)的邏輯去寫,往往是以下代碼:

          class Goods {
            private isDraft: boolean// 是否草稿
            private isAuditing: boolean// 是否審核狀態(tài)
            private isPublished: boolean// 是否已上架
            private info: any// 商品信息
            // 狀態(tài)初始化
            constructor(info: any) {
              this.isDraft = true;
              this.isAuditing = false;
              this.isPublished = false;
              this.info = info;
            }
            // 編輯操作
            edit(info: any) {
              if (!this.isDraft) {
                throw new Error('僅草稿狀態(tài)下可編輯');
              }
              this.info = info;
            }
            // 上架操作
            publish() {
              if (!this.isDraft) {
                throw new Error('僅草稿狀態(tài)下可上架');
              }
              this.isAuditing = true;
              this.isDraft = false;
            }
            // 審核操作
            audit(result: boolean) {
              if (!this.isAuditing) {
                throw new Error('僅在途狀態(tài)下可審核');
              }
              if (!result) {
                this.isDraft = true;
              }
              this.isPublished = result;
              this.isAuditing = false;
            }
            // 撤回操作
            revert() {
              this.isAuditing = false;
              this.isDraft = true;
            }
          }

          這種寫法只能說,無功無過,現(xiàn)實場景中,一個商品的狀態(tài)可能非常復雜,那這個類的邏輯代碼也將會變得十分龐大且復雜,那我們該如何使用 DDD 來對其進行邏輯抽離呢?且看以下寫法:

          // 草稿商品
          class DraftGoods {
            private info: any;
            constructor(info: any) {
              this.info = info;
            }
            // 可以編輯
            edit(info: any) {
              this.info = info;
            }
            // 可以上架
            publish() {
              return new AuditingGoods(this.info);
            }
          }
          // 審核中商品
          class AuditingGoods {
            private info: any;
            constructor(info: any) {
              this.info = info;
            }
            // 審核成功或失敗
            audit(result: boolean) {
              if (result) {
                return new PublishedGoods(this.info);
              } else {
                return new DraftGoods(this.info);
              }
            }
            // 可以撤回
            revert() {
              return new DraftGoods(this.info);
            }
          }
          // 已上架的商品
          class PublishedGoods {
            private info: any;
            constructor(info: any) {
              this.info = info;
            }
            // 獲取商品信息
            getInfo() {
              return this.info;
            }
          }

          經(jīng)過上述轉(zhuǎn)化,我們將「有多個狀態(tài)的實體」,變成「多個有狀態(tài)的實體」,這樣做的好處是,我們僅需要關(guān)心不同狀態(tài)下,實體的業(yè)務邏輯操作,將代碼聚焦于業(yè)務實現(xiàn),真正做到了領(lǐng)域知識的表達,便于橫向擴展。 前文說到,DDD 的戰(zhàn)略價值目前是大于戰(zhàn)術(shù)價值的,根本原因是目前社區(qū)缺少成熟的框架或輪子,開發(fā)者若能只專注領(lǐng)域模型的構(gòu)建,其他的交給框架來處理,才能充分發(fā)揮 DDD 的戰(zhàn)術(shù)優(yōu)勢。Remesh 便是前端 DDD 實踐的產(chǎn)物,且看改造示例:

          // 草稿商品
          type DraftGoods = {
            type'DraftGoods';
            content: any;
          };

          // 審核中商品
          type AuditingGoods = {
            type'AuditingGoods';
            content: any;
          };

          // 已發(fā)布商品
          type PublishedGoods = {
            type'PublishedGoods';
            content: any;
          };

          // 商品領(lǐng)域模型
          export const GoodsDomain = Remesh.domain({
            name: 'GoodsDomain',
            impl: (domain) => {
              // 商品狀態(tài),初始化草稿狀態(tài)
              const GoodsState = domain.state({
                name: 'GoodsDomain',
                default: {
                  type'DraftGoods',
                  content: null
                }
              });

              // 商品查詢
              const GoodsQuery = domain.query({
                name: 'GoodsQuery',
                impl: (get }) => {
                  // 返回商品信息
                  return get(GoodsState());
                }
              });

              // 編輯命令
              const EditCommand = domain.command({
                name: 'EditCommand',
                impl: ({}, info: DraftGoods) => {
                  // 返回更新后的商品
                  return GoodsState().new(info);
                }
              });

              // 上架命令
              const PublishCommand = domain.command({
                name: 'PublishCommand',
                impl: (get }) => {
                  const info = get(GoodsState());
                  // 上架后,返回審核中的商品
                  return GoodsState().new({
                    ...info,
                    type'AuditingGoods'
                  }) as AuditingGoods;
                }
              });

              // 審核命令
              const AuditCommand = domain.command({
                name: 'AuditCommand',
                impl: (get }, result: boolean) => {
                  const info = get(GoodsState());
                  if (result) {
                    // 審核成功,返回已發(fā)布的商品
                    return GoodsState().new({
                      ...info,
                      type'PublishedGoods'
                    }) as PublishedGoods;
                  } else {
                    // 審核失敗,返回草稿的商品
                    return GoodsState().new({
                      ...info,
                      type'DraftGoods'
                    }) as DraftGoods;
                  }
                }
              });

              // 撤回命令
              const RevertCommand = domain.command({
                name: 'RevertCommand',
                impl: (get }) => {
                  const info = get(GoodsState());
                  // 撤回后,返回草稿的商品
                  return GoodsState().new({
                    ...info,
                    type'DraftGoods'
                  }) as DraftGoods;
                }
              });

              return {
                query: {
                  GoodsQuery
                },
                command: {
                  EditCommand,
                  PublishCommand,
                  AuditCommand,
                  RevertCommand
                }
              };
            }
          });

          3. Remesh

          基于 CQRS 模式的 DDD 在前端的落地框架,僅關(guān)心業(yè)務邏輯,若是想在前端嘗試領(lǐng)域劃分,Remesh 不失為一種選擇。

          CQRS

          CQRS(Command Query Responsibility Segregation):命令查詢職責分離,也就是讀寫分離。命令是指會對實體數(shù)據(jù)發(fā)生變化的操作,如新增、刪除、修改;查詢即字面理解,不會對實體數(shù)據(jù)造成改變。

          通常而言,采用 CQRS 模式帶來的性能收益是巨大的,而收益也往往帶來挑戰(zhàn),比如要保證數(shù)據(jù)同步高可用,讀寫模型設(shè)計的雙倍工作量等。CQRS 是對 DDD 的一種補充,并且有幾種子模式來實現(xiàn):單服務/多服務、共享模型/不同模型、共享數(shù)據(jù)源/不同數(shù)據(jù)源,根據(jù)使用場景決定。

          核心概念

          Domain(領(lǐng)域),可以簡單理解為關(guān)于業(yè)務邏輯的 Component

          • State:Domain 的狀態(tài)
          • Query:查詢 States
          • Command:更新 States
          • Event:Domian 中可能發(fā)生的事件
          • Effect:副作用,用于發(fā)送 Query 或 Command 乍一看這結(jié)構(gòu),你會不會覺得與 Redux 等框架有點像?待我們查看其源碼來做判斷。

          源碼實現(xiàn)分析

          我們以官網(wǎng)提供的 React 示例來看 Define your domain(https://github.com/remesh-js/remesh#define-your-domain)

          npm install --save remesh rxjs

          可以明確了解到,remesh 采用 RxJS 進行事件分發(fā)與數(shù)據(jù)流轉(zhuǎn),這也意味著,remesh 的本質(zhì)在于對數(shù)據(jù)操作的高度抽象,利用 RxJS 的能力達到數(shù)據(jù)更新的效果。 我們來看 Remesh.domian 的定義:

          • Remesh.domain => RemeshDomain
          • RemeshDomain => RemeshDomainAction
          let domainUid = 0
          export const RemeshDomain = <T extends RemeshDomainDefinition, U extends Args<Serializable>>(
            options: RemeshDomainOptions<T, U>,
          ): RemeshDomain<T, U> => {

            // 優(yōu)化策略:緩存無參數(shù)時的 RemeshDomainAction 實例
            let cacheForNullary: RemeshDomainAction<T, U> | null = null
            const Domain: RemeshDomain<T, U> = ((arg: U[0]) => {
              if (arg === undefined && cacheForNullary) {
                return cacheForNullary
              }
              const result: RemeshDomainAction<T, U> = {
                type: 'RemeshDomainAction',
                Domain,
                arg,
              }
              if (arg === undefined) {
                cacheForNullary = result
              }
              return result
            }
          as unknown as RemeshDomain<TU>

            // 定義 RemeshDomain 相關(guān)信息,可選參數(shù) nameimpl 等
            Domain.type = 'RemeshDomain'
            Domain.domainId = domainUid++
            Domain.domainName = options.name // 領(lǐng)域名稱
            Domain.impl = options.impl as (context: RemeshDomainContext, arg: U[0]) =>
           T // 具體業(yè)務實現(xiàn)
            Domain.inspectable = options.inspectable ?? true

            return Domain
          }

          接著是在前端框架中通過 hook 的方式引入 useRemeshDomain(CountDomain())

          export const useRemeshDomain = function <T extends RemeshDomainDefinitionU extends Args<Serializable>>(
            domainAction: RemeshDomainAction<T, U>,
          {
            // 獲取注入的 store
            const store = useRemeshStore()
            // 若 domainAction 不在暫存的集合中,會調(diào)用 createDomainStorage 進行創(chuàng)建,且看下文
            const domain = store.getDomain(domainAction)

            // React
            useEffect(() => {
              // 當 domain 被激活后,事件訂閱開始生效,會通過 RxJS 進行事件分發(fā)
              store.igniteDomain(domainAction)
            }, [store, domainAction])

            // Vue
            onMounted(() => {
              store.igniteDomain(domainAction)
            })

            return domain
          }

          為了更好的解釋,上述代碼在 Remesh 的源碼基礎(chǔ)上稍加改動,可以看出,其主要針對不同框架的實現(xiàn)做了適配

          // remesh-react.tsx
          export const useRemeshReactContext = () => {
            const context = useContext(RemeshReactContext)

            if (context === null) {
              throw new Error(`You may forget to add <RemeshRoot />`)
            }

            return context
          }

          export const useRemeshStore = (): RemeshStore => {
            const context = useRemeshReactContext()
            return context.remeshStore
          }

          // remesh-vue.ts
          export const useRemeshStore = () => {
            const store = inject(RemeshVueInjectKey)

            if (!store) {
              throw new Error('RemeshVue plugin not installed')
            }

            return store
          }

          可以清晰的看到,React 采用了 Context 的方式注入 store,而 Vue 采用 Provide Inject 的方式注入 store,現(xiàn)在我們對 React 實現(xiàn)進行分析,來看以下 <RemeshRoot> 的實現(xiàn)

          export const RemeshRoot = (props: RemeshRootProps) => {
            // 可自定義 store
            const storeRef = useRef<RemeshStore | undefined>(props.store)

            if (!storeRef.current) {
              // 通常情況下會創(chuàng)建一個無 options 的 store
              storeRef.current = RemeshStore('options' in props ? props.options : {})
            }

            const store = storeRef.current

            const contextValue: RemeshReactContext = useMemo(() => {
              return {
                remeshStore: store,
              }
            }, [store])

            return <RemeshReactContext.Provider value={contextValue}>{props.children}</RemeshReactContext.Provider>
          }

          可以看一下 RemeshStore  的結(jié)構(gòu),儼然是一個 store 管理工具,實時也確實如此,remesh 后續(xù)的狀態(tài)查詢以及更新都是基于 store 庫執(zhí)行。

          export const RemeshStore = (options?: RemeshStoreOptions) => {
            // ...

            return {
              name: options.name,
              getDomain, // 接上文,在 useRemeshDomain 中會調(diào)用此方法
              igniteDomain,
              query: getCurrentQueryValue,
              send,
              // ...
            }
          }

          調(diào)用 getDomain 時會優(yōu)先判斷 domainAction 是否存在于緩存中,若不存在則創(chuàng)建,關(guān)鍵的來了,我們定義在 Domian 中的 impl 方法,此時會被調(diào)用。

          const createDomainStorage = <T extends RemeshDomainDefinition, U extends Args<Serializable>>(
              domainAction: RemeshDomainAction<T, U>,
            ): RemeshDomainStorage<T, U> => {
              // ...

              // domain 上下文對象,標準化創(chuàng)建過程,也是可以鏈式操作的原因(代碼已簡化)
              const domainContext: RemeshDomainContext = {
                state: (options) => {
                  return RemeshState(options)
                },
                query: (options) => {
                  return RemeshQuery(options)
                },
                event: (options: Omit<RemeshEventOptions<anyany>, 'impl'> | RemeshEventOptions<anyany>) => {
                  return RemeshEvent(options)
                },
                command: (options) => {
                  return RemeshCommand(options)
                },
                effect: (effect) => {
                  if (!currentDomainStorage.ignited) {
                    currentDomainStorage.effectList.push(effect)
                  }
                },
                // ...
              }

                 // domain 中 query, command, event 的具體實現(xiàn)
              const domain = toValidRemeshDomainDefinition(domainAction.Domain.impl(domainContext, domainAction.arg))

              // domain 的存貯對象
              const currentDomainStorage: RemeshDomainStorage<T, U> = {
                id: uid++,
                type'RemeshDomainStorage',
                Domain: domainAction.Domain,
                get domain() {
                  return domain
                },
                arg: domainAction.arg,
                domainContext,
                domainAction,
                effectList: [],
                ignited: false,
              }

              // ...

              return currentDomainStorage
            }

          關(guān)于 store 中 send、query 實現(xiàn)的基本狀態(tài)更新以及事件機制,由于篇幅有限,不再展開描述,相信以上內(nèi)容已經(jīng)達到拋磚引玉的效果。

          4. 總結(jié)

          Remesh 采用了一種獨特的方式,使 DDD 能夠在前端得以應用,為了達到這一效果,開發(fā)者可能會需要適應不同的代碼風格,需要熟悉領(lǐng)域模型的設(shè)計。

          由于項目處于起步階段,目前仍在迭代中,不建議在生產(chǎn)環(huán)境使用,或者說 DDD 在前端的戰(zhàn)術(shù)設(shè)計仍未有最佳實踐,但 Remesh 帶來的意義是非凡的,實現(xiàn)方式可能多樣,解決的問題永遠只會是同一個,我們始終在代碼優(yōu)化的路上艱難前行,DDD 將來未必是一條歧路。

          參考鏈接

          用DDD(領(lǐng)域驅(qū)動設(shè)計)和ADT(代數(shù)數(shù)據(jù)類型)提升代碼質(zhì)量(https://zhuanlan.zhihu.com/p/475789952)

          remesh(https://github.com/remesh-js/remesh)

          從MVC到DDD的架構(gòu)演進(https://zhuanlan.zhihu.com/p/456915280)

          Node 社群


          我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學習感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關(guān)的交流、學習、共建。下方加 考拉 好友回復「Node」即可。

             “分享、點贊在看” 支持一波??

          瀏覽 24
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人a片黄色免费电影 | 在线观看黄A片免费网站 | 暧暖无码 | 澳门三级少妇三级66 | 插40岁女人视频 |