博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
命令式、声明式、函数式、面向对象、控制反转之华山论剑(下)
阅读量:7223 次
发布时间:2019-06-29

本文共 5509 字,大约阅读时间需要 18 分钟。

命令式、声明式、函数式、面向对象、控制反转之华山论剑(下)

本文的所有例子均在下的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().renderconst dispatchOptions = options => ({ render: () => render(options) })const Menu = dispatchOptionsconst Store = dispatchOptionsconst Layout = dispatchOptionsconst Ide = dispatchOptions复制代码

我们看到,我们仅仅是拿到了配置,调用了render函数而已。

而框架代码处理代码,也很简单

const keys = Object.keysconst 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 } = Utilconst 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 } = Objectconst 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']  }}复制代码

分析代码也非常简单,这里卖个关子,各位感兴趣的话,可以自己实现。

总结

笔者不爱写总结,就酱。

转载地址:http://cckfm.baihongyu.com/

你可能感兴趣的文章
Ansible批量修改root密码(playbook)
查看>>
c#-WPF string,color,brush之间的转换
查看>>
镁客网每周硬科技领域投融资汇总(10.21-10.27),AI芯片创企Syntiant获英特尔等头部企业投资...
查看>>
惰性计算辨析
查看>>
单据类报表的制作
查看>>
EVCache缓存在 Spring Boot中的实战
查看>>
Kubernetes-架构路线图
查看>>
java类加载时机与过程
查看>>
Win10安装MySQL5.7.22 解压缩版(手动配置)方法
查看>>
ASP.NET CORE系列【四】基于Claim登录授权
查看>>
PostgreSQL checksum
查看>>
Android-来填写一个验证码吧!(二)
查看>>
activit5升到flowable后的全局设置的修改
查看>>
「镁客早报」苹果HomePod音箱国行版明年国内推出,售价2799;一加与英国最大移动运营商EE达成战略合作...
查看>>
运营技巧 | 用数据了解玩家构成,找回你流失的用户
查看>>
javascript异步编程
查看>>
ReactiveCocoa在UIView上的运用
查看>>
Confluence 6 升级自定义的站点和空间应用你的自定义布局
查看>>
gradle build docker image
查看>>
D语言/DLang 2.085.1 发布,修复性迭代
查看>>