《js 设计模式与开发实践》读书笔记 14(完)

在传统面向对象语言中,给对象添加功能常常使用继承的方式,但是继承的方式并不灵活,还会带来许多问题:一方面会导致超类和子类之间存在强耦合性,当超类改变时,子类也会随之改变;另一方面,继承这种功能复用方式通常被称为白箱复用,白箱时相对可见性而言的,在继承方式中,超类的内部细节对子类可见的,继承常常被认为破坏了封装性。

  给对象动态的增加职责的方式称为装饰者模式。装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态的添加职责。装饰者是一种更轻便灵活的做法,这是一种即用即付的方式,比如天冷了多穿一件外套,需要飞时在头上插上竹蜻蜓。

  在 js 中,几乎一切都是对象,其中函数又被称为一等对象。在平时的开发工作中,也许大部分时间都在和函数打交道。在 js 中可以很方便给某个对象扩展属性和方法,但很难在不改动某个函数源代码的情况下,给该函数添加一些额外的功能。很多时候我们不想去碰原函数,也许原函数是由其他同事编写的,里面实现非常杂乱。甚至在一个古老的项目中,这个函数的源代码被隐藏在阴暗焦炉里。比如我们想给 window 绑定 onload 事件,但是又不确定这个事件是不是已经被其他人绑定过,为了避免覆盖掉之前的 window.onload 函数中的行为,我们一般会先保存好原先的 window.onload,把它放入新的 window.onload 里执行。

window.onload = function () {
  alert(1)
}

var _onload = window.onload || function () {}

window.onload = function () {
  _onload()
  alert(2)
}

  这样在新增功能时,确实没有修改原来的 window.onload 代码,但是这种方式存在两个问题。必须维护_onload 这个中间变量,虽然看起来不起眼,但如果函数的装饰链较长,或者需要装饰的函数较多,这些中间变量的数量也会越来越多。二是遇到了 this 被劫持的问题。所以可以用我们之前讲过的 aop 函数方法。这种在实际开发中,比如项目开发的结尾阶段要加上很多统计数据的代码,这些过程可能让我们被迫改动早已封装好的函数。比如一个按钮被点击了多少次。

<html>
  <button tag="login" id="button">登录</button>
  <script>
    var showLogin = function () {
      console.log('打开登录弹窗')
      log(this.getAttribute('tag'))
    }

    var log = function (tag) {
      console.log('上报标签:' + tag)
    }

    document.getElementById('button').onclick = showLogin

    Function.prototype.after = function (afterfn) {
      var _self = this
      return function () {
        var ret = _self.apply(this, arguments)
        after.apply(this, arguments)
        return ret
      }
    }

    showLogin = showLogin.after(log)
  </script>
</html>

  showLogin 函数里既要负责打开弹窗,也要负责数据上报,这是两个层面的功能,在此处被耦合在一个函数里。使用 aop 分离之后。

  如果现有的接口已经能够正常工作,那我们就永远不会用上适配器模式。适配器模式是一种亡羊补牢的模式,没人会在设计之初就使用它。我们以前做过一个地图的多态实现。

var googleMap = {
  show: function () {
    console.log('谷歌地图渲染')
  }
}

var baiduMap = {
  show: function () {
    console.log('百度地图渲染')
  }
}

var renderMap = function (map) {
  if (map.show instanceof Function) {
    map.show()
  }
}

renderMap(googleMap)
renderMap(baiduMap)

  这段程序顺序运行的关键是两个 map 都提供了 show 方法。假如 baiduMap 提供的不叫 show 叫 display 呢。这个来源第三方,正常情况下我们都不应该去改动它。我们可以加一个 baiduMapAdapter 来解决问题。日常工作中,比如我们先对接了一些城市信息,后续新的城市信息与我们以前的需求格式不统一,我们可以用一种轻便的方式就是新增一个数据格式转换的适配器。

var baiduMapAdapter = {
  show: function () {
    return baiduMap.display()
  }
}

  在人的常规思维中,总是习惯性把一组相关的行为放到一起,如何正确的分离职责不是一件容易的事情。我们也许也许没有考虑过如何分离职责,但这不妨碍我们编写代码完成需求。srp原则的优点是降低了单个类或者对象的复杂度,按照职责把对象分解成更小的粒度,这有助于代码的复用,也有利于进行单元测试。当一个职责需要变更时,不会影响其他的职责。但他的缺点就是,会增加编写代码的复杂度。我们按照职责把对象分解成更小的粒度之后,实际上也增大了这些对象之间相互联系的难度。

  最少知识原则说的是一个软件实体应当尽可能少地与其他尸体发生相互作用。这里的软件实体是一个广义的概念,不仅包括对象,还包括系统,类,模块,函数,变量等。比如说将军需要一些散兵坑。一种完成的方式是将军通知上校让他叫来少校,少校叫来上尉,上尉去通知一个士官,在通知一个士兵,最后挖掘散兵坑。这种方式很荒谬。类似于.after.after.after代码需要通过这么长的消息连才能完成一个任务。最少知识原则要求我们在设计程序时,应当尽量减少对象之间的交互。如果两个对象之间不必彼此直接通信,那么这两个对象就不要发生直接的相互联系。常见做法是引入一个第三者对象。来承担这些对象之间的通信作用。

  开放-封闭原则。就像window.onload函数扩展功能时,用到的两种方式。一种是修改原有的代码,另一种是增加一段新的代码。使用那种方式效果更好,已经不言而喻。现实生活中也可以找到开放-封闭原则相关的故事。有一家生产肥皂的大企业,从欧洲花巨资引入了一条生产线。生产线可以从原材料加工到包装成箱整个流程,美中不足的是有空盒率。于是老板从欧洲找了一支专家,花费数百万改造生产线,解决了这个问题,另一家通过一个大风扇在生产线旁边吹,空肥皂盒就会被吹走。这个故事告诉我们,相比修改源程序,如果通过增加几行代码解决问题,那么显然这更加简单和优雅。而且增加代码并不会影响原系统的稳定。这个故事目的不是说风扇成本多低,而是说明,如果可以用风扇这种简单的方式解决问题,根本没有必要大动干戈改造原有的生产线。

  过多的条件语句是造成程序违反开放-封闭原则的一个常见原因。每当需要增加一个新的if语句时,都要被迫改动原函数。把if换成switch-case是没有用的。这是一种换汤不换药的做法。实际上,每当我们看到一大批的if或者switch-case语句时,第一时间就应该考虑能否利用多态来重构他们。

  提炼函数时一种常见的优化方案。避免出现超大函数,独立出来的函数有助代码复用。独立出来的函数更容易被覆写。独立出来的函数如果有一个好的命名,本身就起到了注释的作用。合并重复的代码。一些条件重复的分支语句。将分支条件语句提炼成函数。传递对象参数代替过长的参数列表。

posted @ 2022-09-19 10:44  艾路  阅读(24)  评论(0编辑  收藏  举报