본문 바로가기

FRONT-END/JAVASCRIPT

자바스크립트 디자인 패턴 #4. Decorator 패턴

반응형

Decorator 패턴

데커레이터 패턴

호출 대상이 되는 객체에 추가 기능들을 자유롭게 추가하는 패턴이다.


데커레이터 패턴을 유용하게 이용할 수 있는 곳은 검증도구이다.

인적사항을 입력하는 양식들은 일반적으로 입력값마다 검증하는 로직이 필욯다.

이런 값을 서버에서 검증해도 되지만, 서버의 트래픽을 줄이기 위해 웹페이지에서 각각의 값을 자바스크립트로 사전 검증하는 것이 일반적이다.

이럴 때 각각의 입력값을 검증하는 방법이 다르므로 많은 웹 페이지는 폼을 제출하는 이벤트에서 일일이 검증하는 경우가 많다.


데커레이터 패턴을 이용하면 웹페이지마다 일일이 코딩하는 것이 아니라 검증 도구를 모듈화해서 사용할 수 있다.

<!DOCTYPE html>

<html lang="en">

 <head>

   <meta charset="utf-8">

   <title>hello webpack</title>

 </head>

 <body>

<form id="personalInformation">

<label>First Name: <input type="text" class="validate" data-validate-rules="required alphabet" name="firstName"/></label><br/>

<label>Last Name: <input type="text" class="validate" data-validate-rules="required alphabet" name="lastName"/></label><br/>

<label>Age: <input type="text" class="validate" data-validate-rules="required number" name="age"/></label><br/>

<label>Gender: <select class="validate" data-validate-rules="required">

<option>Male</option>

<option>Female</option>

</select></label>

<input type="submit"/>

</form>

 <script>

(function(){

var formPersonalInformation = document.getElementById("personalInformation"),

validator = new Validator(formPersonalInformation);


function Validator(form) {

this.validatingForm = form;

form.addEventListener("submit",function(){

if(!validator.validate(this)){

        console.log("test");

event.preventDefault();

event.returnValue = false;

return false;

}

alert("Success to validate");

      event.preventDefault();

return false;

});

}


Validator.prototype.ruleSet = {};

Validator.prototype.decorate = function (ruleName, ruleFunction){

this.ruleSet[ruleName] = ruleFunction;

}


  Validator.prototype.validate = function (form) {

    var validatingForm = form || this.validatingForm,

      inputs = validatingForm.getElementsByClassName("validate"),

      length = inputs.length,

      i, j,

      input,

      checkRules,

      rule,

      ruleLength;

      console.log(length);

    for (var i = 0; i < length; i++) {

      input = inputs[i];

      console.log(input);

      if(input.dataset.validateRules){

        checkRules = input.dataset.validateRules.split(" ");

        ruleLength = checkRules.length;

        console.log(ruleLength);

        for (var j = 0; j < ruleLength; j++) {

            rule = checkRules[j];

            console.log(rule);

          if(this.ruleSet.hasOwnProperty(rule)){

            if(!this.ruleSet[rule].call(null, input)){

              console.log(false);

              return false;

            }

          }

        }

      }

    }

  };


  validator.decorate("required", function(input){

    if(!input.value){

        alert(input.name + " is required");

        return false;

    }

    return true;

  });


  validator.decorate("alphabet",function(input){

    var regex = /^[a-zA-Z\s]*$/;

    if(!regex.test(input.value)){

      alert(input.name + " has to be only alphabet");

      return false;

    }

    return true;

  });


  validator.decorate("number",function(input){

    var regex = /^[0-9]*$/;

    if(!regex.test(input.value)){

      alert(input.name + " has to be only numbers");

      return false;

    }

    return true;

  });


}());

 </script>

 </body>

</html>

<input> 요소는 검증하기 위한 대상으로 표시하기 위해 각가의 속성으로 class="validate"를 설정하였다.

그리고 데커레이터 패턴을 이용한 검증 객체가 어떠한 검증규칙(rule)을 이용할 것인지구분하기 위해 DOM의 dataset 속성을 추가하였다.

각 data-validate-rules 속성에 현재 <input>이나 <select>요소에 어떠한 검증 규칙을 적용할 것인지 순서대로 나열하고 있다.

이름은 필수요소이므로 "required"와 문자열입력가능 "alphabet"을 함께 속성으로 정의하고 있다.

나이는 <input> 요소에는 숫자만 들어올 수 있도록 "number" 라는 속성으로 정의하고 있다.


