JavaScript 디자인 패턴에 대한 종합 가이드
게시 됨: 2022-03-11훌륭한 JavaScript 개발자는 깨끗하고 건강하며 유지 관리 가능한 코드를 작성하기 위해 노력합니다. 고유하지만 반드시 고유한 솔루션이 필요한 것은 아닌 흥미로운 문제를 해결합니다. 이전에 처리했던 완전히 다른 문제의 솔루션과 유사한 코드를 작성하고 있는 자신을 발견했을 것입니다. 모를 수도 있지만 JavaScript 디자인 패턴 을 사용했습니다. 디자인 패턴은 소프트웨어 디자인에서 일반적으로 발생하는 문제에 대한 재사용 가능한 솔루션입니다.
모든 언어의 수명 동안 해당 언어 커뮤니티의 수많은 개발자가 재사용 가능한 솔루션을 만들고 테스트합니다. 이러한 솔루션은 최적화된 방식으로 코드를 작성하는 동시에 당면한 문제를 해결하는 데 도움이 되기 때문에 많은 개발자의 이러한 결합된 경험 덕분입니다.
디자인 패턴을 통해 얻을 수 있는 주요 이점은 다음과 같습니다.
- 검증된 솔루션입니다. 많은 개발자가 디자인 패턴을 자주 사용하기 때문에 효과가 있음을 확신할 수 있습니다. 뿐만 아니라 여러 번 수정되었으며 최적화가 구현되었음을 확신할 수 있습니다.
- 쉽게 재사용할 수 있습니다. 디자인 패턴은 특정 문제에 연결되지 않기 때문에 여러 특정 문제를 해결하기 위해 수정할 수 있는 재사용 가능한 솔루션을 문서화합니다.
- 표현력이 뛰어납니다. 디자인 패턴은 대규모 솔루션을 매우 우아하게 설명할 수 있습니다.
- 의사 소통이 쉬워집니다. 개발자가 디자인 패턴에 익숙하면 주어진 문제에 대한 잠재적인 솔루션에 대해 더 쉽게 의사 소통할 수 있습니다.
- 코드를 리팩토링할 필요가 없습니다. 애플리케이션이 디자인 패턴을 염두에 두고 작성된 경우 주어진 문제에 올바른 디자인 패턴을 적용하는 것이 이미 최적이기 때문에 나중에 코드를 리팩토링할 필요가 없는 경우가 많습니다. 해결책.
- 코드베이스의 크기를 줄입니다. 디자인 패턴은 일반적으로 우아하고 최적의 솔루션이기 때문에 일반적으로 다른 솔루션보다 더 적은 코드가 필요합니다.
이 시점에서 뛰어들 준비가 되었다는 것을 알고 있지만 디자인 패턴에 대해 모두 배우기 전에 몇 가지 JavaScript 기본 사항을 검토해 보겠습니다.
JavaScript의 간략한 역사
JavaScript는 오늘날 웹 개발을 위한 가장 인기 있는 프로그래밍 언어 중 하나입니다. 처음에는 초기 웹 브라우저 중 하나에 대해 클라이언트 측 스크립팅 언어로 알려진 다양한 표시 HTML 요소에 대한 일종의 "접착제"로 만들어졌습니다. Netscape Navigator라고 하는 이 도구는 당시에는 정적 HTML만 표시할 수 있었습니다. 짐작하시겠지만, 이러한 스크립팅 언어의 아이디어는 Netscape Communications(오늘날 Mozilla), Microsoft 등과 같은 브라우저 개발 업계의 거대 기업들 간의 브라우저 전쟁으로 이어졌습니다.
각 대기업은 이 스크립팅 언어의 자체 구현을 추진하기를 원했기 때문에 Netscape는 JavaScript(실제로 Brendan Eich가 만들었습니다)를 만들고 Microsoft는 JScript를 만드는 등의 작업을 수행했습니다. 이미지에서 볼 수 있듯이 이러한 구현 간의 차이가 커서 웹 페이지와 함께 제공되는 가장 많이 본 스티커를 사용하여 웹 브라우저 개발이 브라우저별로 이루어졌습니다. 곧 개발 프로세스를 통합하고 웹 페이지 생성을 단순화하는 크로스 브라우저 솔루션인 표준이 필요하다는 것이 분명해졌습니다. 그들이 생각해 낸 것은 ECMAScript라고 합니다.
ECMAScript는 모든 최신 브라우저가 지원하려고 하는 표준화된 스크립팅 언어 사양이며 ECMAScript의 여러 구현(방언이라고 할 수 있음)이 있습니다. 가장 인기 있는 것은 이 기사의 주제인 JavaScript입니다. ECMAScript는 초기 릴리스 이후로 많은 중요한 사항을 표준화했으며 세부 사항에 더 관심이 있는 사용자를 위해 Wikipedia에서 ECMAScript의 각 버전에 대한 표준화된 항목의 자세한 목록을 볼 수 있습니다. ECMAScript 버전 6(ES6) 이상에 대한 브라우저 지원은 여전히 불완전하며 완전히 지원하려면 ES5로 변환해야 합니다.
자바스크립트란?
이 기사의 내용을 완전히 이해하기 위해 JavaScript 디자인 패턴에 뛰어 들기 전에 알아야 할 몇 가지 매우 중요한 언어 특성을 소개하겠습니다. 누군가가 "JavaScript가 무엇입니까?"라고 묻는다면 다음 라인의 어딘가에 대답할 수 있습니다.
JavaScript는 웹 페이지용 스크립팅 언어로 가장 일반적으로 알려진 일급 기능을 갖춘 경량의 해석된 객체 지향 프로그래밍 언어입니다.
앞서 언급한 정의는 JavaScript 코드가 C++ 및 Java와 같은 인기 있는 언어와 유사한 구문으로 메모리 사용량이 적고 구현하기 쉽고 배우기 쉽다는 것을 의미합니다. 이것은 스크립팅 언어이므로 코드가 컴파일되는 대신 해석됩니다. 절차적, 객체 지향 및 기능적 프로그래밍 스타일을 지원하므로 개발자에게 매우 유연합니다.
지금까지 우리는 다른 많은 언어처럼 들리는 모든 특성을 살펴보았으므로 다른 언어와 관련하여 JavaScript에 대해 특정한 것이 무엇인지 살펴보겠습니다. 나는 몇 가지 특성을 나열하고 그들이 특별한 관심을 받아야 하는 이유를 설명하기 위해 최선을 다할 것입니다.
JavaScript는 일류 함수를 지원합니다
이 특성은 제가 C/C++ 배경 지식을 가지고 있었기 때문에 JavaScript를 막 시작했을 때 이해하기 어려웠습니다. JavaScript는 함수를 일급 시민으로 취급합니다. 즉, 다른 변수와 마찬가지로 함수를 다른 함수에 매개변수로 전달할 수 있습니다.
// we send in the function as an argument to be // executed from inside the calling function function performOperation(a, b, cb) { var c = a + b; cb(c); } performOperation(2, 3, function(result) { // prints out 5 console.log("The result of the operation is " + result); })JavaScript는 프로토타입 기반입니다.
다른 많은 객체 지향 언어의 경우와 마찬가지로 JavaScript는 객체를 지원하며 객체에 대해 생각할 때 가장 먼저 떠오르는 용어 중 하나는 클래스와 상속입니다. 이것은 언어가 일반 언어 형식의 클래스를 지원하지 않고 프로토타입 기반 또는 인스턴스 기반 상속이라는 것을 사용하기 때문에 약간 까다로워지는 부분입니다.
ES6에서 공식 용어 클래스 가 도입된 것은 바로 지금입니다. 이는 브라우저가 여전히 이를 지원하지 않는다는 것을 의미합니다(작성하는 시점에서 완전히 지원되는 마지막 ECMAScript 버전은 5.1임). 그러나 "클래스"라는 용어가 JavaScript에 도입되었지만 여전히 내부에서 프로토타입 기반 상속을 사용한다는 점에 유의하는 것이 중요합니다.
프로토타입 기반 프로그래밍은 프로토타입 역할을 하는 위임을 통해 기존 객체를 재사용하는 프로세스를 통해 동작 재사용(상속이라고 함)이 수행되는 객체 지향 프로그래밍 스타일입니다. 이 특성은 많은 JavaScript 디자인 패턴에서 사용되기 때문에 기사의 디자인 패턴 섹션에 도달하면 이에 대해 더 자세히 알아볼 것입니다.
자바스크립트 이벤트 루프
JavaScript 작업 경험이 있다면 콜백 함수 라는 용어에 익숙할 것입니다. 용어에 익숙하지 않은 사람들을 위해 콜백 함수는 다른 함수에 매개변수로 전송된 함수(JavaScript는 함수를 1급 시민으로 취급함)이며 이벤트가 발생한 후에 실행됩니다. 이것은 일반적으로 마우스 클릭이나 키보드 버튼 누름과 같은 이벤트를 구독하는 데 사용됩니다.
리스너가 연결된 이벤트가 발생할 때마다(그렇지 않으면 이벤트가 손실됨) FIFO 방식(선입선출)으로 동기적으로 처리되는 메시지 대기열로 메시지가 전송됩니다. ). 이것을 이벤트 루프 라고 합니다.
큐의 각 메시지에는 연관된 기능이 있습니다. 메시지가 대기열에서 제거되면 런타임은 다른 메시지를 처리하기 전에 함수를 완전히 실행합니다. 즉, 함수에 다른 함수 호출이 포함되어 있으면 대기열에서 새 메시지를 처리하기 전에 모두 수행됩니다. 이것을 완료까지 실행이라고 합니다.
while (queue.waitForMessage()) { queue.processNextMessage(); } queue.waitForMessage() 는 동기식으로 새 메시지를 기다립니다. 처리 중인 각 메시지에는 자체 스택이 있으며 스택이 비워질 때까지 처리됩니다. 완료되면 대기열에서 새 메시지가 처리됩니다(있는 경우).
JavaScript가 비차단적이라는 말을 들었을 수도 있습니다. 즉, 비동기식 작업이 수행 중일 때 프로그램이 메인을 차단하지 않고 비동기식 작업이 완료될 때까지 기다리는 동안 사용자 입력 수신과 같은 다른 작업을 처리할 수 있음을 의미합니다. 실행 스레드. 이것은 JavaScript의 매우 유용한 속성이며 이 주제에 대해서만 전체 기사를 작성할 수 있습니다. 그러나 이 문서의 범위를 벗어납니다.
디자인 패턴이란?
앞서 말했듯이 디자인 패턴은 소프트웨어 디자인에서 일반적으로 발생하는 문제에 대한 재사용 가능한 솔루션입니다. 디자인 패턴의 몇 가지 범주를 살펴보겠습니다.
프로토 패턴
패턴을 만드는 방법은 무엇입니까? 일반적으로 발생하는 문제를 인식하고 이 문제에 대한 고유한 솔루션을 가지고 있다고 가정해 보겠습니다. 이 솔루션은 전 세계적으로 인정되고 문서화되지 않았습니다. 이 문제가 발생할 때마다 이 솔루션을 사용하고 재사용할 수 있고 개발자 커뮤니티에서 이점을 얻을 수 있다고 생각합니다.
바로 패턴이 되나요? 다행히도. 종종 좋은 코드 작성 습관을 가지고 있고 패턴처럼 보이는 것을 실제로는 패턴이 아닌 것으로 착각하는 경우가 있습니다.
당신이 인식하고 있다고 생각하는 것이 실제로 디자인 패턴인지 어떻게 알 수 있습니까?
다른 개발자들의 의견을 듣고, 패턴 자체를 만드는 과정을 알고, 기존 패턴을 잘 알게 됩니다. 패턴이 본격적인 패턴이 되기까지 거쳐야 하는 단계가 있는데, 이를 프로토패턴이라고 합니다.
프로토 패턴은 패턴이 유용하고 올바른 결과를 제공하는 다양한 개발자 및 시나리오의 일정 기간 테스트를 통과 하면 패턴이 될 것입니다. 커뮤니티에서 완전한 패턴을 인식하도록 하기 위해 수행해야 할 작업과 문서의 양이 상당히 많습니다. 대부분은 이 기사의 범위를 벗어납니다.
안티 패턴
디자인 패턴은 좋은 습관을 나타내고 안티 패턴은 나쁜 습관을 나타냅니다.
안티 패턴의 예는 Object 클래스 프로토타입을 수정하는 것입니다. JavaScript의 거의 모든 객체는 Object 에서 상속하므로(JavaScript는 프로토타입 기반 상속을 사용함을 기억하십시오) 이 프로토타입을 변경한 시나리오를 상상해 보십시오. Object 프로토타입에 대한 변경 사항은 이 프로토타입에서 상속된 모든 객체에서 볼 수 있습니다. 이는 대부분의 JavaScript 객체일 것 입니다. 이것은 일어나기를 기다리고 있는 재앙입니다.
위에서 언급한 것과 유사한 또 다른 예는 소유하지 않은 개체를 수정하는 것입니다. 이에 대한 예는 응용 프로그램 전반에 걸쳐 많은 시나리오에서 사용되는 개체의 함수를 재정의하는 것입니다. 대규모 팀과 함께 작업하는 경우 이로 인해 발생할 혼란을 상상해 보십시오. 명명 충돌, 호환되지 않는 구현 및 유지 관리 악몽에 빠르게 직면할 것입니다.
모든 좋은 관행과 해결책에 대해 아는 것이 얼마나 유용한지와 마찬가지로 나쁜 것들에 대해서도 아는 것이 매우 중요합니다. 이런 식으로, 당신은 그들을 인식하고 실수를 미연에 방지할 수 있습니다.
디자인 패턴 분류
디자인 패턴은 여러 가지 방법으로 분류할 수 있지만 가장 널리 사용되는 것은 다음과 같습니다.
- 창조적인 디자인 패턴
- 구조적 디자인 패턴
- 행동 디자인 패턴
- 동시성 디자인 패턴
- 건축 디자인 패턴
창조적인 디자인 패턴
이러한 패턴은 기본 접근 방식에 비해 객체 생성을 최적화하는 객체 생성 메커니즘을 다룹니다. 객체 생성의 기본 형태는 설계 문제를 일으키거나 설계에 복잡성을 추가할 수 있습니다. 창조적인 디자인 패턴은 어떻게든 객체 생성을 제어함으로써 이 문제를 해결합니다. 이 카테고리에서 인기 있는 디자인 패턴은 다음과 같습니다.
- 공장 방식
- 추상 공장
- 빌더
- 원기
- 하나씩 일어나는 것
구조적 디자인 패턴
이러한 패턴은 개체 관계를 처리합니다. 그들은 시스템의 한 부분이 변경되더라도 전체 시스템이 함께 변경될 필요가 없도록 합니다. 이 범주에서 가장 인기 있는 패턴은 다음과 같습니다.
- 어댑터
- 다리
- 합성물
- 데코레이터
- 정면
- 플라이급
- 대리
행동 디자인 패턴
이러한 유형의 패턴은 시스템에서 서로 다른 개체 간의 통신을 인식, 구현 및 개선합니다. 시스템의 서로 다른 부분이 동기화된 정보를 갖도록 하는 데 도움이 됩니다. 이러한 패턴의 인기 있는 예는 다음과 같습니다.
- 책임의 사슬
- 명령
- 반복자
- 중재인
- 기념물
- 관찰자
- 상태
- 전략
- 방문객
동시성 디자인 패턴
이러한 유형의 디자인 패턴은 다중 스레드 프로그래밍 패러다임을 다룹니다. 인기있는 것들 중 일부는 다음과 같습니다:
- 활성 개체
- 핵반응
- 스케줄러
건축 디자인 패턴
건축적 목적으로 사용되는 디자인 패턴. 가장 유명한 것은 다음과 같습니다.
- MVC(모델-뷰-컨트롤러)
- MVP(모델-뷰-발표자)
- MVVM(모델-뷰-뷰 모델)
다음 섹션에서는 더 나은 이해를 위해 제공된 예제와 함께 앞서 언급한 디자인 패턴 중 일부를 자세히 살펴보겠습니다.
디자인 패턴 예시
각 디자인 패턴은 특정 유형의 문제에 대한 특정 유형의 솔루션을 나타냅니다. 항상 가장 잘 맞는 보편적인 패턴 세트는 없습니다. 우리는 특정 패턴이 언제 유용할 것인지 그리고 그것이 실제 가치를 제공할 것인지를 배워야 합니다. 가장 적합한 패턴과 시나리오에 익숙해지면 특정 패턴이 주어진 문제에 적합한지 여부를 쉽게 결정할 수 있습니다.
주어진 문제에 잘못된 패턴을 적용하면 불필요한 코드 복잡성, 성능에 대한 불필요한 오버헤드 또는 새로운 안티 패턴 생성과 같은 바람직하지 않은 결과를 초래할 수 있음을 기억하십시오.
디자인 패턴을 코드에 적용할 때 고려해야 할 중요한 사항입니다. 개인적으로 유용하다고 생각하는 디자인 패턴 중 일부를 살펴보고 모든 선임 JavaScript 개발자가 익숙해야 한다고 생각합니다.
생성자 패턴
고전적인 객체 지향 언어에 대해 생각할 때 생성자는 기본 및/또는 전송된 값 집합으로 객체를 초기화하는 클래스의 특수 함수입니다.
JavaScript에서 객체를 생성하는 일반적인 방법은 다음 세 가지입니다.
// either of the following ways can be used to create a new object var instance = {}; // or var instance = Object.create(Object.prototype); // or var instance = new Object();객체를 생성한 후 이 객체에 속성을 추가하는 네 가지 방법(ES3 이후)이 있습니다. 그것들은 다음과 같습니다:
// supported since ES3 // the dot notation instance.key = "A key's value"; // the square brackets notation instance["key"] = "A key's value"; // supported since ES5 // setting a single property using Object.defineProperty Object.defineProperty(instance, "key", { value: "A key's value", writable: true, enumerable: true, configurable: true }); // setting multiple properties using Object.defineProperties Object.defineProperties(instance, { "firstKey": { value: "First key's value", writable: true }, "secondKey": { value: "Second key's value", writable: false } });객체를 생성하는 가장 보편적인 방법은 중괄호이며, 속성을 추가하기 위해 점 표기법이나 대괄호를 사용합니다. JavaScript에 대한 경험이 있는 모든 사람이 사용했습니다.
이전에 JavaScript는 기본 클래스를 지원하지 않지만 함수 호출에 접두사를 붙인 "new" 키워드를 사용하여 생성자를 지원한다고 언급했습니다. 이렇게 하면 함수를 생성자로 사용하고 고전적인 언어 생성자와 같은 방식으로 속성을 초기화할 수 있습니다.
// we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; this.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode(); 그러나 여기에는 여전히 개선의 여지가 있습니다. 기억하시겠지만 앞서 JavaScript는 프로토타입 기반 상속을 사용한다고 언급했습니다. 이전 접근 방식의 문제는 writesCode 메서드가 Person 생성자의 각 인스턴스에 대해 재정의된다는 것입니다. 메서드를 함수 프로토타입으로 설정하여 이를 방지할 수 있습니다.
// we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; } // we extend the function's prototype Person.prototype.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode(); 이제 Person 생성자의 두 인스턴스 모두 writesCode() 메서드의 공유 인스턴스에 액세스할 수 있습니다.
모듈 패턴
특성에 관한 한 JavaScript는 놀라움을 멈추지 않습니다. JavaScript의 또 다른 독특한 점은(적어도 객체 지향 언어에 관한 한) JavaScript는 액세스 수정자를 지원하지 않는다는 것입니다. 기존 OOP 언어에서 사용자는 클래스를 정의하고 해당 구성원에 대한 액세스 권한을 결정합니다. 일반 형식의 JavaScript는 클래스나 액세스 수정자를 지원하지 않기 때문에 JavaScript 개발자는 필요할 때 이 동작을 모방하는 방법을 찾았습니다.
모듈 패턴 세부 사항으로 들어가기 전에 클로저의 개념에 대해 이야기합시다. 클로저 는 상위 함수가 닫힌 후에도 상위 범위에 액세스할 수 있는 함수입니다. 범위 지정을 통해 액세스 수정자의 동작을 모방하는 데 도움이 됩니다. 예를 통해 이것을 보여줍시다:
// we used an immediately invoked function expression // to create a private variable, counter var counterIncrementer = (function() { var counter = 0; return function() { return ++counter; }; })(); // prints out 1 console.log(counterIncrementer()); // prints out 2 console.log(counterIncrementer()); // prints out 3 console.log(counterIncrementer());보시다시피 IIFE를 사용하여 카운터 변수를 호출하고 닫았지만 여전히 증가시키는 자식 함수에서 액세스할 수 있는 함수에 연결했습니다. 함수 표현식 외부에서 카운터 변수에 액세스할 수 없기 때문에 범위 조작을 통해 비공개로 만들었습니다.
클로저를 사용하여 비공개 및 공개 부분이 있는 개체를 만들 수 있습니다. 이것을 모듈 이라고 하며 객체의 특정 부분을 숨기고 모듈 사용자에게 인터페이스만 노출하고자 할 때마다 매우 유용합니다. 이것을 예시로 보여드리겠습니다:
// through the use of a closure we expose an object // as a public API which manages the private objects array var collection = (function() { // private members var objects = []; // public members return { addObject: function(object) { objects.push(object); }, removeObject: function(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } }, getObjects: function() { return JSON.parse(JSON.stringify(objects)); } }; })(); collection.addObject("Bob"); collection.addObject("Alice"); collection.addObject("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(collection.getObjects()); collection.removeObject("Alice"); // prints ["Bob", "Franck"] console.log(collection.getObjects());이 패턴이 도입하는 가장 유용한 것은 객체의 private 부분과 public 부분의 명확한 분리입니다. 이것은 고전적인 객체 지향 배경에서 온 개발자와 매우 유사한 개념입니다.

