Structure Pattern

Bard·2022년 1월 11일
3

GoF Design Pattern

목록 보기
1/1

Adapter pattern

Intent

어댑터 패턴은 호환되지 않는 인터페이스를 가진 객체가 소통할 수 있게 만드는 구조 패턴입니다.

Problem

주식 시장 모니터링 앱을 만든다고 상상해 보세요. 이 앱은 XML 형식의 여러 소스에서 주식 데이터를 다운로드한 다음 사용자에게 보기 좋은 차트와 다이어그램을 표시합니다.

이제 우리는 스마트 타사 분석 라이브러리를 통합하여 앱을 개선하기로 결정합니다. 그러나 한 가지 단점이 있습니다. 분석 라이브러리는 JSON 형식의 데이터에서만 작동합니다.

XML에서 작동하도록 라이브러리를 변경할 수 있지만 이 경우 라이브러리에 의존하는 일부 기존 코드가 손상될 수 있습니다. 게다가 애초에 라이브러리의 소스 코드에 액세스할 수 없어 이러한 접근 방식이 불가능할 수도 있습니다.

Solution

어댑터를 생성합니다. 이것은 한 객체의 인터페이스를 다른 객체가 이해할 수 있도록 변환하는 특별한 객체입니다.

어댑터는 백그라운드에서 발생하는 변환의 복잡성을 숨기기 위해 개체 중 하나를 래핑합니다. 포장된 물체는 어댑터도 인식하지 못합니다. 예를 들어 미터 및 킬로미터 단위로 작동하는 객체를 모든 데이터를 피트 및 마일 같은 영국식 단위로 변환하는 어댑터를 사용하여 래핑할 수 있습니다.

어댑터는 데이터를 다양한 형식으로 변환할 수 있을 뿐만 아니라 서로 다른 인터페이스를 가진 객체의 협업을 도울 수 있습니다. 작동 방식은 다음과 같습니다.

  1. 어댑터가 기존 개체 중 하나와 호환되는 인터페이스를 가져옵니다.
  2. 이 인터페이스를 사용하면 기존 객체가 어댑터의 메서드를 안전하게 호출할 수 있습니다.
  3. 호출을 수신하면 어댑터는 요청을 두 번째 개체로 전달하지만 두 번째 개체가 예상하는 형식과 순서로 전달합니다.

때로는 양방향으로 호출을 변환할 수 있는 양방향 어댑터를 만들 수도 있습니다.

주식 시장 앱으로 돌아가보죠. 호환되지 않는 형식의 딜레마를 해결하기 위해 코드가 직접 작동하는 모든 분석 라이브러리 클래스에 대해 XML-JSON 어댑터를 만들 수 있습니다.
그런 다음 이러한 어댑터를 통해서만 라이브러리와 통신하도록 코드를 조정합니다. 어댑터는 호출을 수신하면 들어오는 XML 데이터를 JSON 구조로 변환하고 래핑된 분석 개체의 적절한 메서드로 호출을 전달합니다.

Real-World Analogy

미국에서 유럽으로 처음 여행을 갔을 때 노트북을 충전하려고 할 때 깜짝 놀랄 수 있습니다. 전원 플러그와 소켓 표준은 국가마다 다릅니다. 그래서 미국 플러그가 독일 소켓에 맞지 않는 거군요.
이 문제는 미국식 소켓과 유럽식 플러그가 있는 전원 플러그 어댑터를 사용하면 해결할 수 있습니다.

Structure

Object adapter

이 구현에서는 객체 구성 원리를 사용합니다. 즉, 어댑터는 한 객체의 인터페이스를 구현하고 다른 객체는 래핑합니다. 이는 대부분의 모든 프로그래밍 언어로 구현할 수 있습니다.

  1. 클라이언트는 프로그램의 기존 비즈니스 논리를 포함하는 클래스입니다.

  2. 클라이언트 인터페이스는 클라이언트 코드와 협업하기 위해 다른 클래스가 따라야 하는 프로토콜을 설명합니다.

  3. 서비스는 일부 유용한 클래스(일반적으로 타사 또는 레거시)입니다. 클라이언트가 호환되지 않는 인터페이스를 가지고 있기 때문에 이 클래스를 직접 사용할 수 없습니다.

  4. 어댑터는 클라이언트와 서비스 모두에서 사용할 수 있는 클래스입니다. 서비스 개체를 래핑하는 동안 클라이언트 인터페이스를 구현합니다. 어댑터는 어댑터 인터페이스를 통해 클라이언트로부터 호출을 수신하고 이해할 수 있는 형식으로 래핑된 서비스 개체에 대한 호출로 변환합니다.

  5. 클라이언트 코드는 클라이언트 인터페이스를 통해 어댑터와 함께 작동하는 한 구체적인 어댑터 클래스에 결합되지 않습니다. 덕분에 기존 클라이언트 코드를 깨지 않고 새로운 유형의 어댑터를 프로그램에 도입할 수 있습니다. 이 기능은 서비스 클래스의 인터페이스가 변경되거나 교체될 때 유용할 수 있습니다. 클라이언트 코드를 변경하지 않고 새 어댑터 클래스를 생성할 수 있습니다.

