【设计模式】Javascript 实现接口检查的方式

设计模式对于程序猿来说一点都不陌生,你肯定听过,可能不一定明白,但是你在实际项目中一定多多少少应用过某种设计模式。

个人认为设计模式的重要之处就在于指导你如何更加优雅地组织你的代码,能让你写出更加可维护、可理解的代码。

接下来的一段时间,我将把我学习的一些总结记录下来。主要是防止自己再次忘记,如果能多多少少帮助到你,那真是太好不过了。

接口是面向对象编程里最有用的工具之一,它不关心对象的具体实现,只关心对象有没有需要的接口。我们在面向对象编程时,首要的是针对接口编程而不是实现。

但在 Javascript 中,并没有内置实现接口的方法,也没有检查对象有没有实现某个接口的方法。不过因为 Javascript 的灵活性,实现这些也不是不可能。

一、什么是接口

接口提供了一种用来说明某个对象应该实现了哪些方法的手段。虽然它能表明这些方法的语义,但是并不规定这些方法的实现;如果两个对象都有 SetName 接口,那么我们完全可以在 setName 方法中替换这两个对象。

原本我们只能接受某个对象作为参数,现在我们完全可以只要求这个对象实现了某些接口,只要是实现了某个接口的对象,都可以作为参数传入。这样能将两个完全不相关的对象平等对待。

二、在 Javascript 中模仿接口

1、注释法

注释法是最简单的实现接口的方式,但是也是效果最差的。它把描述接口的信息写在了注释中,通过书面文档的形式描述这个对象的接口实现。

/*
    interface List {
        function add() {}
        function remove() {}
    }
*/

function List() {}

List.prototype.add = function() {}
List.prototype.remove = function() {}

这种实现方式极其简单,但是因为没有具体的检查方法,对象的接口实现完全取决于代码有没有实现。

2、属性检查法

这种实现方法比上一个严谨了一些,类都需要明确声明自己实现了哪些接口,不过这些定义仍然属于注释文档水平上的,一般我们通过检查一个属性来判断类是否实现了某个接口。

/*
    interface List {
        function save() {}
        function remove() {}
    }
*/

function List() {
    this.implementsInterfaces = ['save', 'remove']
}

function addItem(listInstance) {
    if (!checkImplements(listInstance, 'save', 'remove')) {
        throw Error('object has not implement a required interface')
    }
}

function checkImplements(obj) {
    var implementsInterfaces = obj.implementsInterfaces;
    for(var i = 1, len = arguments.length; i < len; i++) {
        var interfaceName = arguments[i];
        var hasFound = false;
        for(var j = 0, length = implementsInterfaces.length; j < length; j++) {
            if (implementsInterfaces[j] === interfaceName) {
                hasFound = true;
                break;
            }
        }
        if (hasFound) {
            return true;
        }
    }
    return false;
}

上面列子中,List 类宣称自己实现了 save、remove 接口,但其实只是把这两个接口名称放到了自己的 implementsInterfaces 属性数组中。这种方法有它的可靠之处,首先是简单方便,类有文档说明,也能通过检查来判断类是否实现了某些接口,没有接口也会报出错误。

但是,用这种方式的实现接口的类,只是声明了自己实现了哪些接口,但是你不能确保它真正地实现了接口。如果你只声明了接口,却忘记实现,这就会给后面代码留下了隐患。

3、鸭式辨型法

鸭式辨型法是基于“只要像鸭子一样走路,并且会呱呱叫的就是鸭子”的原则产生的,它不关心类声明了哪些接口,只关心类实现了哪些接口。它把对象的实现方法集作为判断是不是某个类的实例的唯一标准。

function Interface(name, methods) {
	if (!methods || Object.prototype.toString.call(methods) !== '[object Array]' || methods.length === 0) {
		throw new Error('Interface required methods')
	}

	this.name = name
	this.methods = methods
}

Interface.ensureImplements = function(instance) {
	if (arguments.length === 1) {
		throw new Error('Function Interface.ensureImplements called with 1 argument, but excepted at least 2')
	}

	for (var i = 1, len = arguments.length; i < len; i++) {
		var interface = arguments[i]
		if (!interface instanceof Interface) {
			throw new Error('Interface.ensureImplements excepted arguments above to be instance of Interface')
		}

		for (var j = 0, length = interface.methods.length; j < length; j++) {
			var method = interface.methods[j]
			if (!instance[method] || typeof instance[method] !== 'function') {
				throw new Error('Function Interface.ensureImplements: object does not implement the ' + interface.name + ' interface, method ' + method + ' does not found')
			}
		}
	}
}


function TestClass() {}
// TestClass.prototype.save = function() {}
TestClass.prototype.remove = function() {}

var testInterface = new Interface('TestInterface', ['save', 'remove'])
var testInstance = new TestClass()
try {
	Interface.ensureImplements(testInstance, testInterface)
	window.alert('接口校验通过')
} catch(e) {
	window.alert('接口校验失败')
}

这种方式对接口的实现会进行严格检查,当不存在某一接口时,会抛出错误。

这应该算是最好的实现接口的方式了,但是其也有不好之处,接口定义和类独立开了,你需要一个辅助类和辅助方法,跟传统的接口实现不一样了。