[javascript] 발행 / 구독 패턴 (JS / jQuery에서)을 사용하는 이유는 무엇입니까?

그래서 동료가 저에게 게시 / 구독 패턴 (JS / jQuery에서)을 소개했지만 ‘일반적인’JavaScript / jQuery보다이 패턴을 사용 하는 이유 를 파악하는 데 어려움을 겪고 있습니다.

예를 들어 이전에는 다음 코드가있었습니다.

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    var orders = $(this).parents('form:first').find('div.order');
    if (orders.length > 2) {
        orders.last().remove();
    }
});

예를 들어, 대신 이렇게하는 것의 장점을 볼 수 있습니다.

removeOrder = function(orders) {
    if (orders.length > 2) {
        orders.last().remove();
    }
}

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    removeOrder($(this).parents('form:first').find('div.order'));
});

removeOrder다양한 이벤트 등에 대해 기능을 재사용 할 수있는 기능을 도입하기 때문입니다 .

그러나 게시 / 구독 패턴을 구현하고 동일한 작업을 수행하는 경우 다음 길이로 이동하는 이유는 무엇입니까? (참고로, jQuery tiny pub / sub 사용 )

removeOrder = function(e, orders) {
    if (orders.length > 2) {
        orders.last().remove();
    }
}

$.subscribe('iquery/action/remove-order', removeOrder);

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    $.publish('iquery/action/remove-order', $(this).parents('form:first').find('div.order'));
});

나는 패턴에 대해 확실히 읽었지만 이것이 왜 필요한지 상상할 수 없습니다. 이 패턴을 구현 하는 방법 을 설명 하는 튜토리얼 은 저의 기본 예제 만 다룹니다.

pub / sub의 유용성은 더 복잡한 응용 프로그램에서 분명해질 것이라고 생각하지만 하나는 상상할 수 없습니다. 요점을 완전히 놓치고있는 것이 두렵습니다. 하지만 요점이 있으면 알고 싶습니다!

이 패턴이 유리한 이유와 상황에 대해 간결하게 설명해 주 시겠습니까? 위의 예제와 같은 코드 스 니펫에 pub / sub 패턴을 사용할 가치가 있습니까?



답변

지난 몇 년 동안 매우 현대적인 JavaScript의 MV * (MVC / MVP / MVVM) 패턴과 함께 사용되는 느슨한 결합과 단일 책임에 관한 것입니다.

느슨한 결합 은 시스템의 각 구성 요소가 자신의 책임을 알고 다른 구성 요소에 대해서는 신경 쓰지 않는 객체 지향 원칙입니다 (또는 최소한 가능한 한 신경 쓰지 않으려 고합니다). 느슨한 결합은 다른 모듈을 쉽게 재사용 할 수 있기 때문에 좋은 것입니다. 다른 모듈의 인터페이스와 연결되지 않습니다. 발행 / 구독을 사용하면 큰 문제가 아닌 발행 / 구독 인터페이스와 결합됩니다. 두 가지 방법 만 있습니다. 따라서 다른 프로젝트에서 모듈을 재사용하기로 결정한 경우 복사하여 붙여 넣기 만하면 작동 할 수 있거나 적어도 작동하도록 만드는 데 많은 노력이 필요하지 않습니다.

느슨한 결합에 대해 이야기 할 때 우려분리를 언급해야합니다.. MV * 아키텍처 패턴을 사용하여 애플리케이션을 빌드하는 경우 항상 모델과 뷰가 있습니다. 모델은 애플리케이션의 비즈니스 부분입니다. 다른 응용 프로그램에서 재사용 할 수 있으므로 일반적으로 다른 응용 프로그램에서 다른보기를 갖기 때문에 표시하려는 단일 응용 프로그램의보기와 결합하는 것은 좋지 않습니다. 따라서 Model-View 통신을 위해 게시 / 구독을 사용하는 것이 좋습니다. 모델이 변경되면 이벤트를 게시하고 뷰는 이벤트를 포착하고 자체적으로 업데이트합니다. 게시 / 구독으로 인한 오버 헤드가 없으므로 디커플링에 도움이됩니다. 같은 방식으로 컨트롤러에 애플리케이션 로직을 유지하고 (MVVM, MVP는 정확히 컨트롤러가 아님) 가능한 한 간단하게보기를 유지할 수 있습니다. 뷰가 변경되면 (예를 들어 사용자가 무언가를 클릭) 새 이벤트를 게시하고 컨트롤러가이를 포착하여 수행 할 작업을 결정합니다. 당신이 익숙한 경우MVC 패턴 또는 Microsoft 기술 (WPF / Silverlight)의 MVVM 을 사용하면 게시 / 구독을 Observer 패턴 과 같이 생각할 수 있습니다 . 이 접근 방식은 Backbone.js, Knockout.js (MVVM)와 같은 프레임 워크에서 사용됩니다.

