《js 设计模式与开发实践》读书笔记 1


  设计模式的定义是:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。通俗一点说,设计模式是在某种场合下对某个问题的一种解决方案。再通俗点,设计模式是给面向对象开发中一些好的设计取个名字。我们都知道设计经验非常重要。也许你有过这种感觉:这个问题发生的场景似曾相识,以前我遇到并解决过这个问题,但是我不知道怎么跟别人去描述它。each 函数我们都知道,用来迭代一个数组,一个新手很难想到说这个 each 函数其实就是迭代器模式。于是向别人描述这个函数结构和意图的时候会遇到困难,而一旦大家对迭代器模式这个名称达成了共识,剩下的交流便是自然而然的事情。

  在软件设计中,模式是一些经过了大量实际项目验证的优秀解决方案。熟悉这些模式的程序员,对某些模式也许形成了条件反射。当合适的场景出现时,她们可以很快地找到某种模式作为解决方案。

  设计模式被一些人认为是夸夸其谈的东西,这些人认为设计模式并没有多大用途。毕竟我们用普通的方法就能解决的问题,使用设计模式可能会增加复杂度,或带来一些额外的代码。如果对一些设计模式使用不当,事情还可能变得更糟。从某些角度来看,设计模式确实有可能带来代码量的增加,或许也会把系统的逻辑搞得更复杂。但软件的开发成本并非全部在开发阶段,设计模式的作用是让人们写出可复用和可维护性高的程序。假设有一个空房间,我们要日复一日地往里面放一些东西。最简单的方法当然把这些东西直接扔进去,但时间久了,就会发现很难从这个房子里找打自己想要的东西,要调整某几样东西的位置也不容易。素以在房间里做一些柜子也许是个更好的选择,虽然柜子会增加我们的成本,但它可以在维护阶段为我们带来好处。使用这些柜子存放东西的规则,或许就是一种模式。

  所有设计模式的实现都遵循一条原则,“找出程序中变化的地方,并将变化封装起来”。一个程序设计总是可以分为可变的部分和不变的部分。当我们找出可变的部分,并且把这些部分封装起来,那么剩下的就是不变和稳定的部分。这些不变和稳定的部分是非常容易复用的。设计模式被人误解的一个重要原因是人们对它的误用和滥用,比如将一些模式用在了错误的场景中或者说不该使用模式的地方刻意使用模式。初学者刚学会一个模式,恨不得所以代码都用这个模式实现。锤子理论:当我们有了一把锤子,看什么都是钉子。

  在设计模式的学习中,会发现,代理模式和装饰者模式,策略模式和状态模式,策略模式和智能命令模式,这些模式的类图看起来几乎一模一样,模式只有放在具体的环境下才有意义。比如我们手机,打电话时,它是电话,闹钟响时它是闹钟,玩游戏时它是游戏机。很多模式的类图和结构确实很相似,辨别模式的关键是这个模式出现的场景,以及为我们解决了什么。

  js 没有提供面向对象语言中的类式继承,而是通过原型委托的方式实现对象与对象之间的继承。编程语言按照数据类型大体可以分为两类,一类是静态类型语言,另一类是动态类型语言。静态类型语言在编译时变已经确定变量的类型,而动态型语言的变量类型要到程序运行的时候,待变量被赋予某个值之后,才会具有某种类型。

  静态类型语言的优点首先是在编译时就能发现类型不匹配的错误,编辑器可以帮助我们提前避免程序在运行期间有可能发生的一些错误。其次,如果在程序中明确地规定了数据类型,编译器还可以针对这些信息对程序进行一些优化工作,提高程序执行速度。静态类型语言的缺点首先是迫使程序员依照强契约来编写程序,为每个变量规定数据类型,归根结底只是辅助我们编写可靠性高程序的一种手段,而不是编写程序的目的,毕竟我们编写程序的目的是为了完成需求交付生产。其次,类型的声明也会增加更多的代码,在程序编写过程中,这些细节会让程序员的精力从思考业务逻辑上分散开来。

  动态类型语言的优点是编写的代码数量更少,看起来也更简洁,可以把精力更多放在业务逻辑上面。动态类型语言的缺点是无法保证变量的类型,从而在程序的运行期有可能发生跟类型相关的错误。

  js 是什么类型的语言呢。它是一门典型的动态类型语言。动态类型语言对变量类型的宽容给实际编码带来了很大的灵活性。由于无需进行类型检测,我们可以尝试调用任何对象的任意方法,而无需考虑它原本是否被设计为拥有该方法。这一切都建立在鸭子类型 duck typing 的概念上。鸭子类型的通俗说法是:如果它走起来像鸭子,叫起来像鸭子,那么它就是鸭子。

  讲过故事,有个 js 王国,国王觉得世界上最美妙的声音就是鸭子的叫声,于是他要组建一个 1000 只鸭子的合唱团,只是找遍全国,只有 999 只鸭子,最终还是差一只,大臣发现一只鸡叫声和鸭子一模一样,于是这只鸡就成为了合唱团的最后一员。这个故事意思是,国王要的只是鸭子的叫声,声音的主人是谁不重要。鸭子类型指导我们只关注对象的行为,而不关注对象本身。也就是关注 HAS-A,而不是 IS-A。

