JavaScript中的黑盒驱动开发

每个开发者迟早都会发现设计模式的美丽之处。同样,开发者迟早也会发现大多数模式并不能以其纯粹的形式适用。我们经常使用变体。我们改变众所周知的定义以适应我们的用例。我知道我们(程序员)喜欢流行语。这里有一个新的——黑盒驱动开发,或简称BBDD。我在几个月前就开始应用这个概念,并且可以说结果很有希望。在完成几个项目后,我开始看到良好的实践并形成了三个原则。

什么是黑盒?

在介绍BBDD的原则之前,让我们看看黑盒的含义。根据维基百科

在科学和工程中,黑盒是指可以根据其输入、输出和传输特性进行查看的设备、系统或对象,而无需了解其内部工作原理。

在编程中,每个接受输入、执行操作并返回输出的代码段都可以被视为黑盒。在JavaScript中,我们可以通过使用函数轻松地应用这个概念。例如

var Box = function(a, b) {
    var result = a + b;
    return result;
}

这是BBDD单元的最简单版本。它是一个执行操作并立即返回输出的盒子。然而,我们经常需要其他东西。我们需要与盒子持续交互。这是我用来称呼“活黑盒”的另一种盒子。

var Box = function(a, b) {
    var api = {
        calculate: function() {
            return a + b;
        }
    };
    return api;
}

我们有一个包含盒子所有公共函数的API。它与揭示模块模式相同。这种模式最重要的特征是它带来了封装。我们对公共对象和私有对象进行了清晰的分离。

现在我们知道了什么是黑盒,让我们看看BBDD的三个原则。

原则1:模块化一切

每个逻辑片段都应该作为一个独立的模块存在。换句话说——一个黑盒。在开发周期的开始,识别这些片段有点困难。在没有编写任何代码的情况下花费太多时间来“架构”应用程序可能不会产生好的结果。有效的方法涉及编码。我们应该草拟应用程序,甚至制作其中的一部分。一旦我们有了某些东西,我们就可以开始考虑将其黑盒化。在不考虑是否正确的情况下,跳入代码并制作某些东西也更容易。关键是重构实现,直到你觉得它足够好了。

让我们以以下示例为例

$(document).ready(function() {
    if(window.localStorage) {
        var products = window.localStorage.getItem('products') || [], content = '';
        for(var i=0; i';
        }
        $('.content').html(content);
    } else {
        $('.error').css('display', 'block');
        $('.error').html('Error! Local storage is not supported.')
    }
});

我们从浏览器的本地存储中获取一个名为products的数组。如果浏览器不支持本地存储,则显示一条简单的错误消息。

代码本身很好,并且可以工作。但是,有几个职责合并到单个函数中。我们必须做的第一个优化是形成代码的良好入口点。仅将新定义的闭包发送到$(document).ready不够灵活。如果我们想延迟初始代码的执行或以其他方式运行它该怎么办?上面的代码片段可以转换为以下内容

var App = function() {
    var api = {};
    api.init = function() {
        if(window.localStorage) {
            var products = window.localStorage.getItem('products') || [], content = '';
            for(var i=0; i';
            }
            $('.content').html(content);
        } else {
            $('.error').css('display', 'block');
            $('.error').html('Error! Local storage is not supported.');
        }
        return api;
    }
    return api;
}

var application = App();
$(document).ready(application.init);

现在,我们对引导过程有了更好的控制。

我们目前的数据源是浏览器的本地存储。但是,我们可能需要从数据库中获取产品,或者简单地使用模型。提取代码的这部分是有意义的

var Storage = function() {
    var api = {};
    api.exists = function() {
        return !!window && !!window.localStorage;
    };
    api.get = function() {
        return window.localStorage.getItem('products') || [];
    }
    return api;
}

我们还有另外两个操作可以形成另一个盒子——设置HTML内容和显示元素。让我们创建一个模块来处理DOM交互。

var DOM = function(selector) {
    var api = {}, el;
    var element = function() {
        if(!el) {
            el = $(selector);
            if(el.length == 0) {
                throw new Error('There is no element matching "' + selector + '".');
            }
        }
        return el;
    }
    api.content = function(html) {
        element().html(html);
        return api;
    }
    api.show = function() {
        element().css('display', 'block');
        return api;
    }
    return api;
}