Class adapter

이 구현에서는 상속을 사용합니다. 어댑터는 두 객체 모두에서 인터페이스를 동시에 상속합니다. 이 접근 방식은 C++와 같은 다중 상속을 지원하는 프로그래밍 언어에서만 구현될 수 있습니다. (Javascript 또한 불가능합니다.)

  1. 클래스 어댑터는 클라이언트와 서비스 모두에서 동작을 상속하므로 개체를 래핑할 필요가 없습니다. 어댑션은 재정의된 방법 내에서 발생합니다. 결과 어댑터를 기존 클라이언트 클래스 대신 사용할 수 있습니다.

Pros and Cons

✅ Pros

  • SRP: 인터페이스 또는 데이터 변환 코드를 프로그램의 주요 비즈니스 로직과 분리할 수 있습니다.
  • OCP: 클라이언트 인터페이스를 통해 어댑터와 함께 작동하는 한 기존 클라이언트 코드를 손상하지 않고 프로그램에 새로운 유형의 어댑터를 도입할 수 있습니다. (Open)
    ❎ Cons
  • 일련의 새로운 인터페이스와 클래스를 도입해야 하므로 코드의 전반적인 복잡성이 증가합니다. 때때로 서비스 클래스를 코드의 나머지 부분과 일치하도록 변경하는 것이 더 간단합니다.
class Target {
    public request(): string {
        return 'Target: The default target\'s behavior.';
    }
}

class Adaptee {
    public specificRequest(): string {
        return '.eetpadA eht fo roivaheb laicepS';
    }
}

class Adapter extends Target {
    private adaptee: Adaptee;

    constructor(adaptee: Adaptee) {
        super();
        this.adaptee = adaptee;
    }

    public request(): string {
        const result = this.adaptee.specificRequest().split('').reverse().join('');
        return `Adapter: (TRANSLATED) ${result}`;
    }
}

function clientCode(target: Target) {
    console.log(target.request());
}

const adapter = new Adapter(adaptee);
clientCode(adapter);

Bridge Pattern

Intent

브릿지는 큰 클래스 또는 밀접하게 관련된 클래스 집합을 서로 독립적으로 개발할 수 있는 두 개의 개별 계층(추상 및 구현)으로 분할할 수 있는 구조 패턴입니다.

Problem

추상화? 구현? 무섭게 들리나요? 침착하고 간단한 예를 생각해 봅시다.

한 쌍의 하위 클래스가 있는 기하학적 모양 클래스가 있다고 가정해 봅시다. 원과 정사각형. 색상을 통합하도록 이 클래스 계층을 확장하려는 경우 빨간색 및 파란색 모양 하위 클래스를 만들 계획입니다. 그러나 이미 두 개의 하위 클래스가 있으므로 BlueCircle 및 RedSquare와 같은 네 개의 클래스 조합을 만들어야 합니다.

새 모양 유형과 색상을 계층에 추가하면 계층 구조가 기하급수적으로 증가합니다. 예를 들어 삼각형 모양을 추가하려면 각 색상에 하나씩 두 개의 하위 클래스를 도입해야 합니다. 그 후에 새 색상을 추가하려면 각 모양 유형별로 하나씩 3개의 하위 클래스를 만들어야 합니다. 가면 갈수록 문제가 더 심각해지겠죠?

Solution

이 문제는 모양 클래스를 형태별과 색상별로 두 가지 독립적인 차원으로 확장하려고 하기 때문에 발생합니다. 이건 상속과 관련된 매우 흔한 문제입니다.

브릿지는 상속에서 객체 합성으로 전환하여 이 문제를 해결하려고 합니다. 즉, 한 클래스 내에 모든 상태와 동작을 포함하는 대신 원래 클래스가 새 계층의 개체를 참조하도록 차원 중 하나를 별도의 클래스 계층 구조로 추출합니다.

