와~ 위험한 질문이군요? 본질적인 것에 대한 우리의 이해는 해결해야 할 문제에 대한 우리의 이해에 따라 확실히 달라집니다. 그래서 저는 거짓말을 하지 않을 것입니다. 제가 1년 전에 이해한 내용의 본질은 불행하게도 불완전합니다. 왜냐하면 제가 쓰려고 하는 내용이 현재 거의 6개월 동안 저와 함께 해왔다고 확신하기 때문입니다. 따라서 이 기사는 JavaScript에서 클라이언트 측 메시징 패턴을 성공적으로 사용하기 위한 몇 가지 핵심 사항을 제가 발견한 내용을 간략하게 보여줍니다.
1.) 중재자와 관찰자의 차이점을 이해하세요
대부분의 사람들은 이벤트/메시지 메커니즘을 설명할 때 "게시자/구독자"(pub/sub)를 사용하는 것을 좋아하지만 이 용어는 추상화와 잘 연결되지 않는 것 같습니다. 물론 근본적으로 어떤 것은 다른 것이 게시한 이벤트를 구독합니다. 그러나 게시자와 구독자가 캡슐화되는 수준으로 인해 좋은 패턴이 무뎌질 수 있습니다. 그렇다면 차이점은 무엇입니까?
관찰자
관찰자 패턴은 한 명 이상의 관찰자가 개체를 관찰하는 것과 관련됩니다. 일반적으로 이 객체는 모든 관찰자의 추적을 기록하며 일반적으로 관찰자가 등록한 콜백 메서드를 저장하는 목록을 사용하며 관찰자는 알림을 받기 위해 구독합니다. 참고: (아, 말장난 의도, 얼마나 사랑하는지) (번역자 주: 관찰 관찰, 참고)
var observer = { listen : function() { console.log("Yay for more cliché examples..."); } }; var elem = document.getElementById("cliche"); elem.addEventListener("click", observer.listen);
참고할 사항은 다음과 같습니다.
중개자
중재자 패턴은 객체와 관찰자 사이에 '제3자'를 도입하여 둘을 효과적으로 분리하고 통신 방식을 캡슐화합니다. 중재자의 API는 "게시", "구독", "구독 취소"처럼 간단할 수도 있고, 좀 더 의미 있는 의미 뒤에 이러한 메서드를 숨기기 위해 도메인 전체 구현이 제공될 수도 있습니다. 내가 사용한 대부분의 서버 측 구현은 단순하기보다는 도메인 전체에 적용되는 경향이 있지만 일반 중재자를 제한하는 규칙은 없습니다! 일반적으로 보편적 중개자는 정보 중개자라는 생각이 있습니다. 두 경우 모두 결과는 동일합니다. 특정 객체와 관찰자는 더 이상 서로를 직접 알지 못합니다.
你可能会想,除了特别纯粹的中介者实现,特定对象不再负有保存订阅者列表的责任,而且“时光老人”(FatherTime)与“开发者”(Developer)实例永远没法真正互相知道。他们只是共享了一个信息——将如我们今后所见,这是一个很重要的合约。 “很好,Jim。这对我而言仍然是发布者/订阅者,那么重点呢?我选择某个方向真的会有区别吗?”哦,继续吧,亲爱的读者们,继续吧。
2.) 了解什么时候使用中介者和观察者
使用本地的观察者和中介者,即写在组件当中的,而中介者看起来又像远程的组件间通信。不管怎样。我对待这种情况的原则虽然是——tl;dr(too long; don't read)(太长,不读了)。但无论如何,反正串联在一起最好。
要我简捷地说真是麻烦,就像把几个月来的细致体验压缩到装不下140个字的沟里。现实中回答这个问题肯定不简洁。所以有一个长版本的解释:
观察者除了关心数据映射之外还有必要引用别的项目吗?例如Backbone.View视图有各种理由直接引用它的模型。这是非常自然的关系,视图不仅要在模型改变时进行渲染,还需要调用模型的事件处理。如果段首的问题答案是”yes“,那观察者就是有意义的。
如果观察者和观察对象的关系仅仅是依赖数据,那我愿意使用中介pub/sub方式。两个Backbone.View视图或模型之间的通信,用观察者是合适的。比如控制导航菜单的视图发出的信息,是面包屑(breadcrumb)挂件需要的(响应当前的层级)。挂件不需要引用导航视图,它只需要导航视图提供信息。更关键的,导航视图也许不是唯一的信息来源,别的视图可能也可以提供。此时,中介pub/sub模式是最理想的——而且自身扩展性良好。
看起来这样又好又全面,但是其实还有一个露点:如果我给对象定义一个本地事件,既想要观察者直接调用,又可以被订阅者间接访问到,怎么办?这就是我为什么说要串联在一起:你推送或者桥接本地事件到消息组去吧。需要些更多代码?很有可能——但是总比你把观察对象传递给所有观察者,一直紧耦合下去的情况好。然后,我们可以很好地继续以下两点...
3.) 选择性的“提交”本地事件到总线
最开始我几乎只用观察者模式来在JavaScript中触发事件。这是我们一次又一次遇到的模式,但更流行的客户端辅助库行为方式根本上来说是混合中介者的,给我们提供了就像它们是观察者模式的API。我最初写postal.js的时候,开始走进“为所有事物搭中介”的阶段。在我写的原型与构造函数中,分布各处的发布与订阅的调用并不罕见。当我从这个改变中自然的解耦受益时,非基础的代码开始似乎充满了相关于基础的部分。构造函数到处都要带上一个通道,订阅被当作新实例的一部分被创建,原型方法直接发布一个数值到总线(甚至本地的订阅者都不能直接的而必须监听总线以获得信息)。将这些明显关于总线的东西纳入app的这些部分,开始像是代码的味道。代码的“叙述”似乎总是被打断,如“噢,将这个向所有订阅者发布出去”,“等等!等等!监听这个通道那个事情。好,现在继续吧”。我的测试忽然开始需要依赖总线来做低层次的单元测试。而这感觉有点不对劲。
钟摆摆动的指向了中间,我认识到我应该保持一个“本地API”,并且在需要的时候通过一个中介者为应用扩展其可以触及的数据。 例如,我的backbone视图与模型,仍然用普通的Backbome.Events行为来给本地观察者发送事件(就是说,模型的事件被它相应的视图所观察)。当app的其它部分需要知道模型的变化时,我开始通过这些行将本地事件与总线桥接起来:
var SomeModel = Backbone.Model.extend({ initialize: function() { this.on("change:superImportantField", function(model, value) { postal.publish({ channel : "someChannel", topic : "omg.super.important.field.changed", data : { muyImportante: value, otherFoo: "otherBar" } }); }); } });
重要的是要认识到,当有可能透明的推送事件到消息总线时,本地事件和消息必须被认为是分开的合约——至少概念上如此。换句话说,你要能够修改“内部的/本地的”事件而不破坏消息合约。这是要在脑海中记住的重要事实——否则你就是为紧耦合提供了一个新的途径,在一个方法上走反了!
所以理所当然,上述的模型是可以在没有消息总线的情况下被测试。而且如果我移去桥接在本地事件与总线之间的逻辑,我的视图与模型依然工作得毫无不畅。但是,这可是七行的例子(尽管格式化了)。 仅仅桥接四个事件就需要几乎三十行的代码。
噢,你怎样才能二者兼顾呢—— 在适合直接观察者时本地通知,同时使涉及事件可以扩展,以便你的对象不必给所有对象都发送一圈——不需要代码膨胀。通知怎样才能很少的代码又有更多的味道呢?
4.)在你的构架中隐藏样板
这并不是说上面的例子中的代码 —— 将事件接入总线 —— 的语法或概念是错误的(假设你接受本地和远程/桥接事件的概念)。然而,这是一个很好的体现在代码基础之上培养良好习惯的作用的例子。有时我们会听到类似“代码实在太多了”的抱怨(特别是当 LOC 作为代码质量的唯一判定者时)。 当这种情况下,我表示赞同。 它是一个可怕的样板。 下面是我在桥接 Backbone 对象的本地事件到 postal.js 时使用的模式:
// the logic to wire up publications and subscriptions // exists in our custom MsgBackboneView constructor var SomeView = MsgBackboneView.extend({ className : "i-am-classy", // bridging local events triggered by this view publications: { // This is the more common 'shorthand' syntax // The key name is the name of the event. The // value is "channel topic" in postal. So this // means the bridgeTooFar event will get // published to postal on the "comm" channel // using a topic of "thats.far.enough". By default // the 1st argument passed to the event callback // will become the message payload. bridgeTooFar : "comm thats.far.enough", // However, the longhand approach works like this: // The key is still the event name that will be bridged. // The value is an object that provides a channel name, // a topic (which can be a string or a function returning // a string), and an optional data function that returns // the object that should be the message payload. bridgeBurned: { channel : "comm", topic : "match.lit", data : function() { return { id: this.get("id"), foo: 'bar' }; } }, // This is how we subscribe to the bus and invoke // local methods to handle incoming messages subscriptions: { // The key is the name of the method to invoke. // The value is the "channel topic" to subscribe to. // So this will subscribe to the "hotChannel" channel // with a topic binding of "start.burning.*", and any // message arriving gets routed to the "burnItWithFire" // method on the view. burnItWithFire : "hotChannel start.burning.*" }, burnItWithFire: function(data, envelope) { // do stuff with message data and/or envelope } // other wire-up, etc. });
显然你可以用几种不同的方式做这些——选择总线式的框架——这要比样板方式少很多无关内容,而且为Backbone开发人员所熟知。当你同时控制事件发送器和消息总线的实现时,桥接要更容易。这里有个将monologue.js发送器桥接到postal.js的例子:
// using the 'monopost' add-on for monologue/postal: // assuming we have a worker instance that has monologue // methods on its prototype chain, etc. The keys are event // topic bindings to match local events to, and if a match is // found, it gets published to the channel specified in the // value (using the same topic value) worker.goPostal({ "match.stuff.like.#" : "ThisChannelYo", "secret.sauce.*" : "SeeecretChannel", "another.*.topic" : "YayMoarChannelsChannel" });
以不同的方式使用样板是令人愉快的好习惯。现在我可以分别独立的测试我的本地对象,桥接代码,甚至测试二者合一的生产&消费期待的消息过程等等。
同样重要的是要注意到,如果我需要在上述的场景访问普通的postal API,没有什么可以阻止我这么做。没有丢失灵活性这么就等于成功了
5.) 消息是合约——要明智的选择实现方式
有两种将数据传递给订阅者的方法——也许可以给他们贴上更“官方”的标签,我将如此描述他们:
看看这些例子:
// 0-n args this.trigger("someGuyBlogged", "Jim", "Cowart", "JavaScript"); // envelope style this.emit("someGuyBlogged", { firstName: "Jim", lastName: "Cowart", category: "JavaScript" }); /* In an emitter like monologue.js, the emit call above would actually publish an envelope that looked similar to this: { topic: "someGuyBlogged", timeStamp: "2013-02-05T04:54:59.209Z", data : { firstName: "Jim", lastName: "Cowart", category: "JavaScript" } } */
经过一段时间,我发现封套方式比0-n参数方式要少很多很多麻烦(与代码)。"0-n参数"途径的挑战主要在于两个原因(就我的经验而言):第一,很典型的是“当事件触发时,你还记得要传递哪一个参数吗?不记得?好,我想我会看看触发的源头”。不是一个真正意义上的好方法,对吗?但它可以打断代码的正常流程。你可以用一个调试工具,检测执行条件下的参数值并由此推断基于这些数值的”标签“,但哪个更简单呢——看到一个”1.21“的参数值,困惑于它的意义,或者检测一个对象并发现{千兆瓦:1.21}。第二个原因是由于伴随事件传送可选的数据,以及当方法签名变得更长带来的痛苦。
"说实话,Jim,你这是在搭车棚。"或许是的,但是一段时间以来我一直看到代码的基础在扩充与变形,简单的包含一两个参数的原始事件,在其间包含了可选的参数以后开始变得畸形:
// 最开始是这样的 this.trigger("someEvent", "a string!", 99); // 有一天, 它变得包含了一切 this.trigger("someEvent", "string", 99, { sky: "blue" }, [1,2,3,4], true, 0); // 可是等等——第4和第5个参数是可选的,因此也可能传的是: this.trigger("someEvent", "string", 99, [1,2,3,4], true, 0); // 噢,你还检查第5个参数的真/假吗? // 哎呦!现在是早先的参数了…… this.trigger("someEvent", "string", 99, true, 0);
선택 사항인 데이터가 있는 경우 해당 데이터에 대한 테스트는 없습니다. 그러나 코드가 더 적게 필요하고 확장성이 더 높아야 하며 일반적으로 개체가 전달될 때 개체에 대해 수행되는 종류의 테스트를 수행할 수 있도록 자체 설명이 가능해야 합니다(멤버 이름 덕분에). 구독자 콜백 메소드를 하나씩. 나는 여전히 "0-n 매개변수"를 사용해야 하는 경우에 이 방법을 사용하지만, 그것이 나에게 달렸다면 이벤트 발신자와 메시지 버스 모두에 대해 항상 봉투 접근 방식을 사용하는 것입니다. (편향된 모습을 보여줍니다. Monologue와 Postal은 동일한 봉투 데이터 구조를 공유하며, Monologue가 사용하지 않는 채널은 제거됩니다.)
따라서 - 가입자에게 데이터를 전송하는 데 사용되는 구조는 "계약"의 일부라는 점을 인식해야 합니다. 래퍼 방향에서는 추가 매개변수를 추가하지 않고 추가 메타데이터를 사용하여 이벤트를 설명할 수 있습니다. 이렇게 하면 각 이벤트 및 구독자에 대해 메서드 서명(계약의 일부)이 일관되게 유지됩니다. 또한 메시지 구조의 버전을 쉽게 지정할 수도 있습니다(또는 필요한 경우 다른 봉투 수준 메시지를 추가할 수도 있습니다). 이 방향으로 갈 경우 일관된 봉투 구조를 사용해야 합니다.
6.) 메시지 '토폴로지'는 생각보다 중요합니다
여기에는 묘책이 없습니다. 하지만 주제와 채널의 이름을 지정하는 방법과 메시지 페이로드를 구성하는 방법에 대해 신중하게 생각해야 합니다. 나는 두 가지 방법 중 하나로 모델을 매핑하는 경향이 있습니다. 단일 데이터 채널을 사용하면 주제 앞에 모델 이름이 붙고 그 뒤에 고유 ID가 붙으며, 해당 모델은 해당 작업({modelType.id.operation})에 의해 처리됩니다. , 또는 모델 자체 채널의 경우 주제는 {id.operation}입니다. 모델이 데이터를 요청할 때 이 동작에 자동으로 응답하는 것이 지속적인 관행입니다. 그러나 버스의 모든 작업이 요청되는 것은 아닙니다. 앱에 간단한 이벤트가 게시될 수 있습니다. 이벤트를 설명하기 위해 테마의 이름을 지정합니까(이상적으로는)? 아니면 구독자의 성향이 있는 행동을 설명하기 위해 주제에 이름을 지정하는 함정에 빠졌습니까? 예를 들어 "route.changed" 또는 "show.customer.ui" 주제가 포함된 메시지입니다. 하나는 이벤트를 나타내고 다른 하나는 순서를 나타냅니다. 이러한 결정을 내릴 때 신중하게 생각하십시오. 명령은 나쁘지 않지만 요청/응답 또는 명령이 필요하기 전에 얼마나 많은 이벤트를 설명할 수 있는지 놀라게 될 것입니다.