그러나 모든 것이 그렇게 완벽하지는 않습니다. 멤버의 가시성을 변경하려면 공개 및 비공개 부분에 액세스하는 특성이 다르기 때문에 이 멤버를 사용한 모든 위치에서 코드를 수정해야 합니다. 또한 생성 후 개체에 추가된 메서드는 개체의 private 멤버에 액세스할 수 없습니다.
모듈 패턴 공개
이 패턴은 위에서 설명한 모듈 패턴을 개선한 것입니다. 주요 차이점은 모듈의 비공개 범위에 전체 개체 논리를 작성한 다음 익명 개체를 반환하여 공개하려는 부분을 단순히 노출한다는 것입니다. private 멤버를 해당 public 멤버에 매핑할 때 private 멤버의 이름을 변경할 수도 있습니다.
// we write the entire object logic as private members and // expose an anonymous object which maps members we wish to reveal // to their corresponding public members var namesCollection = (function() { // private members var objects = []; function addObject(object) { objects.push(object); } function removeObject(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } } function getObjects() { return JSON.parse(JSON.stringify(objects)); } // public members return { addName: addObject, removeName: removeObject, getNames: getObjects }; })(); namesCollection.addName("Bob"); namesCollection.addName("Alice"); namesCollection.addName("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(namesCollection.getNames()); namesCollection.removeName("Alice"); // prints ["Bob", "Franck"] console.log(namesCollection.getNames());공개 모듈 패턴은 모듈 패턴을 구현할 수 있는 최소한 세 가지 방법 중 하나입니다. 공개 모듈 패턴과 모듈 패턴의 다른 변형 간의 차이점은 주로 공개 멤버가 참조되는 방식에 있습니다. 결과적으로 표시 모듈 패턴은 사용 및 수정하기가 훨씬 쉽습니다. 그러나 상속 체인에서 RMP 개체를 프로토타입으로 사용하는 것과 같은 특정 시나리오에서는 취약한 것으로 판명될 수 있습니다. 문제가 되는 상황은 다음과 같습니다.
- 공개 함수를 참조하는 비공개 함수가 있는 경우 비공개 함수가 계속해서 함수의 비공개 구현을 참조하여 시스템에 버그를 도입하므로 공개 함수를 재정의할 수 없습니다.
- private 변수를 가리키는 public 멤버가 있고 모듈 외부에서 public 멤버를 재정의하려고 하면 다른 함수는 여전히 변수의 private 값을 참조하여 시스템에 버그가 발생합니다.
싱글톤 패턴
싱글톤 패턴은 정확히 하나의 클래스 인스턴스가 필요한 시나리오에서 사용됩니다. 예를 들어, 무언가에 대한 구성이 포함된 객체가 필요합니다. 이러한 경우 구성 개체가 시스템 어딘가에 필요할 때마다 새 개체를 만들 필요가 없습니다.
var singleton = (function() { // private singleton value which gets initialized only once var config; function initializeConfiguration(values){ this.randomNumber = Math.random(); values = values || {}; this.number = values.number || 5; this.size = values.size || 10; } // we export the centralized method for retrieving the singleton value return { getConfig: function(values) { // we initialize the singleton value only once if (config === undefined) { config = new initializeConfiguration(values); } // and return the same config value wherever it is asked for return config; } }; })(); var configObject = singleton.getConfig({ "size": 8 }); // prints number: 5, size: 8, randomNumber: someRandomDecimalValue console.log(configObject); var configObject1 = singleton.getConfig({ "number": 8 }); // prints number: 5, size: 8, randomNumber: same randomDecimalValue as in first config console.log(configObject1);예제에서 볼 수 있듯이 생성된 난수는 전송된 구성 값과 마찬가지로 항상 동일합니다.
싱글톤 값을 검색하기 위한 액세스 포인트는 매우 잘 알려진 단 하나의 액세스 포인트여야 한다는 점에 유의하는 것이 중요합니다. 이 패턴을 사용할 때의 단점은 테스트하기가 다소 어렵다는 것입니다.
관찰자 패턴
관찰자 패턴은 최적화된 방식으로 시스템의 이질적인 부분 간의 통신을 개선해야 하는 시나리오가 있을 때 매우 유용한 도구입니다. 객체 간의 느슨한 결합을 촉진합니다.
이 패턴에는 다양한 버전이 있지만 가장 기본적인 형태에서는 패턴의 두 가지 주요 부분이 있습니다. 첫 번째는 주체이고 두 번째는 관찰자입니다.
주체는 관찰자가 구독하는 특정 주제에 대한 모든 작업을 처리합니다. 이러한 작업은 관찰자를 특정 주제에 구독하고, 관찰자를 특정 주제에서 구독 취소하고, 이벤트가 게시될 때 관찰자에게 특정 주제에 대해 알립니다.
그러나 게시자/구독자 패턴이라고 하는 이 패턴의 변형이 있습니다. 이 패턴을 이 섹션에서 예로 사용하겠습니다. 기존 관찰자 패턴과 게시자/구독자 패턴의 주요 차이점은 게시자/구독자가 관찰자 패턴보다 훨씬 더 느슨한 결합을 촉진한다는 것입니다.
관찰자 패턴에서 주체는 구독된 관찰자에 대한 참조를 보유하고 개체 자체에서 직접 메서드를 호출하는 반면 게시자/구독자 패턴에서는 구독자와 게시자 간의 통신 브리지 역할을 하는 채널이 있습니다. 게시자는 이벤트를 발생시키고 해당 이벤트에 대해 전송된 콜백 함수를 실행하기만 하면 됩니다.
나는 게시자/구독자 패턴의 짧은 예를 표시할 것이지만 관심 있는 사람들을 위해 고전적인 관찰자 패턴 예는 온라인에서 쉽게 찾을 수 있습니다.
var publisherSubscriber = {}; // we send in a container object which will handle the subscriptions and publishings (function(container) { // the id represents a unique subscription id to a topic var id = 0; // we subscribe to a specific topic by sending in // a callback function to be executed on event firing container.subscribe = function(topic, f) { if (!(topic in container)) { container[topic] = []; } container[topic].push({ "id": ++id, "callback": f }); return id; } // each subscription has its own unique ID, which we use // to remove a subscriber from a certain topic container.unsubscribe = function(topic, id) { var subscribers = []; for (var subscriber of container[topic]) { if (subscriber.id !== id) { subscribers.push(subscriber); } } container[topic] = subscribers; } container.publish = function(topic, data) { for (var subscriber of container[topic]) { // when executing a callback, it is usually helpful to read // the documentation to know which arguments will be // passed to our callbacks by the object firing the event subscriber.callback(data); } } })(publisherSubscriber); var subscriptionID1 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Bob's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID2 = publisherSubscriber.subscribe("mouseHovered", function(data) { console.log("I am Bob's callback function for a hovered mouse event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID3 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Alice's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); // NOTE: after publishing an event with its data, all of the // subscribed callbacks will execute and will receive // a data object from the object firing the event // there are 3 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"}); // we unsubscribe from an event by removing the subscription ID publisherSubscriber.unsubscribe("mouseClicked", subscriptionID3); // there are 2 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"});이 디자인 패턴은 발생하는 단일 이벤트에 대해 여러 작업을 수행해야 하는 상황에서 유용합니다. 백엔드 서비스에 대해 여러 AJAX 호출을 수행한 다음 결과에 따라 다른 AJAX 호출을 수행해야 하는 시나리오가 있다고 상상해 보십시오. AJAX 호출을 서로 중첩해야 하므로 콜백 지옥으로 알려진 상황이 발생할 수 있습니다. 게시자/구독자 패턴을 사용하는 것이 훨씬 더 우아한 솔루션입니다.
이 패턴을 사용할 때의 단점은 시스템의 다양한 부분을 테스트하기 어렵다는 것입니다. 시스템의 구독 부분이 예상대로 작동하는지 여부를 알 수 있는 우아한 방법은 없습니다.
중재자 패턴
우리는 분리된 시스템에 대해 이야기할 때 매우 유용한 패턴을 간략하게 다룰 것입니다. 시스템의 여러 부분이 통신하고 조정해야 하는 시나리오가 있는 경우 아마도 좋은 솔루션은 중재자를 도입하는 것입니다.
중재자는 시스템의 이질적인 부분 간의 통신을 위한 중심점으로 사용되며 이들 사이의 워크플로를 처리하는 개체입니다. 이제 워크플로를 처리한다는 점을 강조하는 것이 중요합니다. 이것이 왜 중요한가?
게시자/구독자 패턴과 큰 유사점이 있기 때문입니다. 이 두 패턴은 모두 개체 간의 더 나은 통신을 구현하는 데 도움이 됩니다. 차이점은 무엇입니까?
차이점은 중재자가 워크플로를 처리하는 반면 게시자/구독자는 "발사 후 잊어버리는" 유형의 통신을 사용한다는 것입니다. 게시자/구독자는 단순히 이벤트 수집자입니다. 즉, 이벤트 발생을 처리하고 올바른 구독자에게 어떤 이벤트가 발생했는지 알릴 뿐입니다. 이벤트 수집기는 이벤트가 발생하면 어떤 일이 발생하는지 상관하지 않습니다. 이는 중재자의 경우가 아닙니다.
중재자의 좋은 예는 마법사 유형의 인터페이스입니다. 작업한 시스템에 대한 대규모 등록 프로세스가 있다고 가정해 보겠습니다. 종종 사용자에게 많은 정보가 필요한 경우 이를 여러 단계로 나누는 것이 좋습니다.
이렇게 하면 코드가 훨씬 더 깨끗해지고(유지 관리가 더 쉬움) 사용자는 등록을 완료하기 위해 요청되는 정보의 양에 압도되지 않습니다. 중재자는 각 사용자가 잠재적으로 고유한 등록 프로세스를 가질 수 있다는 사실로 인해 발생할 수 있는 다양한 워크플로를 고려하여 등록 단계를 처리하는 개체입니다.
이 디자인 패턴의 명백한 이점은 시스템의 서로 다른 부분 간의 향상된 통신이며, 이제 모두 중재자와 더 깨끗한 코드베이스를 통해 통신합니다.
단점은 이제 단일 실패 지점이 시스템에 도입되었다는 것입니다. 즉, 중재자가 실패하면 전체 시스템이 작동을 멈출 수 있습니다.
프로토타입 패턴
이 기사 전체에서 이미 언급했듯이 JavaScript는 기본 형식의 클래스를 지원하지 않습니다. 객체 간의 상속은 프로토타입 기반 프로그래밍을 사용하여 구현됩니다.
이는 생성 중인 다른 객체의 프로토타입 역할을 할 수 있는 객체를 생성할 수 있도록 합니다. 프로토타입 객체는 생성자가 생성하는 각 객체의 청사진으로 사용됩니다.
이전 섹션에서 이미 이에 대해 이야기했듯이 이 패턴을 사용하는 방법에 대한 간단한 예를 보여 드리겠습니다.
var personPrototype = { sayHi: function() { console.log("Hello, my name is " + this.name + ", and I am " + this.age); }, sayBye: function() { console.log("Bye Bye!"); } }; function Person(name, age) { name = name || "John Doe"; age = age || 26; function constructorFunction(name, age) { this.name = name; this.age = age; }; constructorFunction.prototype = personPrototype; var instance = new constructorFunction(name, age); return instance; } var person1 = Person(); var person2 = Person("Bob", 38); // prints out Hello, my name is John Doe, and I am 26 person1.sayHi(); // prints out Hello, my name is Bob, and I am 38 person2.sayHi();Take notice how prototype inheritance makes a performance boost as well because both objects contain a reference to the functions which are implemented in the prototype itself, instead of in each of the objects.
명령 패턴
The command pattern is useful in cases when we want to decouple objects executing the commands from objects issuing the commands. For example, imagine a scenario where our application is using a large number of API service calls. Then, let's say that the API services change. We would have to modify the code wherever the APIs that changed are called.
This would be a great place to implement an abstraction layer, which would separate the objects calling an API service from the objects which are telling them when to call the API service. This way, we avoid modification in all of the places where we have a need to call the service, but rather have to change only the objects which are making the call itself, which is only one place.
As with any other pattern, we have to know when exactly is there a real need for such a pattern. We need to be aware of the tradeoff we are making, as we are adding an additional abstraction layer over the API calls, which will reduce performance but potentially save a lot of time when we need to modify objects executing the commands.
// the object which knows how to execute the command var invoker = { add: function(x, y) { return x + y; }, subtract: function(x, y) { return x - y; } } // the object which is used as an abstraction layer when // executing commands; it represents an interface // toward the invoker object var manager = { execute: function(name, args) { if (name in invoker) { return invoker[name].apply(invoker, [].slice.call(arguments, 1)); } return false; } } // prints 8 console.log(manager.execute("add", 3, 5)); // prints 2 console.log(manager.execute("subtract", 5, 3));외관 패턴
The facade pattern is used when we want to create an abstraction layer between what is shown publicly and what is implemented behind the curtain. It is used when an easier or simpler interface to an underlying object is desired.
A great example of this pattern would be selectors from DOM manipulation libraries such as jQuery, Dojo, or D3. You might have noticed using these libraries that they have very powerful selector features; you can write in complex queries such as:
jQuery(".parent .child div.span")It simplifies the selection features a lot, and even though it seems simple on the surface, there is an entire complex logic implemented under the hood in order for this to work.
We also need to be aware of the performance-simplicity tradeoff. It is desirable to avoid extra complexity if it isn't beneficial enough. In the case of the aforementioned libraries, the tradeoff was worth it, as they are all very successful libraries.
다음 단계
Design patterns are a very useful tool which any senior JavaScript developer should be aware of. Knowing the specifics regarding design patterns could prove incredibly useful and save you a lot of time in any project's lifecycle, especially the maintenance part. Modifying and maintaining systems written with the help of design patterns which are a good fit for the system's needs could prove invaluable.
In order to keep the article relatively brief, we will not be displaying any more examples. For those interested, a great inspiration for this article came from the Gang of Four book Design Patterns: Elements of Reusable Object-Oriented Software and Addy Osmani's Learning JavaScript Design Patterns . I highly recommend both books.
