设计模式之鸭子模式

701 阅读4分钟

When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.
  这就是鸭子模式的简单概括。

引入

  有一个国王,他觉得世界上最美妙的声音就是鸭子的叫声,于是国王召集大臣,要组建一个100只鸭子组成的合唱团。大臣们找遍了全国,终于找到99只鸭子,但是始终还差一只,最后大臣发现有一只非常特别的鸡,它的叫声跟鸭子一模一样,于是这只鸡就成为了合唱团的最后一员。
  这大概就是鸭子类型的最早实践。我们不用关心这只鸡到底是不是鸭子,只要我们需要用鸭子的地方(叫声),它和鸭子一样就好了。


代码实现

class Chicken {
  constructor(sing) {
    this.sing = sing;
  }
}

class Duck {
  constructor() {
    this.sing = () => {
      console.log('嘎嘎嘎');
    };
  }
}

const choir = Array(99)
  .fill()
  .map(() => new Duck());

choir.push(new Chicken(() => console.log('嘎嘎嘎')));

choir.forEach(({ sing }) => sing());
// 可以看到输出了一百个"嘎嘎嘎"。

在程序设计中有何意义?

  你可能对鸭子模式听过的比较少,但其实它的理念在现代编程语言中的应用非常多。
  在鸭子类型中,关注的不是对象的类型本身,而是它是如何使用的。例如,在不使用鸭子类型的语言中,我们可以编写一个函数,它接受一个类型为鸭的对象,并调用它的走和叫方法。在使用鸭子类型的语言中,这样的一个函数可以接受一个任意类型的对象,并调用它的走和叫方法。如果这些需要被调用的方法不存在,那么将引发一个运行时错误。任何拥有这样的正确的走和叫方法的对象都可被函数接受的这种行为引出了以上表述,这种决定类型的方式因此得名。
  它不关注对象的类型,而是关注对象具有的行为(方法)。
  这有点类似于面向对象编程语言中的多态,注意但不完全等于多态,鸭子模式可能只涵盖其中一部分,多态更加灵活和强大。在多态中,一个接口可以有多种不同的实现方式,不同的类都可以去实现一个接口,从而有了特地的方法。倘若我只需要调用这个方法,那我就可以不用管每个对象具体是什么类型。
  这种思想可以使得我们的代码更易于维护扩展


静态类型语言应用举例

  静态类型语言,对类型要求比较严格,在编译时就确定了变量的类型,我们可以用接口或抽象类实现。以Java举例。

import java.io.*;
import java.util.List;
import java.util.ArrayList;

interface Singable {
	void sing();
}

class Chicken implements Singable {
    private String word;

    public Chicken(String word) {
        this.word = word;
    }
    
	public void sing() {
		System.out.println(word);
	}
}

class Duck implements Singable {
	public void sing() {
		System.out.println("嘎嘎嘎");
	}
}

class Test {
	public static void main (String[] args) throws java.lang.Exception {
		List<Singable> choir = new ArrayList<>();
		
		for (int i = 0; i < 99; i++) {
		    choir.add(new Duck());
		}
		
		choir.add(new Chicken("嘎嘎嘎"));
		
		choir.forEach(item -> item.sing());
	}
}

动态类型语言应用举例

  动态类型语言,在运行时确定变量的类型。鸭子模式在这种情况下,应用更加简单和广泛。以JavaScript举例。

  • 一个对象只要Symbol.iterator是一个迭代器,就可以使用...展开。比如数组、Set。
  • 一个对象乘以一个数字。都是乘以对象的valueOf()值。比如new Date() * 3
  • 一个对象只要实现了pushpop,就可以把它当做栈来使用,尽管我们不知道它的行为是不是符合栈的定义。
  • 一个对象具有索引属性和length属性,就可以当做数组。比如Array.from({ '0': 'a', '1': 'b', '2': 'c', length: 3 })

还有很多,请你来想想?

缺点与不足

  没有什么是完美的。鸭子模式的不足主要在于两点:

  1. 调用了未实现的方法,可能导致程序异常

    上面那只特别的鸡,虽然会鸭子叫了。但是如果合唱结束,需要游泳过河回家,这时候把它扔进河里,就会出问题。因为它没有一个swim方法。
    在JavaScript中,是不是经常会出现*** is not a function的异常?

  2. 有些已实现的方法,可能由于安全限制而无法使用

    为了避免第一条中的风险出现,我们可以规定,只使用通用的方法sing。于是鸭子们失去了过河回家这个选择。
    在上述的Java示例中,List中的元素就只能调用Singable接口中的方法。无论Chicken和Duck他们各自有什么好用的方法,鸡能啄米鸭能游泳,都被禁止使用。