다음은 예입니다.

//Model
function Book(name, isbn) {
    this.name = name;
    this.isbn = isbn;
}

function BookCollection(books) {
    this.books = books;
}

BookCollection.prototype.addBook = function (book) {
    this.books.push(book);
    $.publish('book-added', book);
    return book;
}

BookCollection.prototype.removeBook = function (book) {
   var removed;
   if (typeof book === 'number') {
       removed = this.books.splice(book, 1);
   }
   for (var i = 0; i < this.books.length; i += 1) {
      if (this.books[i] === book) {
          removed = this.books.splice(i, 1);
      }
   }
   $.publish('book-removed', removed);
   return removed;
}

//View
var BookListView = (function () {

   function removeBook(book) {
      $('#' + book.isbn).remove();
   }

   function addBook(book) {
      $('#bookList').append('<div id="' + book.isbn + '">' + book.name + '</div>');
   }

   return {
      init: function () {
         $.subscribe('book-removed', removeBook);
         $.subscribe('book-aded', addBook);
      }
   }
}());

다른 예시. MV * 접근 방식이 마음에 들지 않으면 약간 다른 것을 사용할 수 있습니다 (다음에 설명 할 것과 마지막에 언급 한 것 사이에 교차점이 있습니다). 애플리케이션을 다른 모듈로 구성하기 만하면됩니다. 예를 들어 Twitter를보십시오.

Twitter 모듈

인터페이스를 보면 단순히 다른 상자가 있습니다. 각 상자를 다른 모듈로 생각할 수 있습니다. 예를 들어 트윗을 게시 할 수 있습니다. 이 작업을 수행하려면 몇 가지 모듈을 업데이트해야합니다. 먼저 프로필 데이터 (왼쪽 상단 상자)를 업데이트해야하지만 타임 라인도 업데이트해야합니다. 물론 두 모듈에 대한 참조를 유지하고 공개 인터페이스를 사용하여 개별적으로 업데이트 할 수 있지만 이벤트를 게시하는 것이 더 쉽고 (더 좋습니다). 이렇게하면 결합이 느슨해져 애플리케이션을 더 쉽게 수정할 수 있습니다. 새 트윗에 의존하는 새 모듈을 개발하는 경우 “publish-tweet”이벤트를 구독하고 처리 할 수 ​​있습니다. 이 접근 방식은 매우 유용하며 애플리케이션을 매우 분리 할 수 ​​있습니다. 모듈을 매우 쉽게 재사용 할 수 있습니다.

다음은 마지막 접근 방식의 기본 예입니다 (이는 원본 트위터 코드가 아니라 제가 작성한 샘플 일뿐입니다).

var Twitter.Timeline = (function () {
   var tweets = [];
   function publishTweet(tweet) {
      tweets.push(tweet);
      //publishing the tweet
   };
   return {
      init: function () {
         $.subscribe('tweet-posted', function (data) {
             publishTweet(data);
         });
      }
   };
}());


var Twitter.TweetPoster = (function () {
   return {
       init: function () {
           $('#postTweet').bind('click', function () {
               var tweet = $('#tweetInput').val();
               $.publish('tweet-posted', tweet);
           });
       }
   };
}());

이 접근 방식에 대해 Nicholas Zakas 의 훌륭한 강연이 있습니다. MV * 접근 방식의 경우 내가 아는 최고의 기사와 책은 Addy Osmani 가 출판했습니다 .

단점 : 발행 / 구독의 과도한 사용에주의해야합니다. 수백 개의 이벤트가있는 경우 모든 이벤트를 관리하는 것이 매우 혼란 스러울 수 있습니다. 네임 스페이스를 사용하지 않거나 올바른 방식으로 사용하지 않는 경우에도 충돌이 발생할 수 있습니다. 게시 / 구독과 매우 유사한 Mediator의 고급 구현은 여기 https://github.com/ajacksified/Mediator.js 에서 찾을 수 있습니다 . 물론 중단 될 수있는 이벤트 “버블 링”과 같은 네임 스페이스 및 기능이 있습니다. 게시 / 구독의 또 다른 단점은 하드 단위 테스트입니다. 모듈에서 다른 기능을 분리하고 독립적으로 테스트하기가 어려울 수 있습니다.