var duck = {
  duckSinging: function () {
    console.log('gagaga')
  }
}

var chicken = {
  duckSinging: function () {
    console.log('gagaga')
  }
}

var choir = []

var joinChoir = function (animal) {
  if (animal && typeof animal.duckSinging === 'function') {
    choir.push(animal)
    console.log('welcome to choir', choir.length)
  }
}

joinChoir(duck)
joinChoir(chicken)

  我们看到只要它们拥有 duckSinging 方法,就可以加入合唱团。在动态类型语言的面向对象设计中,鸭子类型的概念至关重要。利用鸭子类型的思想,我们不必借助于超类型的帮助,就能轻松在动态语言中实现一个原则:面向接口编程,而不是面向实现编程。例如一个对象若有 push 和 pop 方法,并且这些方法提供了正确的实现,它可以被当作栈来使用。一个对象如果有 length 属性,也可以依照下标来存取属性,这个对象就可以被当作数组来使用。在 js 中,面向接口编程的过程跟主流的静态类型语言不一样,因此,在 js 中实现设计模式的过程也与我们熟悉的语言中实现的过程会大相径庭。

  面向对象语言经常说的三种封装,继承,多态。多态的含义是:同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。换句话说,给不同的对象发送同一个消息的时候,这些对象会根据这个消息分别给出不同的反馈。从字面上理解比较困难,举个例子:家里养了只猫,养了只狗,当你向它们发出叫的命令时,一只喵喵喵,一只汪汪汪。它们都是动物,并且可以发出叫声,根据命令,发出不同的叫声。这里,就蕴含了多态的思想。

var makeSound = function (animal) {
  if (animal instanceof Cat) {
    console.log('miaomiaomiao')
  } else if (animal instanceof Dog) {
    console.log('wangwang')
  }
}

var Cat = function () {}
var Dog = function () {}

var objCat = new Cat()
makeSound(objCat) //miaomiaomiao
makeSound(new Dog()) //wangwang

  instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。objCat instanceof Object //true。上面代码中体现了多态性,但是当我们再加入动物时,我们得继续改动 makeSound 函数,才能让他发出声音,当动物越来越多,makeSound 有可能变成一个具体的函数。

  多态的思想是将做什么和谁去做以及怎么去做分离开,也就是将不变的事物和可能改变的事物分离开来。在这个故事中,动物都会叫,这是不变的,但是不同类型的动物具体怎么叫是可变的。把不变的部分隔离出来,把可变的部分封装起来,这给予了我们扩展程序的能力,程序看起来是可生长的,也是符合开放-封闭原则的。相对于修改代码来说,仅仅增加代码就能完成同样的功能,这显然优雅和安全得多。所以我们怎么做呢?

var makeSound = function (animal) {
  animal.sound()
}

var Cat = function () {}
Cat.prototype.sound = function () {
  console.log('miaomiao')
}
var Dog = function () {}
Dog.prototype.sound = function () {
  console.log('wangwang')
}

var Pig = function () {}
Pig.prototype.sound = function () {
  console.log('hengheng')
}

var objCat = new Cat()
makeSound(objCat) //miaomiaomiao
makeSound(new Dog()) //wangwang
makeSound(new Pig()) //hengheng

  现在我们向猫和狗都发出叫的消息,他们接到消息做出不同反应,如果有一天动物增加了。这时候简单追加一些代码就好了,而不需要改动以前的。

  类型检查时在表现出对象多态性之前一个绕不开的话题,但是 js 时一门不必进行类型检查的动态类型语言,但是为了了解多态的目的,我们要从静态类型语言讲起。静态类型会在编译时进行类型匹配检查。以 java 为例,由于代码编译时进行严格的类型检查,所以不能给变量赋予不同类型的值,这种类型检查有时候会让代码显得僵硬。

String str;
str="abc" //没问题
str=2 //报错。

public class Cat{
  public void makeSound(){
    System.out.println('miaomiaomiao')
  }
}

public class Dog{
  public void makeSound(){
    System.out.println('wangwang')
  }
}

public class AnimalSound{
  public void makeSound(Cat cat){
    cat.makeSound()
  }
}