代码与第一个版本执行的操作相同。但是,我们有一个测试函数element,它检查传递的选择器是否与DOM树中的任何内容匹配。我们还将jQuery元素黑盒化,这使得我们的代码更加灵活。想象一下,我们决定删除jQuery。DOM操作隐藏在这个模块中。值得注意的是,可以编辑它并开始使用原生JavaScript或其他库。如果我们保留旧的变体,我们可能会遍历整个代码库替换代码片段。

这是转换后的脚本。一个使用我们上面创建的模块的新版本

var App = function() {
    var api = {},
        storage = Storage(),
        c = DOM('.content'),
        e = DOM('.error');
    api.init = function() {
        if(storage.exists()) {
            var products = storage.get(), content = '';
            for(var i=0; i';
            }
            c.content(content);
        } else {
            e.content('Error! Local storage is not supported.').show();
        }
        return api;
    }
    return api;
}

请注意,我们职责分离了。我们有扮演角色的对象。使用这样的代码库更容易也更有趣。

原则2:仅公开公共方法

使黑盒有价值的是它隐藏了复杂性这一事实。程序员应该只公开需要的 方法(或属性)。用于内部流程的所有其他函数都应该是私有的。

让我们获取上面的DOM模块

var DOM = function(selector) {
    var api = {}, el;
    var element = function() { … }
    api.content = function(html) { … }
    api.show = function() { … }
    return api;
}

当开发人员使用我们的类时,他感兴趣的两件事是——更改内容和显示DOM元素。他不应该考虑验证或更改CSS属性。在我们的示例中,有私有变量el和私有函数element。它们对外部世界隐藏。

原则3:使用组合而非继承

在JavaScript中继承类的一种流行方法是使用原型链。在下面的代码片段中,我们有类A被类C继承

function A(){};
A.prototype.someMethod = function(){};

function C(){};
C.prototype = new A();
C.prototype.constructor = C;

但是,如果我们使用揭示模块模式,则使用组合是有意义的。这是因为我们正在处理对象而不是函数(*实际上,JavaScript中的函数也是对象)。假设我们有一个实现观察者模式的盒子,并且我们想扩展它。

var Observer = function() {
    var api = {}, listeners = {};
    api.on = function(event, handler) { … };
    api.off = function(event, handler) { … };
    api.dispatch = function(event) { … };
    return api;
}

var Logic = function() {
    var api = Observer();
    api.customMethod = function() { … };
    return api;
}

我们通过为api变量分配初始值来获取所需的功能。我们应该注意到,使用此技术的每个类都接收一个全新的观察者对象,因此无法产生冲突。

总结

黑盒驱动开发是一种架构应用程序的好方法。它提供了封装和灵活性。BBDD附带了一个简单的模块定义,有助于组织大型项目(和团队)。我看到几个开发人员在一个项目上工作,他们都独立地构建了自己的黑盒。

关于 Krasimir Tsonev

Krasimir Tsonev 是一位前端开发人员、博主和演讲者。他喜欢编写JavaScript并试验最新的CSS和HTML功能。作为“Node.js 蓝图”一书的作者,他专注于交付尖端应用程序。Krasimir 从平面设计师开始他的职业生涯,他花费数年时间编写ActionScript3代码。现在,随着移动开发的兴起,他热衷于开发针对各种设备的响应式应用程序。他居住和工作在保加利亚,并在瓦尔纳理工大学获得了计算机科学学士和硕士学位。

Krasimir Tsonev 的更多文章…

关于 Robert Nyman [荣誉编辑]

Mozilla Hacks 的技术布道者和编辑。发表关于 HTML5、JavaScript 和开放网络的演讲和博客文章。Robert 是 HTML5 和开放网络的坚定支持者,自 1999 年以来一直从事 Web 前端开发工作——在瑞典和纽约市。他还在 http://robertnyman.com 上定期发表博客文章,并且喜欢旅行和结识新朋友。

Robert Nyman [荣誉编辑] 的更多文章…