검증도구를 보면 Validator라는 클래스를 정의하여 prototype에 decorate()함수와 ruleSet 변수를 추가해서 검증에 사용할 규칙을 관리하고 추가할 수 있도록 하고,

검증 규칙의 종류는 decorate() 함수를 통해서 추가할수 있도록 하였다.

"required"와 "alphabet", 그리고 "number" 세가지 규칙을 추가하고 있다.


<input> 태그에서는 이러한 검증 규칙을 data-validate-rules속엇에 정의하도록 하였다.

나중에 폼이 제출될 때 해당 폼 안에 class="validate"로 표시된 영역 양식을 가져와서 생성한 Validator 클래스 객체안에서 추가한 검증 규칙을 기반으로 검증을 수행한다.


데커레이터 패턴을 이용해서 검증 모듈을 만들어두면 입력양식이 여러종류가 있는 페이지에서는 공통 모듈로 검증도구를 사용하고 HTML에서 어떠한 검증 규칙을 사용할지만 명시하면 간단하게 확장할 수 있다.

데커레이터 패턴의 장점은 decorate()함수를 통해서 검증 규칙 이름과 검증 방식을 손쉽게 확장할 수 있다.


객체 기반 데커레이터 패턴

객체지향 개념을 자바스크립트에 적용할려고 할때 이용할 수 있다.,

즉 하나의 객체에 여러가지 기능들을 추가함으로써 기존의 객체에 추가로 꾸며진 객체를 만들어낼 수 있다.

(function(){

  function Computer(name) {

    this.name = name;

    this.price = 0;

    this.parts = [];

  }


  Computer.prototype = {

    showPrice: function(){

      console.log(this.name + " costs $"+this.price);

    },

    showParts: function(){

      var partString = "- Parts information\n",

          length = this.parts.length,

          i;


      for (var i = 0; i < length; i++) {

        partString += this.parts[i].name + ": $" + this.parts[i].price + "\n";

      }

      console.log(partString+ "\n -Total: $" + this.price);

    },

    decorate: function(part){

      this.price += part.price;

      this.parts.push(part);

    }

  };


  function ComputerDecorator(){

    this.decorateParts = {};

  }


  ComputerDecorator.prototype.decorateComputer = function(computer, partName){

    if(this.decorateParts.hasOwnProperty(partName)){

      computer.decorate(this.decorateParts[partName]);

      console.log("Decorating" +computer.name + " with "+ partName);

    }

    return computer;

  };


  ComputerDecorator.prototype.addDecoratePart = function(partName, price){

    this.decorateParts[partName] = {

      name: partName,

      price: price

    };

  };


  var computerDecorator = new ComputerDecorator();

  computerDecorator.addDecoratePart("CPU", 200);

  computerDecorator.addDecoratePart("8GB", 100);

  computerDecorator.addDecoratePart("4GB", 50);


  console.log("1. Home computer");

  var homeComputer = new Computer("Home computer");

  homeComputer = computerDecorator.decorateComputer(homeComputer, "CPU");

  homeComputer = computerDecorator.decorateComputer(homeComputer, "4GB");

  homeComputer.showPrice();

  homeComputer.showParts();

  console.log(homeComputer.parts);


  console.log("\n2. Work computer");

  var workComputer = new Computer("Work computer");

  workComputer = computerDecorator.decorateComputer(workComputer, "CPU");

  workComputer = computerDecorator.decorateComputer(workComputer, "8GB");

  workComputer.showPrice();

  workComputer.showParts();

  console.log(workComputer.parts);



}());

기본 Computer 클래스를 정의하여 현재 컴퓨터의 이름과 가격, 부품 목록을 저장하고, 가격과 각 부품의 세부내역을 출력하는 함수를 구현

decorate()함수로 부품을 추가하여 Computer 클래스 객체를 꾸밀수 있도록 하였다.

그다음에는 데커레이터 패턴의 주요 역학을 수행하는 ComputerDecorator 클래스를 생성하여 여기에 동적으로 컴퓨터를 구성할 부품과 가격 정보를 추가하여 관리하도록 하였다.


Computer와 ComputerDecorator 클래스를 모두 정의하고 나서, 먼저 ComputerDecorator 클래스 객체에 사용할 부품들의 이름과 가격을 입력하고 이후에 각각 Home Computer와 Work Computer 두가지  Computer 클래스 객체를 생성해서

꾸미도록 하였다.

