在JavaScript中尝试模仿接口/抽象类是一种不好的实践吗?

13
我正在使用Node.js编写一些代码,发现使用策略模式对其进行更好的结构化。来自.Net的我会创建接口并在此基础上移动,但在JavaScript中这不是那么明显。
我理解JavaScript作为一种原型语言没有接口继承的概念,因此我不确定我所做的是否有问题,因为我似乎找不到参考资料,除了一篇博客文章,该文章试图通过使用基本抽象类来推断接口,强制继承类实现函数(因为它会抛出异常)。 我的基类
QueryStrategy = function () {

};

QueryStrategy.prototype.create = function(){
throw new Error("Not Implemented");
}

module.exports = QueryStrategy;

实现方式 1

var util = require('util');
var QueryStrategy = require('./queryStrategy');

SelectQueryStrategy = function (query) {
    this.parameters = query.parameters || [];
    this.entity = query.entity || '';
};

util.inherits(SelectQueryStrategy, QueryStrategy);

SelectQueryStrategy.prototype.create = function () {
    var self = this,
        params = self.parameters,
        paramList = self.parameters.length >= 1 ? '' : '*';

    for (var i = 0; i < params.length; i++) {
        var suffix = i === (params.length - 1) ? '' : ', ';
        paramList = paramList + params[i].key + suffix;
    }

    return util.format("SELECT %s FROM %s", paramList, self.entity);
};

module.exports = SelectQueryStrategy;

目前基础部分没有任何共享功能或属性。共享函数将会出现,因为在原型链搜索中,由于被实例创建覆盖了“共享”属性,我看不到添加“共享”属性的意义(如果这是错误的,请让我知道)。

这种方法可行吗?还是应该忽略继承?可能会有一些类型检查,如果您可以推断类型为一种类型(即它们必须全部属于类型QueryStrategy),那么会更容易,但我不希望我的.Net偏见在这里占上风。

另一种方法

首先,我不是在这里销售书籍,只是在阅读我拥有的书籍时学到很多东西,但也想给予他人应有的赞誉。

根据几条评论,可以说类型推断在这里并不会产生任何区别,可能应该把它留给鸭子类型进行检查,而试图插入未在语言中定义的内容可能不是最好的方法。我在Addy Osmani的Javascript模式中读到过接口的使用方法,尽管我的实现方式不同,虽然接口的使用是可以接受的,但可能并不需要。

可能更简单的方法是保持策略方法,但采用不同的实现方式,一种类似于Stoyan Stefanov的Javascript模式中概述的方法,您可以基于某种配置知道使用哪种策略。

伪代码示例

QueryBuilder = function () {
   this.types = [];
}

QueryBuilder.prototype.create = function (type, query) {
    var strategy = types[type];
    strategy.create(query);
};

QueryBuilder.types.Select = {

    create: function (query) {
        params = query.parameters,
        paramList = query.parameters.length >= 1 ? '' : '*';

        for (var i = 0; i < params.length; i++) {
            var suffix = i === (params.length - 1) ? '' : ', ';
            paramList = paramList + params[i].key + suffix;
        }

        return util.format("SELECT %s FROM %s", paramList, query.entity);
    }
};

这可能是一个更清晰的方法,有一件事我不确定是否应该为原型添加类型,但我想在这种简单情况下不需要,但在更复杂的情况下,您可能需要抽象出许多策略,并且在一个文件中需要进行抽象。

这是一个更可接受的方法吗?

我最终得到的结果

我四处查找,尝试更多地了解鸭子类型、优缺点等,并根据一些评论和答案简化了我的设计。我坚持使用策略式方法,每个策略定义相同的函数,基本上封装了不同实现之间变化的内容,并为它们提供了一个共同的合同(缺乏更好的术语)。

发生改变的一件事是基类/接口被删除了,因为它不起任何积极作用,每个策略都是独立的(所以不完全符合模式的经典表示),它们只是定义了相同的create函数。

从嵌套的if和switch的地方被替换为单个switch,它根据请求的查询类型决定要使用哪种查询策略。稍后可以将其重构为一个工厂,但目前它已经达到其目的:

Query.prototype.generate = function () {

var self = this,
    strategy = undefined;

switch (this.queryType) {
    case "SELECT":
        strategy = new Select(self);
        break;
    case "INSERT":
        strategy = new Insert(self);
        break;
    case "UPDATE":
        strategy = new Update(self);
        break;
    case "DELETE":
        strategy = new Delete(self);
        break;
}

return strategy.create();
};
每个策略都独立测试,我觉得这样更容易维护,因为如果出现问题,它只会在一个地方失败,并且根据单元测试进行调查将更加容易。显然存在一些权衡,有更多的文件和稍微复杂的结构...我们会看到会发生什么。

我并不真正看出这给你带来了什么好处。接口契约无论是通过工具、编译时还是运行时都没有被强制执行。另一方面,如果你可以使用 TypeScript,TypeScript 接口则会在编译时强制执行契约。 - James Gaunt
在这种新方法中,您仍然无法获得任何类型安全性(您无法 - 这是JavaScript的限制)。如果您坚持在一个地方定义策略,也许这种方法可以使维护工作变得更容易,但您仍然可以将任何对象添加为types[]的成员。我想这取决于您的目标是什么,是强制执行合同(您无法 - 除非在运行时检查),还是其他什么? - James Gaunt
1
我并不是在寻求类型安全,但我欣赏第一种方法看起来像这样。我只是在重构一个方法,该方法有几个switch case语句,if语句等等,而且它似乎以JavaScript形式的策略模式(我知道它不会完全相同)为基础。至于传递任何类型,我想我可以检查以确保它具有执行所需函数,如果没有,则返回错误。我的总体目标是可读性和编写更好结构化的代码,从.NET转过来,我明白我不能直接使用这些模式。 - Modika
1个回答

9
这是一种可接受的方法,但实际上并没有任何好处,并且可能会使这段代码更难维护。策略模式只在静态类型语言中才有真正的好处。你可以像其他评论者提到的那样尝试使用TypeScript。
在JavaScript中,你将更多地依赖于“鸭子类型”...如果它看起来像一只鸭子,闻起来像一只鸭子,那么它很可能就是一只鸭子。

http://en.wikipedia.org/wiki/Duck_typing


“鸭子类型”可能是正确的选择,我看到了创建一个使用鸭子类型来强制约定的接口函数 https://gist.github.com/1057989。即使在 JavaScript 中,我仍然认为策略模式是合法的。看看另一种替代方法,更新了问题。 - Modika
我会接受这个答案,因为它回答了原始问题,并指向了一些进一步的阅读材料。我主要还是坚持我的解决方案(已在上面更新),但基于这个答案和其他评论,我简化了一些东西。 - Modika

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接