35 条评论

  1. m

    请不要在以下示例中使用此(反)模式
    C.prototype = new A();
    为什么准备一个类的原型应该调用另一个类的构造函数(以及所有可能的副作用)?
    恕我直言,(在2014年)这会更好
    C.prototype = Object.create(A.prototype);

    2014年8月27日 06:27

    1. Krasimir Tsonev

      已记下。我会牢记这一点。

      2014年8月27日 07:35

    2. Adam Freidin

      在下一行…

      C.prototype.constructor = A;

      不应该改为

      C.prototype.constructor = C;

      ?

      2014年8月27日 07:46

      1. Krasimir Tsonev

        嗨,Adam,你说得对。
        @RobertNyman:你能否修复它。
        C.prototype.constructor = A;

        C.prototype.constructor = C;

        2014年8月27日 08:33

        1. Robert Nyman [编辑]

          是的,已更改!

          2014年8月27日 08:37

    3. Luke

      为另一个类的原型“创建”另一个类的原型是什么意思?它有什么好处?

      MDN 文章提供了一些本文中使用的方法示例,它似乎是一种简洁易懂的模式
      https://mdn.org.cn/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/prototype#Examples

      2014年8月27日 20:27

  2. Pavel Ivanov

    不错 :) 伙计们干得好 :)

    2014年8月27日 06:31

  3. Norman Kabir

    精彩的文章。在“原则2”中缺少一个“l”。

    2014年8月27日 06:33

    1. Robert Nyman [编辑]

      谢谢!已修复。

      2014年8月27日 06:50

  4. Felix Hammerl

    使用模块模式创建实例是一种隐藏私有函数的好方法,但它也有缺点。将实例方法附加到原型上速度更快且更节省内存,这也是原型系统存在的意义。在您的情况下,每个实例都有其自己的函数,而不是每个实例都使用相同的方法。

    根据我的经验,用下划线作为前缀来表示内部使用的函数就足够了,因为下划线表示:“不鼓励直接使用此函数”。如果有人违反了此规则,它将在代码审查中出现。可读性至关重要,我不知道这是否真的比this._foo()更容易理解,因为它更清楚地说明了正在发生的事情。此外,“new”优于便利构造函数。其他语言中的一些框架曾经使用过便利构造函数,但后来反对它…

    2014年8月27日 06:44

    1. Krasimir Tsonev

      多么精彩的评论。谢谢Felix。我一直在与自己进行无休止的争论,究竟应该使用什么。原型式架构还是揭示模块模式。老实说,我仍然无法做出决定。

      2014年8月27日 09:02

  5. Ted k’

    不错!!!太棒了!!!

    2014年8月27日 06:55

  6. Erick Mendoza

    非常棒的阅读!这是我们在开发任何类型的 JavaScript 应用程序时都应该牢记的东西。我还没有想过如何应用原则 #3,现在我看到了它的便利性!谢谢。

    2014年8月27日 07:36

    1. Krasimir Tsonev

      嗨,Erick,
      我很高兴你发现这篇文章很有趣。这是我过去几年一直在使用的概念。最后我抽出时间来定义和记录它 :)

      2014年8月27日 08:35

  7. Dave

    这种方法不正是单元测试的本质吗?有什么区别?

    2014年8月27日 07:48

    1. Krasimir Tsonev

      没错!我所说的黑盒驱动开发鼓励编写单元。我使用测试驱动开发已经好几年了,当我开始时,我编写的是集成测试。不是单元测试,因为我通常没有单元。但是,一旦我开始遵循本文中描述的概念,情况就发生了变化。

      2014年8月27日 08:40

  8. Marcos Rodrigues

    重构的示例很好,尽管我会称之为良好的面向对象设计。
    我建议进行一些更改
    – 允许为 Storage 类中要获取的键指定参数;
    – 将数据获取、数据呈现和视图问题分开。

    2014年8月27日 08:24

    1. Krasimir Tsonev

      +1 表示建议。我真的很喜欢关于在本地存储中获取键的想法。第二个想法也带来了一些改进的好建议。我看到那里有一点“投机性泛化”(http://goo.gl/yth5bX),但总的来说,它们都是很好的建议。

      2014年8月27日 08:58

      1. Marcos Rodrigues

        谢谢!是的,我认为对于这个简单的示例来说,这些建议会过度设计,但由于提出的原则应该适用于大型项目,因此这些建议会很有用。

        不过,我认为即使在这种情况下,数据呈现类也可以用来提高可读性。这是一个想法

        var products = storage.get();
        c.content(ProductsPresenter(products).list());

        2014年8月27日 09:37

        1. Krasimir Tsonev

          我同意。这是有道理的。

          2014年8月27日 14:10

  9. Rene

    不错的文章。 :)

    但我建议在 api 对象中添加一个“addMethod”方法,而不是这样添加属性:api.customMethod = function() { … };

    2014年8月27日 08:57

  10. azendal

    黑盒化不仅有助于编码,还有助于在创建程序规范(在创建程序之前)时创建程序、其职责和结果的简单心理模型。这可用于在开发的建模阶段运行“测试”。

    2014年8月27日 09:49

  11. AutomateAllTheThings

    在过去的 6 个月里,我一直提倡这种开发风格,但从未给它起过一个好名字!我们称之为“封装设计”,但现在我们更喜欢你的术语。

    我无法充分表达这种方法的好处。我想我会写一篇关于我们设置的文章,现在我知道该怎么称呼它了!

    2014年8月27日 18:54

    1. Krasimir Tsonev

      我很高兴你喜欢这个名字 :) 我也花了几个星期的时间在想该怎么称呼它 :)

      2014年8月28日 03:49

  12. Pulak

    非常棒的文章,值得一读。

    我从这篇文章中理解到的重点是代码模块化。此外,编写代码时应使其不依赖于任何外部库,例如 jQuery。

    在“原则 1”的代码示例中,既然您已经在函数末尾返回了 api,为什么还要在 api.int、api.content 和 api.show 函数中返回 api 呢?

    2014 年 8 月 27 日 19:34

    1. Krasimir Tsonev

      一般来说,返回某些内容(我的意思是每个函数都返回)是一个好的实践。更实际的解释是,我们可以像下面这样链式调用方法:
      .init().content().show()

      2014 年 8 月 28 日 03:52

  13. David Hanson

    我们不需要 BBDD……我们已经有 SOLID 了,它甚至更好。

    2014 年 8 月 27 日 22:17

    1. Krasimir Tsonev

      +1 赞同提及 SOLID。这是我喜欢的另一种方法。在某些情况下,我无法应用所有原则,但其中列出了一些很棒的原则。

      2014 年 8 月 28 日 03:55

      1. David Hanson

        所以有趣的是,如果你使用 TypeScript 和 Angular 进行开发,你可以实现完整的 SOLID。

        TypeScript 提供了接口,Angular 提供了依赖注入和 IoC,

        2014 年 8 月 28 日 05:48

  14. TedvG

    很高兴你了解了模块化原则和设计:o) 但更严肃地说:你可以通过一开始就使用模块化的面向对象语言来避免所有这些麻烦,因为对象天生就是模块化的黑盒。例如,使用 Dart,它也可以编译成(更快的)Javascript。Javascript 并不真正适合模块化编程,它一开始就不是为了这个目的而设计的。玩得开心

    2014 年 8 月 27 日 23:57

    1. Krasimir Tsonev

      嗨,TedvG,
      感谢你的评论。我肯定会在接下来的几个月里查看 Dart。

      2014 年 8 月 28 日 03:56

  15. Matt Perdeck

    “黑盒驱动开发”与面向对象编程有什么区别?

    你可能想看看 TypeScript。它使创建对象、继承、私有方法等比纯 JavaScript 更容易。

    2014 年 8 月 28 日 03:14

    1. Krasimir Tsonev

      嗨,Matt,

      感谢你的评论。实际上,在 BBDD 中我使用了 OOP。我的意思是,我并没有试图发明新的东西。我只是用一个新的流行语定义了我的工作流程:)

      我不是 TypeScript 类语言的忠实粉丝。这是因为我试图在我的开发周期的各个层面尽可能少地使用抽象。CSS 预处理器也是如此。我甚至创建了一个,但我现在不喜欢使用它们了。这当然是个人的观点,取决于很多因素。

      2014 年 8 月 28 日 06:38

  16. Acaz

    另一个用于良好面向对象代码的流行语。

    2014 年 8 月 28 日 09:43

  17. Loops

    在使用黑盒之前,您必须牢记:是否有人,在任何时候,可能需要对私有内容进行少量更改?如果是,则不要使用黑盒。

    在原型导向编程中,您可以覆盖原型方法,以使所有使用新方法的新实例。使用黑盒,类的原型能力在时间和空间中丢失了。实际上,这就像说:我将像使用“滑板车”一样使用我的自行车。

    2014 年 9 月 4 日 01:08

本文的评论已关闭。