답변

주요 목표는 코드 간의 결합을 줄이는 것입니다. 다소 이벤트 기반 사고 방식이지만 “이벤트”는 특정 대상에 묶여 있지 않습니다.

JavaScript와 비슷한 의사 코드로 아래에 큰 예제를 작성하겠습니다.

Radio 클래스와 Relay 클래스가 있다고 가정 해 보겠습니다.

class Relay {
    function RelaySignal(signal) {
        //do something we don't care about right now
    }
}

class Radio {
    function ReceiveSignal(signal) {
        //how do I send this signal to other relays?
    }
}

라디오가 신호를 수신 할 때마다 우리는 여러 릴레이가 어떤 방식 으로든 메시지를 릴레이하기를 원합니다. 릴레이의 수와 유형은 다를 수 있습니다. 다음과 같이 할 수 있습니다.

class Radio {
    var relayList = [];

    function AddRelay(relay) {
        relayList.add(relay);
    }

    function ReceiveSignal(signal) {
        for(relay in relayList) {
            relay.Relay(signal);
        }
    }

}

이것은 잘 작동합니다. 그러나 이제 다른 구성 요소가 Radio 클래스가 수신하는 신호, 즉 Speakers의 일부를 취하기를 원한다고 상상해보십시오.

(비유가 최고 수준이 아니라면 죄송합니다 …)

class Speakers {
    function PlaySignal(signal) {
        //do something with the signal to create sounds
    }
}

패턴을 다시 반복 할 수 있습니다.

class Radio {
    var relayList = [];
    var speakerList = [];

    function AddRelay(relay) {
        relayList.add(relay);
    }

    function AddSpeaker(speaker) {
        speakerList.add(speaker)
    }

    function ReceiveSignal(signal) {

        for(relay in relayList) {
            relay.Relay(signal);
        }

        for(speaker in speakerList) {
            speaker.PlaySignal(signal);
        }

    }

}

“SignalListener”와 같은 인터페이스를 만들어서 더 좋게 만들 수 있습니다. 따라서 Radio 클래스에 하나의 목록 만 필요하고 신호를 듣고 자하는 객체에 대해 항상 동일한 함수를 호출 할 수 있습니다. 그러나 그것은 우리가 결정한 인터페이스 / 기본 클래스 / 등과 Radio 클래스 사이에 여전히 결합을 생성합니다. 기본적으로 Radio, Signal 또는 Relay 클래스 중 하나를 변경할 때마다 다른 두 클래스에 어떻게 영향을 미칠 수 있는지 생각해야합니다.

이제 다른 것을 시도해 봅시다. RadioMast라는 네 번째 클래스를 만들어 보겠습니다.

class RadioMast {

    var receivers = [];

    //this is the "subscribe"
    function RegisterReceivers(signaltype, receiverMethod) {
        //if no list for this type of signal exits, create it
        if(receivers[signaltype] == null) {
            receivers[signaltype] = [];
        }
        //add a subscriber to this signal type
        receivers[signaltype].add(receiverMethod);
    }

    //this is the "publish"
    function Broadcast(signaltype, signal) {
        //loop through all receivers for this type of signal
        //and call them with the signal
        for(receiverMethod in receivers[signaltype]) {
            receiverMethod(signal);
        }
    }
}

이제 우리는 우리가 알고 있는 패턴 을 가지고 있으며, 다음과 같은 한 어떤 수와 유형의 클래스에도 사용할 수 있습니다.

  • RadioMast (모든 메시지 전달을 처리하는 클래스)를 알고 있습니다.
  • 메시지 보내기 / 받기를위한 메서드 서명을 알고 있습니다.

그래서 우리는 Radio 클래스를 최종적이고 간단한 형태로 변경합니다.

class Radio {
    function ReceiveSignal(signal) {
        RadioMast.Broadcast("specialradiosignal", signal);
    }
}

그리고 우리는 이러한 유형의 신호에 대해 RadioMast의 수신기 목록에 스피커와 릴레이를 추가합니다.

RadioMast.RegisterReceivers("specialradiosignal", speakers.PlaySignal);
RadioMast.RegisterReceivers("specialradiosignal", relay.RelaySignal);