이 접근법에 따라 색상 관련 코드를 두 가지 하위 클래스로 추출할 수 있습니다. 빨강과 파랑. 그런 다음 Shape 클래스는 색상 객체 중 하나를 가리키는 참조 필드를 가져옵니다. 이제 모양은 색과 관련된 모든 작업을 링크된 색 객체에 위임할 수 있습니다. 이 참조는 Shape 클래스와 Color 클래스 사이의 다리 역할을 합니다. 이제부터 새로운 색상을 추가하는 것은 형태 계층을 변경할 필요가 없으며, 그 반대의 경우도 마찬가지입니다.

Abstraction and Implementation

GoF 책에서는 브리지 정의의 일부로 추상화와 구현이라는 용어를 소개합니다.

추상화(interface)는 일부 엔티티를 위한 높은 수준의 제어 계층입니다. 이 층은 스스로 어떠한 실제 작업도 할 수 없게 되어 있습니다. 작업은 구현 계층(플랫폼이라고도 함)에 위임해야 합니다.

프로그래밍 언어의 인터페이스나 추상 클래스에 대해 이야기하는 것이 아닙니다. 아예 다른 이야기입니다.

실제 애플리케이션에 대해 이야기하자면, 추상화는 그래픽 사용자 인터페이스(GUI)로 나타낼 수 있으며 구현체는 사용자 상호 작용에 대한 응답으로 GUI 계층이 호출하는 기본 운영 체제 코드(API)가 될 수 있습니다.

일반적으로 이러한 앱을 두 가지 독립적인 방향으로 확장할 수 있습니다.

  • 몇 가지 다른 GUI(예: 일반 고객 또는 관리자에게 맞춤 제공)를 사용합니다.
  • Windows, Linux 및 macOS에서 앱을 실행할 수 있도록 여러 다른 API를 지원합니다.

최악의 경우 이 앱은 여기서 수백 가지의 조건들이 코드 전체에 다양한 API와 함께 서로 다른 종류의 GUI를 연결하기 때문에 거대한 스파게티 그릇처럼 보일 수 있습니다.

특정 인터페이스-플랫폼 조합과 관련된 코드를 별도의 클래스로 추출하여 이러한 혼돈의 순서를 가져올 수 있습니다. 하지만, 여러분은 곧 이러한 클래스가 많다는 것을 알게 될 것입니다.
새로운 GUI를 추가하거나 다른 API를 지원하려면 더 많은 클래스를 만들어야 하기 때문에 클래스 계층은 기하급수적으로 증가할 것입니다.

브릿지 패턴으로 이 문제를 해결해봅시다. 우선 클래스를 두 개의 계층으로 나눠보죠.

  • Abstraction: 앱의 GUI 레이어
  • Implementation: 운영 체제의 API

추상화 개체는 앱의 모양을 제어하여 실제 작업을 연결된 구현 개체에 위임합니다. 다른 구현체들은 공통 인터페이스를 따르는 한 상호 교환이 가능하므로 동일한 GUI가 윈도우와 리눅스에서 작동할 수 있다.

따라서 API 관련 클래스를 건드리지 않고 GUI 클래스를 변경할 수 있습니다. 게다가 다른 운영 체제에 대한 지원을 추가하는 것은 구현 계층 구조에서 하위 클래스만 생성하면 됩니다.

Structure

  1. 추상화는 높은 수준의 제어 논리를 제공합니다. 실제 낮은 수준의 작업을 수행하기 위해 구현 객체에 의존합니다.
  2. 구현은 모든 구체적인 구현에 공통적인 인터페이스를 선언합니다. 추상화는 여기서 선언된 메소드를 통해서만 구현 객체와 통신할 수 있습니다.
    추상화는 구현과 같은 방법들을 나열할 수 있지만, 대개 추상화는 구현에 의해 선언되는 광범위한 원시적 운영들에 의존하는 몇몇 복잡한 행동들을 선언한다.
  3. 구체적인 구현에는 플랫폼별 코드가 포함되어 있습니다.
  4. 정제된 추상화는 제어 논리의 변형을 제공합니다. 그들의 부모들처럼, 그들은 일반 구현 인터페이스를 통해서 다른 구현들과 함께 일합니다.
  5. 일반적으로 클라이언트는 추상화 작업에만 관심이 있습니다. 그러나 추상화 객체를 구현 객체 중 하나와 연결하는 것은 클라이언트의 일입니다.

Pros and Cons