public class Test{
  public static void main(String args[]){
    AnimalSound animalSound = new AnimalSound()
    Cat cat = new Cat()
    animalSound.makeSound(cat) // miaomiaomiao
  }
}

   我们已经顺利让猫叫出声,但是如果现在想要狗也叫出来,发现不行,因为 AnimalSound 中的 makeSound,只接收 Cat 类型的参数。为了解决这个问题,静态类型的面向对象语言通常被设计为可以向上转型:当给一个变量赋值时,这个变量的类型可以使用这个类本身,也可以使用这个类的超类。类似我们在描述天上的一只麻雀或一只喜鹊时,通常说一只麻雀在天上飞,或者一只喜鹊在天上飞。但如果想忽略它们的具体类型,那么也可以说一只鸟在飞。同理,当 Cat 和 Dog 对象的类型都隐藏在超类型 Animal 身后,Cat 对象和 Dog 对象就能被交换使用,这是让对象表现出多态性的必经之路,而多态性的表现正是实现众多设计模式的目标。

  使用继承来得到多态效果,是让对象表现出多态性的最常用手段。继承通常包括实现继承和接口继承。我们先创建一个 Animal 抽象类,再分别让 Cat 和 Dog 都继承自 Animal 抽象类。

public abstract class Animal{
  abstract void makeSound()  //抽象方法
}


public class Cat extends Animal{
  public void makeSound(){
    System.out.priintln('miaomiaomiao')
  }
}

public class Dog extends Animal{
  public void makeSound(){
    system.out.println('wangwang')
  }
}

Animal cat = new Cat();
Animal dog = new Dog();

public class AnimalSound{
  public void makeSound(Animal animal){
    animal.makeSound()
  }
}

public class Test{
  public static void main(string args[]){
    AnimalSound animalSound = new AnimalSound();
    Animal cat =new Cat();
    Animal dog =new Dog();
    animalSound.makeSound(cat);// miao
    animalSound.makeSound(dog);// wang
  }
}

  多态的思想实际上是把做什么和谁去做分离开,要实现这一点,就是要消除类型之间的耦合关系。在 java 中,可以通过向上转型来实现多态。而 js 的变量类型在运行期是可变的。一个 js 对象,既可以表示 dog 类型对象,也可以表示 cat 类型对象,这意味 js 对象的多态性是与生俱来的。

  多态的最根本好处在于,你不必在向对象询问你是什么类型而后根据得到的答案调用对象的某个行为,你只管调用该行为就是来。其他的一切多态机制都会为你安排妥当。换句话说,多态最根本的作用就是通过把过程化的条件分支语句转换为对象的多态性,从而消除这些条件的分支语句。举个例子:在拍电影,当导演喊出 action 时,主角开始背台词,照明时负责打灯光,后面的群众演员假装中枪倒地,道具师给镜头里撒雪花。在得到同一个消息时,每个对象都知道自己应该做什么。如果不利用对象的多态性,而是用面向过程的方式来编写这一段代码,那么相当于在电影开始拍摄之后,导演每次都要走到每个人面前,确认他们的工种,然后告诉他们做什么。如果写在程序里,里面会充斥条件分支语句。利用对象的多态性,导演在发布消息时,就不必考虑各个对象接到消息后应该做什么,对象应该做什么不是临时决定的,而是已经事先约定和排练完毕的。每个对象做什么,已经成了该对象的一个方法,被放在对象的内部,每个对象负责他们自己的行为,所以这些对象可以根据同一个消息,有条不紊的进行各自的工作。将行为分布在各个对象中,并让这些对象各自负责自己的行为,这正是面向对象设计的优点。

  看一个现实开发中遇到的例子,这个例子的思想和动物叫声的故事非常相似。假设我们编写一个地图应用,我们选择谷歌地图,通过 api 中的 show 方法,负责在页面上展示整个地图。

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

var renderMap = function () {
  googleMap.show()
}

renderMap()

  后来某些原因,要把谷歌地图换成百度地图,为了让 renderMap 函数保持一定的弹性,我们用一些条件分支来让 renderMap 函数同时支持谷歌地图和百度地图。

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

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

var renderMap = function (type) {
  if (type === 'google') {
    googleMap.show()
  } else if (type == 'baidu') {
    baiduMap.show()
  }
}

renderMap('google')
renderMap('baidu')

  这种弹性很脆弱,一旦换成腾讯地图,必须要修改 renderMap 函数,继续往里面堆砌条件分支语句。我们还是要先把程序中相同的部分抽象出来,就是显示地图。

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

renderMap(googleMap)
renderMap(baiduMap)

var tengxunMap={
  show:function(){
    console.log('tengxun ditu')
  }
}

renderMap(tengxunMap)

  在这个例子中,我们假设每个地图API提供展示地图的方法都是show,在实际开发中也许不会如此顺利,这时候可以借助适配器模式来解决问题。那就是下次了!

posted @ 2022-09-16 11:07  艾路  阅读(43)  评论(0编辑  收藏  举报