命令式、宣告式、函式式、物件導向、控制反轉之華山論劍(下)

瘋狂的小蘑菇發表於2017-06-26

命令式、宣告式、函式式、物件導向、控制反轉之華山論劍(下)

本文的所有例子均在當前目錄下的html檔案中,出於對慵懶同學的保護,雙擊即可執行

命令式與宣告式的實際例子

上文說了一堆理論,部分同學已經出現了大海的感覺。下面我們通過一個實際的例子(本例子根據真實場景改編),介紹下命令式與宣告式的區別與函數語言程式設計中的控制反轉。

我們要編寫一個網頁版的IDE,IDE依賴Layout模組和Store模組,Layout依賴Menu等模組。如果使用指令式程式設計,IDE程式碼如下:

function IDE(options){
    // 處理屬性
    ...
    // 初始化元件
    this.layout = Layout.init(options.layout)
    this.store = Store.init(options.store)
    // 渲染元件
    return Layout.render(this.layout)
}複製程式碼

作為一個初級程式設計師,這樣完全能實現業務需求。但是缺點顯而易見,邏輯與資料完全耦合在IDE內部,只要業務有變動,我們就需要頻繁修改元件的程式碼。這樣非常不利於程式碼維護。

我們來把程式碼改造下。把邏輯封裝在配置宣告中。

// 宣告式
const options = {
  title: 'ide',
  args: 'ide args',
  dependency: {
    Layout: {
      title: 'layout',
      args: 'layout args',
      dependency: {
        Menu: {
          title: 'menus',
          args: 'menus args',
        }
      },
    },
    Store: {
      title: 'store',
      args: 'store args',
    },
  },
}複製程式碼

我們把元件的依賴放到配置檔案中的dependency欄位,依賴關係清晰可見。而不用像程式式程式設計中,我們需要閱讀原始碼,才能看懂元件之間的依賴於呼叫關係。

使用的地方更加簡潔

const render = Frame().render
const dispatchOptions = options => ({ render: () => render(options) })

const Menu = dispatchOptions
const Store = dispatchOptions
const Layout = dispatchOptions
const Ide = dispatchOptions複製程式碼

我們看到,我們僅僅是拿到了配置,呼叫了render函式而已。

而框架程式碼處理程式碼,也很簡單

const keys = Object.keys
const json2Str = 'stringify'
const drawHandleName = 'render'

const Util = {
  resolve: (string, handle)  => eval(string)[handle](),
  tranlateJSON: (object, type) => JSON[type](object),
  concat: (items, sep) => Array.isArray(items) ? items.join(sep) : items,
}

const { resolve, tranlateJSON, concat } = Util


const Frame = () => {
  const renderModule = (children, funName) => {
    const finalFunArgs = tranlateJSON(children[funName], json2Str)
    return resolve(`${funName}(${finalFunArgs})`, drawHandleName)
  }

  const analyzeDependency = (dependency = {} ) =>
    concat(keys(dependency).map(childrenName => renderModule(dependency, childrenName)), '')

  const render = ({ title, options, dependency }) =>`
標題:${title}-引數:${options}
${analyzeDependency(dependency)}` return { render, } }複製程式碼

最終對外只暴露了一個render函式,這個函式拿到元件的配置與依賴,分別做輸入與處理依賴。

analyzeDependency拿到每個元件依賴,迴圈依賴,然後呼叫渲染模組函式。

渲染模組拿到依賴名字(如Layout)與引數args(如{ title: 'layout', options: 'layout options', dependency: ... }), 執行Layout().render(args)。把執行結果返回給analyzeDependency再返回給render。

在實際生產中,每個元件的引數分析函式可能並不相同,這個小例子裡並沒有處理。當然,這個並不難處理。

OK,這就是宣告式程式設計,業務需求並不關心Frame是如何處理,也就是說,我們無需關心計算機如何處理。每當有依賴變化時候,我們只需要處理宣告的配置與每個元件的render即可,實現起來非常簡單。

控制反轉-例子

當然,雖然上個例子做到了宣告式程式設計,但是依賴仍然耦合在配置宣告中,缺點如下:

  • 不利於做配置宣告(宣告耦合,最後的宣告非常長,且難以分析)。
  • 不利於做單元測試(因為我們必須依賴於最外層的IDE以及所有父級的元件和所有的配置宣告)。
  • 不利於做邏輯分離,看似沒有任何邏輯依賴,但是每個元件render函式卻顯式呼叫了render函式。
  • 不利於做依賴分析,每個元件的配置只有在執行時才能通過options傳入,在執行元件前,我們並不知道元件依賴哪些元件。(在不分析配置宣告的情況下)