✅ Pros

  • 플랫폼 독립적인 클래스 및 앱을 만들 수 있습니다.
  • 클라이언트 코드는 높은 수준의 추상화와 함께 작동합니다. 플랫폼 디테일에 노출되지 않습니다.
  • OCP: 새로운 추상화와 구현을 서로 독립적으로 도입할 수 있습니다.
  • SRP: 추상화의 고급 논리와 구현의 플랫폼 세부 정보에 초점을 맞출 수 있습니다.
    ❎ Cons
  • 패턴을 매우 응집력 있는 클래스에 적용하면 코드를 더 복잡하게 만들 수 있습니다.
class Abstraction {
    protected implementation: Implementation;

    constructor(implementation: Implementation) {
        this.implementation = implementation;
    }

    public operation(): string {
        const result = this.implementation.operationImplementation();
        return `Abstraction: Base operation with:\n${result}`;
    }
}

class ExtendedAbstraction extends Abstraction {
    public operation(): string {
        const result = this.implementation.operationImplementation();
        return `ExtendedAbstraction: Extended operation with:\n${result}`;
    }
}

interface Implementation {
    operationImplementation(): string;
}

class ConcreteImplementationA implements Implementation {
    public operationImplementation(): string {
        return 'ConcreteImplementationA: Here\'s the result on the platform A.';
    }
}

class ConcreteImplementationB implements Implementation {
    public operationImplementation(): string {
        return 'ConcreteImplementationB: Here\'s the result on the platform B.';
    }
}

function clientCode(abstraction: Abstraction) {
    // ..

    console.log(abstraction.operation());

    // ..
}

let implementation = new ConcreteImplementationA();
let abstraction = new Abstraction(implementation);
clientCode(abstraction);

console.log('');

implementation = new ConcreteImplementationB();
abstraction = new ExtendedAbstraction(implementation);
clientCode(abstraction);

Composite Pattern

Intent

컴포지트 패턴은 객체를 트리 구조로 합성한 다음 이러한 구조를 개별 객체인 것처럼 작업할 수 있는 구조 설계 패턴입니다.

Problem

컴포지트 패턴을 사용하는 것은 앱의 핵심 모델을 트리로 나타낼 수 있는 경우에만 의미가 있습니다.

예를 들어, 두 가지 개체 유형(제품 및 상자)이 있다고 가정합니다. 상자 하나에 여러 개의 제품 및 여러 개의 작은 상자가 포함될 수 있습니다. 이러한 작은 상자에는 일부 제품 또는 더 작은 상자 등이 포함될 수도 있습니다.

이러한 클래스를 사용하는 주문 시스템을 만들기로 결정했다고 가정해 보십시오. 주문에는 포장 없이 간단한 제품뿐만 아니라 제품이 가득 들어 있는 상자도 포함될 수 있습니다. 그리고 다른 박스들. 당신은 그러한 주문의 총 가격을 어떻게 결정하시겠습니까?

포장 상자를 모두 열고 모든 제품을 검토한 다음 총계를 계산하는 직접적인 방법을 시도할 수 있습니다. 그것은 현실에서 할 수 있을 것입니다. 그러나 프로그램에서 그것은 루프를 돌리는 것만큼 간단하지 않습니다. 여러분은 여러분이 겪고 있는 상품과 상자의 종류, 상자의 중첩 수준, 그리고 다른 더러운 세부 사항들을 미리 알고 있어야 합니다. 이 모든 것이 직접적인 접근을 너무 어색하거나 심지어 불가능하게 만듭니다.

Solution

컴포지트 패턴은 총 가격 계산 방법을 선언하는 공통 인터페이스를 통해 제품 및 상자로 작업할 것을 제안합니다.

이 방법은 어떻게 작동할까요? 제품의 경우, 단순히 제품의 가격을 반환해 줄 것입니다. 박스의 경우, 박스가 들어 있는 각 품목을 살펴본 후 가격을 물어본 후 이 박스에 대한 총계를 돌려줄 것입니다. 만약 이 품목들 중 하나가 작은 상자라면, 내부의 모든 부품들의 가격이 계산될 때까지 그 상자도 내용물을 검토하기 시작할 것이다. 한 박스는 포장 비용과 같은 최종 가격에 약간의 추가 비용을 추가할 수도 있다.

이 접근 방식의 가장 큰 이점은 트리를 구성하는 객체의 구체적인 종류에 신경 쓸 필요가 없다는 것입니다. 물건이 단순한 제품인지 정교한 상자인지 알 필요는 없습니다. 공통 인터페이스를 통해 모두 동일하게 취급할 수 있습니다. 메서드를 호출하면 개체 자체가 트리 아래로 요청을 전달합니다.