이제 Speakers 및 Relay 클래스는 신호를 수신 할 수있는 메서드가 있고 게시자 인 Radio 클래스가 신호를 게시하는 RadioMast를 알고 있다는 점을 제외하고는 아무것도 알지 못합니다. 이것이 발행 / 구독과 같은 메시지 전달 시스템을 사용하는 지점입니다.


답변

다른 답변은 패턴이 어떻게 작동하는지 보여주는 데 큰 역할을했습니다. 나는 최근에이 패턴으로 작업을 해왔 기 때문에 ” 이전 방식에 무엇이 잘못 되었는가? ” 라는 묵시적인 질문에 답하고 싶었습니다 .

우리가 경제 게시판에 가입했다고 상상해보십시오. 이 게시판에는 ” 다우 존스를 200 포인트 낮추기 “라는 제목이 게시 되어 있습니다. 그것은 이상하고 다소 무책임한 메시지가 될 것입니다. 그러나 ” Enron이 오늘 아침 11 장 파산 보호를 위해 신청했습니다 “라고 게시 했다면 이것은 더 유용한 메시지입니다. 이 메시지로 인해 Dow Jones가 200 포인트 하락할 수 있지만 이는 또 다른 문제입니다.

명령을 보내는 것과 방금 일어난 일에 대해 조언하는 것에는 차이가 있습니다. 이를 염두에두고 지금은 핸들러를 무시하고 pub / sub 패턴의 원래 버전을 사용합니다.

$.subscribe('iquery/action/remove-order', removeOrder);

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    $.publish('iquery/action/remove-order', $(this).parents('form:first').find('div.order'));
});

사용자 행동 (클릭)과 시스템 반응 (주문 제거) 사이에는 이미 암시 된 강력한 결합이 있습니다. 귀하의 예에서 효과적으로 행동은 명령을 내리고 있습니다. 이 버전을 고려하십시오.

$.subscribe('iquery/action/remove-order-requested', handleRemoveOrderRequest);

$container.on('click', '.remove_order', function(event) {
    event.preventDefault();
    $.publish('iquery/action/remove-order-requested', $(this).parents('form:first').find('div.order'));
});

이제 핸들러는 발생한 관심있는 일에 응답하지만 주문을 제거 할 의무는 없습니다. 실제로 핸들러는 주문 제거와 직접적으로 관련되지는 않지만 호출 작업과 관련이있을 수있는 모든 종류의 작업을 수행 할 수 있습니다. 예를 들면 :

handleRemoveOrderRequest = function(e, orders) {
    logAction(e, "remove order requested");
    if( !isUserLoggedIn()) {
        adviseUser("You need to be logged in to remove orders");
    } else if (isOkToRemoveOrders(orders)) {
        orders.last().remove();
        adviseUser("Your last order has been removed");
        logAction(e, "order removed OK");
    } else {
        adviseUser("Your order was not removed");
        logAction(e, "order not removed");
    }
    remindUserToFloss();
    increaseProgrammerBrowniePoints();
    //etc...
}

명령과 알림을 구분하는 것은이 패턴 인 IMO를 사용하는 데 유용한 구분입니다.


답변

메서드 / 함수 호출을 하드 코딩 할 필요가 없도록 누가 수신하는지 신경 쓰지 않고 이벤트를 게시하면됩니다. 이렇게하면 게시자가 구독자로부터 독립되어 애플리케이션의 서로 다른 두 부분 간의 종속성 (또는 원하는 용어에 관계없이 결합)이 줄어 듭니다.

wikipedia에서 언급 한 커플 링의 단점은 다음과 같습니다.

밀접하게 결합 된 시스템은 종종 단점으로 간주되는 다음과 같은 발달 특성을 나타내는 경향이 있습니다.

  1. 한 모듈의 변경은 일반적으로 다른 모듈의 변경으로 인한 파급 효과를 강제합니다.
  2. 모듈 조립은 모듈 간 종속성 증가로 인해 더 많은 노력 및 / 또는 시간이 필요할 수 있습니다.
  3. 특정 모듈은 종속 모듈을 포함해야하므로 재사용 및 / 또는 테스트가 더 어려울 수 있습니다.

비즈니스 데이터를 캡슐화하는 객체와 같은 것을 고려하십시오. 연령이 설정 될 때마다 페이지를 업데이트하는 하드 코딩 된 메서드 호출이 있습니다.

var person = {
    name: "John",
    age: 23,

    setAge: function( age ) {
        this.age = age;
        showAge( age );
    }
};