각각의 컴퓨터는 ComputerDecorator를 통해서 부품을 추가하고 가격이 증가하게 된다.

Computer와 ComputerDecorator 처럼 전체적인 구조를 구축해 놓으면 이를 활용하고 상호작용하는 부분이 매우 간단해져서 다양하게 응용할 수 있다.


함수 데커레이터 패턴

함수에도 추가 기능을 수행하기 위한 데커레이터 패턴을 적용

특히 프락시 패턴에 살펴봤던 래퍼 기능을 데커레이터 패턴과 함께 사용하면, 해당 함수가 호출되기 전에 여러가지 함수가 호출될수 있도록 응용할 수 있다.

이렇게 응용할 수 있는 상황은 바로 각 함수의 모니터링 기능을 넣고자 할 때이다.

(function(){

  var monitorTool,

      car,

      wrapperFunction;


  monitorTool = (function(){

    var functionSequence = [];

    return {

      decorate: function(name, func){

        functionSequence.push({

          name: name,

          func: func

        });

      },

      monitor: function(func){

        var length = functionSequence.length;

        console.log(functionSequence);

        for (var i = 0; i < length; i++) {

          functionSequence[i].func.apply(this, arguments);

        }

      }

    };

  })();


  monitorTool.decorate("before",function(func){

    console.log(func.name + "function has started at " + Date.now());

  });

  monitorTool.decorate("decorated", function(func){

    console.log(func.name + "function has decorated ");

    func.apply(this, Array.prototype.slice(arguments, 1));

  });

  monitorTool.decorate("after", function(func){

    console.log(func.name + "function has finished at " + Date.now());

  });


  wrapperFunction = monitorTool.monitor;

  console.log(wrapperFunction);


  car = {

    beep: function beep(){

      alert("BEEP");

    },

    brake: function brake(){

      alert("STOP");

    },

    accelerator: function accelerator(){

      alert("GO");

    }

  };



  function wrap(func, wrapper){

    return function(){

      var args = [func].concat(Array.prototype.slice.call(arguments));

      console.log(args);

      return wrapper.apply(this, args);

    };

  }


  function wrapObject(obj, wrapper) {

    var prop;

    for (prop in obj) {

      if (obj.hasOwnProperty(prop) && typeof obj[prop] === "function") {

        obj[prop] = wrap(obj[prop], wrapper);

      }

    }

  }


  wrapObject(car, wrapperFunction);


  console.log("A. car.accelerator() monitor");

  car.accelerator();



}());

데커레이터 패턴을 적용하여 래퍼 함수로 호출될 함수들을 monitorTool이라는 객체를 통해 순선대로 적용하고 있다.

앞선 프락시 패턴 예에서 래퍼를 설정하는 부분에 대한 수정은 거의 없이 wrapperFunction() 함수만 다시 정의하고 있다.

wrapperFunction() 함수가 호출될 때 monitorTool.monitor() 함수가 호출되어 데커레이터 패턴을 통해 정의한 함수들이 순서대로 호출된다.


데커레이터 패턴을 통해서 monitorTool 객체는 각 래퍼로 설정되는 함수들이 호출되면 첫번째 인자로 함수가 처음 호출되는 시간을 출력하고, 두번째로 프락시 패턴을 통해 인자로 넘어오는 원래 함수를 호출한다.

그 다음 함수 호출이 끝나면 마지막으로 함수 호출이 끝나느 시간을 출력한다.


실행결과로 각 함수에서 호출이 시작되고 끝나는데 걸리는 시간을 알수 있다.

이러한 모니터링 도구를 변형하여 사용자의 사용 패턴이나 각 자바스크립트 함수의 호출 시간을 기록하여 컴퓨팅 자원을 분산하거나 병목을 분산하여 사용자 경험을 높일 수 있는 자료로 활요할 수 있다.

데커레이터 패턴의 장점은 한번 체계를 만들어두면 허용되는 범위 안에서 확장하고 응용하는 것이 매우 편리하다.

하지만 반대로 첫 번째 체계를 만드는 것이 어려울 수도 있고, 허용가능한 범위를 결정하고 난 다음 , 특정 상황에 조금 다르게 처리하고 싶을 때 소스를 따라가면서 수정하는 것이 다소 어려울 수도 있다.

그럼에도 자바스크립트를 이용하여 객체지향 프로그래밍을 매우 유용하게 사용할수 있는 디자인 패턴이다.


출처-속깊은 자바스크립트 양성익 지음

반응형