Structure

  1. Component 인터페이스는 트리의 단순 요소와 복잡한 요소 모두에 공통적인 작업을 설명합니다.

  2. Leaf는 하위 요소가 없는 트리의 기본 요소입니다.
    보통, Leaf는 작업을 위임할 곳이 없기 때문에 대부분의 실제 작업을 하게 됩니다.

  3. Container는 잎 또는 기타 용기와 같은 하위 요소를 가진 요소입니다. 컨테이너는 자식들의 구체적인 계급을 알지 못한다. 구성 요소 인터페이스를 통해서만 모든 하위 요소와 함께 작동합니다.
    요청을 받으면 컨테이너는 작업을 하위 요소에 위임하고 중간 결과를 처리한 다음 클라이언트에게 최종 결과를 반환합니다.

  4. Client는 Component를 통해 모든 요소와 함께 작동합니다.

Pros and Cons

✅ Pros

  • 복잡한 트리 구조를 보다 편리하게 사용할 수 있습니다. 다형성 및 재귀 기능을 사용할 수 있습니다.
  • OCP: 이제 객체 트리와 함께 작동하는 기존 코드를 중단하지 않고 새로운 요소 유형을 앱에 도입할 수 있습니다.
    ❎ Cons
  • 기능이 너무 다른 클래스에 공통 인터페이스를 제공하는 것은 어려울 수 있습니다. 특정 시나리오에서는 구성요소 인터페이스를 지나치게 일반화해야 하므로 이해하기 어렵습니다.
abstract class Component {
    protected parent: Component;

    /**
     * Optionally, the base Component can declare an interface for setting and
     * accessing a parent of the component in a tree structure. It can also
     * provide some default implementation for these methods.
     */
    public setParent(parent: Component) {
        this.parent = parent;
    }

    public getParent(): Component {
        return this.parent;
    }

    /**
     * In some cases, it would be beneficial to define the child-management
     * operations right in the base Component class. This way, you won't need to
     * expose any concrete component classes to the client code, even during the
     * object tree assembly. The downside is that these methods will be empty
     * for the leaf-level components.
     */
    public add(component: Component): void { }

    public remove(component: Component): void { }

    /**
     * You can provide a method that lets the client code figure out whether a
     * component can bear children.
     */
    public isComposite(): boolean {
        return false;
    }

    /**
     * The base Component may implement some default behavior or leave it to
     * concrete classes (by declaring the method containing the behavior as
     * "abstract").
     */
    public abstract operation(): string;
}

/**
 * The Leaf class represents the end objects of a composition. A leaf can't have
 * any children.
 *
 * Usually, it's the Leaf objects that do the actual work, whereas Composite
 * objects only delegate to their sub-components.
 */
class Leaf extends Component {
    public operation(): string {
        return 'Leaf';
    }
}

/**
 * The Composite class represents the complex components that may have children.
 * Usually, the Composite objects delegate the actual work to their children and
 * then "sum-up" the result.
 */
class Composite extends Component {
    protected children: Component[] = [];

    /**
     * A composite object can add or remove other components (both simple or
     * complex) to or from its child list.
     */
    public add(component: Component): void {
        this.children.push(component);
        component.setParent(this);
    }

    public remove(component: Component): void {
        const componentIndex = this.children.indexOf(component);
        this.children.splice(componentIndex, 1);

        component.setParent(null);
    }

    public isComposite(): boolean {
        return true;
    }

    /**
     * The Composite executes its primary logic in a particular way. It
     * traverses recursively through all its children, collecting and summing
     * their results. Since the composite's children pass these calls to their
     * children and so forth, the whole object tree is traversed as a result.
     */
    public operation(): string {
        const results = [];
        for (const child of this.children) {
            results.push(child.operation());
        }

        return `Branch(${results.join('+')})`;
    }
}

/**
 * The client code works with all of the components via the base interface.
 */
function clientCode(component: Component) {
    // ...

    console.log(`RESULT: ${component.operation()}`);

    // ...
}

/**
 * This way the client code can support the simple leaf components...
 */
const simple = new Leaf();
console.log('Client: I\'ve got a simple component:');
clientCode(simple);
console.log('');

/**
 * ...as well as the complex composites.
 */
const tree = new Composite();
const branch1 = new Composite();
branch1.add(new Leaf());
branch1.add(new Leaf());
const branch2 = new Composite();
branch2.add(new Leaf());
tree.add(branch1);
tree.add(branch2);
console.log('Client: Now I\'ve got a composite tree:');
clientCode(tree);
console.log('');

/**
 * Thanks to the fact that the child-management operations are declared in the
 * base Component class, the client code can work with any component, simple or
 * complex, without depending on their concrete classes.
 */