//Different module

function showAge( age ) {
    $("#age").text( age );
}

이제 showAge함수를 포함하지 않고 사람 객체를 테스트 할 수 없습니다 . 또한 다른 GUI 모듈에서도 나이를 표시해야하는 경우 해당 메서드 호출을에서 하드 코딩해야
.setAge하며 이제 person 개체에 관련되지 않은 2 개의 모듈에 대한 종속성이 있습니다. 또한 이러한 호출이 수행되고 동일한 파일에 있지 않을 때 유지 관리하기가 어렵습니다.

물론 동일한 모듈 내에서 직접 메서드를 호출 할 수 있습니다. 그러나 비즈니스 데이터와 피상적 인 GUI 동작은 합리적인 표준에 따라 동일한 모듈에 상주해서는 안됩니다.


답변

PubSub 구현은 일반적으로 다음 위치에서 볼 수 있습니다.

  1. 이벤트 버스의 도움으로 통신하는 여러 포틀릿이있는 구현과 같은 포틀릿이 있습니다. 이것은 aync 아키텍처에서 생성하는 데 도움이됩니다.
  2. 긴밀한 결합으로 손상된 시스템에서 pubsub는 다양한 모듈 간의 통신을 돕는 메커니즘입니다.

예제 코드-

var pubSub = {};
(function(q) {

  var messages = [];

  q.subscribe = function(message, fn) {
    if (!messages[message]) {
      messages[message] = [];
    }
    messages[message].push(fn);
  }

  q.publish = function(message) {
    /* fetch all the subscribers and execute*/
    if (!messages[message]) {
      return false;
    } else {
      for (var message in messages) {
        for (var idx = 0; idx < messages[message].length; idx++) {
          if (messages[message][idx])
            messages[message][idx]();
        }
      }
    }
  }
})(pubSub);

pubSub.subscribe("event-A", function() {
  console.log('this is A');
});

pubSub.subscribe("event-A", function() {
  console.log('booyeah A');
});

pubSub.publish("event-A"); //executes the methods.


답변

“발행 / 구독의 다양한면” 이라는 논문 은 좋은 읽기이며 그들이 강조하는 한 가지는 세 가지 “차원”에서 분리하는 것입니다. 여기에 내 조잡한 요약이 있지만 논문도 참조하십시오.

  1. 공간 분리.상호 작용하는 당사자는 서로를 알 필요가 없습니다. 게시자는 누가 듣고 있는지, 얼마나 많은 사람이 듣고 있는지 또는 이벤트에서 무엇을하고 있는지 알지 못합니다. 구독자는 이러한 이벤트를 누가 제작하는지, 얼마나 많은 제작자가 있는지 등을 모릅니다.
  2. 시간 분리. 상호 작용하는 당사자는 상호 작용 중에 동시에 활성화 될 필요가 없습니다. 예를 들어 게시자가 일부 이벤트를 게시하는 동안 구독자의 연결이 끊어 질 수 있지만 온라인 상태가되면 이에 반응 할 수 있습니다.
  3. 동기화 디커플링. 게시자는 이벤트를 생성하는 동안 차단되지 않으며 구독자는 구독 한 이벤트가 도착할 때마다 콜백을 통해 비동기 적으로 알림을받을 수 있습니다.

답변

간단한 대답
원래 질문은 간단한 대답을 찾고있었습니다. 여기 내 시도가 있습니다.

자바 스크립트는 코드 객체가 자체 이벤트를 생성하는 메커니즘을 제공하지 않습니다. 따라서 일종의 이벤트 메커니즘이 필요합니다. 게시 / 구독 패턴은 이러한 요구에 응답 할 것이며 자신의 요구에 가장 적합한 메커니즘을 선택하는 것은 사용자에게 달려 있습니다.

이제 pub / sub 패턴이 필요하다는 것을 알 수 있습니다. 그러면 pub / sub 이벤트를 처리하는 방법과 다르게 DOM 이벤트를 처리해야합니까? 복잡성을 줄이고 SoC (관심 분리)와 같은 기타 개념을 줄이기 위해 모든 것이 균일 한 이점을 확인할 수 있습니다.

따라서 역설적이게도 코드가 많을수록 우려 사항이 더 잘 분리되어 매우 복잡한 웹 페이지까지 확장됩니다.

나는 누군가가 자세히 설명하지 않고 충분히 좋은 토론이라고 생각하기를 바랍니다.