現在,我們把剛才的程式碼做一下改動,把配置宣告挪到每個元件內,把控制權完全交給程式,讓程式根據元件配置,動態生成邏輯。

const render = ({ title, options }, children) =>
  `
    
標題:${title}-引數:${options}
${children} ` const Menu = () => { return { dependency: {}, render, } } const Store = () => { return { dependency: {}, render, } } const Layout = () => { return { dependency: { Menu: { title: 'menus', options: 'menus options', } }, render, } } const Ide = () => { return { dependency: { Layout: { title: 'layout', options: 'layout options', }, Store: { title: 'store', options: 'store options', }, }, render, } } const options = { dependency: { Ide: { title: 'ide', options: 'ide options', }, }, }複製程式碼

我們看到,配置宣告移到了各個元件內部,而每個元件的配置自己實現了render函式,render函式不在依賴於框架,而是自己實現。每個render拿到子元件可以隨意處理。而框架要做的,就是把分析出每個元件要依賴的子元件,並且完成子元件的渲染,然後傳遞給父元件的render引數。

接下來,我們看一下框架的程式碼

const { assign, keys } = Object
const dependencyField = 'dependency'

const iocFrame = (options, modules) => {
  const { dependency } = options

  const concat = (items, sep) => Array.isArray(items) ? items.join(sep) : items

  // 依賴彙總
  const collectDependency = (depend, modules) => {
    if (!depend || !keys(depend).length) return []
    return keys(depend).map((moduleId) => {
      const { dependency, render } = modules[moduleId]()
      return assign(depend[moduleId], { render, dependency: collectDependency(dependency, modules) })
    })
  }
  const dependencyTree = collectDependency(dependency, modules)

  // 分析依賴
  const analyzeDependency = (options, childrenName) => {
    if (Array.isArray(options[childrenName])) {
      const renderArgs = concat(options[childrenName].map((child, index) =>
        analyzeDependency(child, childrenName)), '')
      return options.render(options, renderArgs)
    }
    return options.render(options)
  }

  return {
    render: () => analyzeDependency(dependencyTree[0], dependencyField),
  }
}複製程式碼

collectDependency函式遞迴查詢所有的函式依賴,並且拼裝成一顆完整的依賴樹。analyzeDependency函式拿到依賴樹中的配置宣告,遞迴依次執行render函式。

注意renderArgs這個變數,我們遞迴分析出依賴的render函式返回值。把這個返回值,也就是元件所依賴所有子元件的render函式全部執行一遍,每次執行render的時候,我們查詢依賴,如果有依賴,把依賴當做引數在去遞迴查詢,直到沒有找到依賴為止。然後彙總所有render的返回結果,當做當前元件的子元件傳遞給當前元件的render函式

有人說我們不是做了兩次遞迴,一次分析依賴,一次處理依賴,為什麼不在同一次遞迴內完成,這樣效能會有所增加,請注意,我們只在靜態初始化時候分析依賴,而在元件渲染時候才去處理依賴。而分析依賴,拿到完整的依賴列表,能讓我們處理更多的事情。

這樣我們僅僅宣告瞭配置(當然配置裡也可以有某些行為,就是這裡的render函式),通過框架來處理如何呼叫行為,做到了動態生成邏輯。

控制反轉

如果我們把函式呼叫完全宣告在配置宣告中,那是不是就可以不用寫任何邏輯,僅僅依靠宣告就可以完全控制邏輯的走向?沒錯,這就是控制反轉,把控制權完全交給底層框架(也可以認為是計算機),我們僅僅需要宣告函式即可,而函式如何呼叫,何時呼叫,我們需要框架來約定。至於框架如何約定,那完全取決於業務需求,對於同樣的業務,做好業務分析,找出常見(依賴)的變化,把不變的封裝成框架,把變化的留作配置宣告。

舉個例子:

function a(opts){
    if (cond1)
        b()
    opts.forEach(opt=> c(opt))
    if (cond2)
        d()
    ...
}複製程式碼

這種程式碼通過控制反轉,如何宣告呢?

function a(opts) {
  return {
    logic: {
      if_1: { condition: cond1 , handle: b, },
      for: { source: opts, handle: opt => c(opt), },
      if_2: { condition: cond2, handle: d, },
    },
    dependency : ['b', 'c', 'd']
  }
}複製程式碼

分析程式碼也非常簡單,這裡賣個關子,各位感興趣的話,可以自己實現。

總結

筆者不愛寫總結,就醬。

相關文章