function clientCode2(component1: Component, component2: Component) {
    // ...

    if (component1.isComposite()) {
        component1.add(component2);
    }
    console.log(`RESULT: ${component1.operation()}`);

    // ...
}

console.log('Client: I don\'t need to check the components classes even when managing the tree:');
clientCode2(tree, simple);

Decorator Pattern

Intent

데코레이터 패턴은 동작을 포함하는 특수 래퍼 객체 안에 객체를 배치하여 객체에 새 동작을 부착할 수 있는 구조 설계 패턴입니다.

Problem

다른 프로그램이 사용자에게 중요한 이벤트를 알릴 수 있는 알림 라이브러리를 작업하고 있다고 가정합시다.

라이브러리의 초기 버전은 몇 개의 필드, 생성자 및 단일 송신 메서드를 가진 Notifier 클래스에 기반을 두었습니다. 메소드는 클라이언트의 메시지를 받고 생성자를 통해 통보자에게 전달된 이메일 목록으로 메시지를 보낼 수 있습니다. 클라이언트 역할을 하는 서드파티 앱은 알림 객체를 한 번 만들어 구성한 뒤 중요한 일이 발생할 때마다 사용하도록 되어있었습니다.

우리는 라이브러리 사용자가 이메일 알림 이상을 기대한다는 것을 알 수 있습니다. 그들 중 많은 사람들이 중요한 문제에 대한 SMS를 받고 싶어합니다. 어떤 사람들은 페이스북에서 알림을 받고 싶어하고, 물론 기업 사용자들은 슬랙 알림을 받고 싶어할 것입니다.

얼마나 어렵겠나요? Notifier 클래스를 확장하고 추가 알림 방법을 새 하위 클래스에 넣었습니다. 이제 클라이언트는 원하는 알림 클래스를 인스턴스화하고 이후 모든 알림에 사용하도록 되어 있습니다.

그러나 누군가가 합리적으로 "한 번에 여러 가지 알림 유형을 사용할 수 없는 이유는 무엇입니까? 만약 여러분의 집에 불이 난다면, 여러분은 아마 모든 경로를 통해 정보를 얻고 싶을 것입니다." 라고 묻습니다.

한 클래스 내에 여러 알림 방법을 결합한 특수 하위 클래스를 만들어 이 문제를 해결하려고 했습니다. 그러나 이 접근 방식은 라이브러리 코드뿐만 아니라 클라이언트 코드도 엄청나게 부풀릴 것이 분명해졌습니다.

이러다가 기네스 기록을 깨지 않도록 알림 클래스를 구성하는 다른 방법을 찾아야 합니다.

Solution

개체의 동작을 변경해야 할 때 가장 먼저 떠오르는 것은 클래스 상속입니다. 그러나 상속에는 몇 가지 심각한 주의사항이 있습니다.

상속은 고정입니다. 런타임에 기존 개체의 동작을 변경할 수 없습니다. 전체 객체를 다른 하위 클래스에서 생성된 다른 오브젝트로만 바꿀 수 있습니다.
하위 클래스는 부모 클래스를 하나만 가질 수 있습니다. 대부분의 언어에서 상속은 클래스가 여러 클래스의 동작을 동시에 상속하도록 허용하지 않습니다.
이러한 경고를 극복하는 방법 중 하나는 C언어를 사용하는 상속 대신 Aggregation 또는 Composition을 사용하는 것입니다. 두 가지 대안은 거의 같은 방식으로 동작합니다: 한 개체는 다른 개체에 대한 참조를 가지고 일부 작업을 위임하는 반면, 상속을 통해 개체 자체는 슈퍼클래스의 동작을 이어받으며 해당 작업을 수행할 수 있다.

이 새로운 접근 방식을 사용하면 링크된 "헬퍼" 개체를 다른 개체로 쉽게 대체하여 런타임에 컨테이너의 동작을 변경할 수 있습니다. 개체는 여러 개체를 참조하고 모든 종류의 작업을 위임하는 다양한 클래스의 동작을 사용할 수 있습니다. Aggregation/composition은 데코레이터를 포함한 많은 디자인 패턴의 핵심 원리입니다. 그 점에 대해서는, 패턴에 대한 논의로 돌아갑시다.

"Wrapper"는 패턴의 주요 아이디어를 명확하게 표현하는 데코레이터 패턴의 대체 별명입니다. 래퍼(wrapper)는 일부 대상 객체와 연결할 수 있는 객체입니다. 래퍼에는 대상과 동일한 메소드 집합이 포함되어 있으며 대상이 수신하는 모든 요청을 대상으로 위임합니다. 그러나 래퍼들은 대상에 요청을 전달하기 전이나 후에 무언가를 함으로써 결과를 바꿀 수 있습니다.

언제 간단한 포장지가 진짜 데코레이터가 될까요? 말했듯이 래퍼는 래핑된 객체와 동일한 인터페이스를 구현합니다. 그것이 고객의 관점에서 이 물건들이 동일한 이유입니다. 래퍼의 참조 필드가 해당 인터페이스 다음에 오는 모든 개체를 허용하도록 합니다. 이렇게 하면 개체를 여러 래퍼에 포함시켜 모든 래퍼의 결합된 동작을 개체에 추가할 수 있습니다.

알림 예제에서는 간단한 전자 메일 알림 동작을 기본 알림 클래스 안에 두고 다른 모든 알림 방법을 장식자로 변경해 보겠습니다.

클라이언트 코드는 기본 알림 개체를 클라이언트의 기본 설정과 일치하는 데코레이터 집합으로 래핑해야 합니다. 결과 객체는 스택으로 구조화됩니다.

스택의 마지막 데코레이터는 클라이언트가 실제로 작업할 수 있는 객체입니다. 모든 데코레이터가 기본 알림과 동일한 인터페이스를 구현하기 때문에 클라이언트 코드의 나머지 부분은 "순수" 알림 개체와 함께 작동하든 장식된 개체와 함께 작동하든 상관하지 않습니다.

우리는 메시지 형식 지정이나 수신자 목록 구성과 같은 다른 행동에도 같은 접근법을 적용할 수 있습니다. 클라이언트는 다른 사용자와 동일한 인터페이스를 따르는 한 어떤 사용자 지정 장식가로도 객체를 장식할 수 있습니다.

/**
 * The base Component interface defines operations that can be altered by
 * decorators.
 */
interface Component {
    operation(): string;
}

/**
 * Concrete Components provide default implementations of the operations. There
 * might be several variations of these classes.
 */
class ConcreteComponent implements Component {
    public operation(): string {
        return 'ConcreteComponent';
    }
}

/**
 * The base Decorator class follows the same interface as the other components.
 * The primary purpose of this class is to define the wrapping interface for all
 * concrete decorators. The default implementation of the wrapping code might
 * include a field for storing a wrapped component and the means to initialize
 * it.
 */
class Decorator implements Component {
    protected component: Component;

    constructor(component: Component) {
        this.component = component;
    }

    /**
     * The Decorator delegates all work to the wrapped component.
     */
    public operation(): string {
        return this.component.operation();
    }
}

/**
 * Concrete Decorators call the wrapped object and alter its result in some way.
 */
class ConcreteDecoratorA extends Decorator {
    /**
     * Decorators may call parent implementation of the operation, instead of
     * calling the wrapped object directly. This approach simplifies extension
     * of decorator classes.
     */
    public operation(): string {
        return `ConcreteDecoratorA(${super.operation()})`;
    }
}

/**
 * Decorators can execute their behavior either before or after the call to a
 * wrapped object.
 */
class ConcreteDecoratorB extends Decorator {
    public operation(): string {
        return `ConcreteDecoratorB(${super.operation()})`;
    }
}

/**
 * The client code works with all objects using the Component interface. This
 * way it can stay independent of the concrete classes of components it works
 * with.
 */
function clientCode(component: Component) {
    // ...

    console.log(`RESULT: ${component.operation()}`);

    // ...
}

/**
 * This way the client code can support both simple components...
 */
const simple = new ConcreteComponent();
console.log('Client: I\'ve got a simple component:');
clientCode(simple);
console.log('');

/**
 * ...as well as decorated ones.
 *
 * Note how decorators can wrap not only simple components but the other
 * decorators as well.
 */
const decorator1 = new ConcreteDecoratorA(simple);
const decorator2 = new ConcreteDecoratorB(decorator1);
console.log('Client: Now I\'ve got a decorated component:');
clientCode(decorator2);

Facade Pattern

Intent

파사드패턴은 라이브러리, 프레임워크 또는 기타 복잡한 클래스 집합에 대한 단순화된 인터페이스를 제공하는 구조 설계 패턴입니다.

Problem

정교한 라이브러리나 프레임워크에 속하는 광범위한 객체 집합에서 코드가 작동하도록 해야 한다고 생각해 보십시오. 일반적으로 이러한 모든 개체를 초기화하고, 종속성을 추적하고, 메서드를 올바른 순서로 실행하는 등의 작업이 필요합니다.

그 결과 클래스의 비즈니스 로직은 타 클래스의 구현 세부 사항과 긴밀하게 결합되어 이해와 유지에 어려움을 겪게 됩니다.

Solution

파사드는 많은 이동 부품을 포함하는 복잡한 하위 시스템에 단순한 인터페이스를 제공하는 클래스입니다. 파사드는 서브시스템과 직접 연동하는 것에 비해 제한된 기능을 제공할 수 있다. 하지만 고객이 진정으로 신경 쓰는 기능만 포함하고 있습니다.

파사드를 갖추는 것은 수십 개의 기능이 있는 정교한 라이브러리와 당신의 앱을 통합해야 할 때 편리하지만, 그것의 약간의 기능만 있으면 된다.

예를 들어, 고양이와 함께 찍은 짧은 재미있는 영상을 소셜 미디어에 업로드하는 앱은 잠재적으로 전문 비디오 변환 라이브러리를 사용할 수 있습니다. 그러나 실제로 필요한 것은 단일 메소드 인코딩(파일 이름, 형식)의 클래스이다. 그런 클래스를 만들고 영상 변환 라이브러리와 연결하면 파사드가 나옵니다.

Structure

  1. 파사드는 서브시스템 기능의 특정 부분에 대한 편리한 액세스를 제공합니다. 고객의 요청을 어디로 지시해야 하는지, 모든 이동 부품을 어떻게 작동해야 하는지 알고 있습니다.

  2. 다른 복잡한 구조를 만들 수 있는 관련 없는 피쳐가 있는 단일 파사드를 오염시키지 않도록 추가 파사드 클래스를 작성할 수 있습니다. 추가 파사드는 클라이언트와 다른 파사드 모두 사용할 수 있습니다.

  3. 복잡한 하위 시스템은 수십 개의 다양한 개체로 구성됩니다. 이들 모두가 의미 있는 작업을 수행하도록 하려면 객체를 올바른 순서로 초기화하고 적절한 형식으로 데이터를 제공하는 등 하위 시스템의 구현 세부 정보를 깊이 살펴봐야 합니다.

하위 시스템 클래스는 파사드의 존재를 인식하지 못합니다. 시스템 내에서 작동하며 서로 직접 작동합니다.

  1. 클라이언트는 서브시스템 객체를 직접 호출하는 대신 파사드를 사용합니다.
class Facade {
    protected subsystem1: Subsystem1;

    protected subsystem2: Subsystem2;

    /**
     * Depending on your application's needs, you can provide the Facade with
     * existing subsystem objects or force the Facade to create them on its own.
     */
    constructor(subsystem1: Subsystem1 = null, subsystem2: Subsystem2 = null) {
        this.subsystem1 = subsystem1 || new Subsystem1();
        this.subsystem2 = subsystem2 || new Subsystem2();
    }

    /**
     * The Facade's methods are convenient shortcuts to the sophisticated
     * functionality of the subsystems. However, clients get only to a fraction
     * of a subsystem's capabilities.
     */
    public operation(): string {
        let result = 'Facade initializes subsystems:\n';
        result += this.subsystem1.operation1();
        result += this.subsystem2.operation1();
        result += 'Facade orders subsystems to perform the action:\n';
        result += this.subsystem1.operationN();
        result += this.subsystem2.operationZ();

        return result;
    }
}

/**
 * The Subsystem can accept requests either from the facade or client directly.
 * In any case, to the Subsystem, the Facade is yet another client, and it's not
 * a part of the Subsystem.
 */
class Subsystem1 {
    public operation1(): string {
        return 'Subsystem1: Ready!\n';
    }

    // ...

    public operationN(): string {
        return 'Subsystem1: Go!\n';
    }
}

/**
 * Some facades can work with multiple subsystems at the same time.
 */
class Subsystem2 {
    public operation1(): string {
        return 'Subsystem2: Get ready!\n';
    }

    // ...

    public operationZ(): string {
        return 'Subsystem2: Fire!';
    }
}

/**
 * The client code works with complex subsystems through a simple interface
 * provided by the Facade. When a facade manages the lifecycle of the subsystem,
 * the client might not even know about the existence of the subsystem. This
 * approach lets you keep the complexity under control.
 */
function clientCode(facade: Facade) {
    // ...

    console.log(facade.operation());

    // ...
}

/**
 * The client code may have some of the subsystem's objects already created. In
 * this case, it might be worthwhile to initialize the Facade with these objects
 * instead of letting the Facade create new instances.
 */
const subsystem1 = new Subsystem1();
const subsystem2 = new Subsystem2();
const facade = new Facade(subsystem1, subsystem2);
clientCode(facade);
profile
The Wandering Caretaker

0개의 댓글