flagtags / OOP-study

0 stars 0 forks source link

9장. 반복자 패턴과 컴포지트 패턴 #25

Open j03y14 opened 4 months ago

j03y14 commented 4 months ago

두 가게가 합병.

class MenuItem {
    name: string;

    description: string;

    vegetarian: boolean;

    price: number;

    constructor(name: string, description: string, vegetarian: boolean, price: number) {
        this.name = name;
        this.description = description;
        this.vegetarian = vegetarian;
        this.price = price;
    }

    getName() {
        return this.name;
    }

    getDescription() {
        return this.description;
    }

    isVegetarian() {
        return this.vegetarian;
    }

    getPrice() {
        return this.price;
    }
}

위와 같은 메뉴 아이템 클래스가 있을 때 두 가게의 메뉴에 의존하는 코드가 다르다. 팬케이크 하우스에서는 ArrayList로 구현이 되어있고, DinerMenu는 배열로 되어있다.

클라이언트가 사용하는 입장에서 두 클래스를 사용하는 방법이 다르다.

인터페이스가 같아진다면, 반복문을 각 클래스에 맞춰서 돌리거나, 조건문을 사용하거나 할 일이 없다.

반복을 캡슐화 하기

바뀌는 부분을 캡슐화 해야 하는데, 반복 처리 작업이 바뀌고 있다.

반복처리 방법을 캡슐화한 iterator 객체를 만들면?

interface Iterator {
    hasNext(): boolean;
    next(): any;

}

class MenuItem {
    name: string;

    description: string;

    vegetarian: boolean;

    price: number;

    constructor(name: string, description: string, vegetarian: boolean, price: number) {
        this.name = name;
        this.description = description;
        this.vegetarian = vegetarian;
        this.price = price;
    }

    getName() {
        return this.name;
    }

    getDescription() {
        return this.description;
    }

    isVegetarian() {
        return this.vegetarian;
    }

    getPrice() {
        return this.price;
    }
}

class DinerMenuIterator implements Iterator {
    menuItems: MenuItem[];

    position: number = 0;

    constructor(menuItems: MenuItem[]) {
        this.menuItems = menuItems;
    }

    hasNext() {
        return this.position < this.menuItems.length;
    }

    next() {
        const menuItem = this.menuItems[this.position];
        this.position += 1;
        return menuItem;
    }

}

class DinerMenu {
    static MAX_ITEMS = 6;

    numberOfItems = 0;

    menuItems: MenuItem[] = [];

    constructor() {
        this.addItem('blt', 'Bacon, lettuce, tomato on whole wheat', false, 2.99);
        this.addItem('veggie blt', 'Bacon, lettuce, tomato on whole wheat', true, 2.99);
    }

    addItem(name: string, description: string, vegetarian: boolean, price: number) {
        const menuItem = new MenuItem(name, description, vegetarian, price);
        if (this.numberOfItems >= DinerMenu.MAX_ITEMS) {
            console.error('Sorry, menu is full! Can\'t add item to menu');
        } else {
            this.menuItems.push(menuItem);
            this.numberOfItems += 1;
        }
    }

    createIterator() {
        return new DinerMenuIterator(this.menuItems);
    }

}

class Witress {
    dinerMenu: DinerMenu;

    constructor(dinerMenu: DinerMenu) {
        this.dinerMenu = dinerMenu;
    }

    printMenu() {
        const iterator = this.dinerMenu.createIterator();
        while (iterator.hasNext()) {
            const menuItem = iterator.next();
            console.log(menuItem.getName());
            console.log(menuItem.getDescription());
            console.log(menuItem.getPrice());
        }
    }
}

내장 iterator 사용

class MenuItem {
    name: string;

    description: string;

    vegetarian: boolean;

    price: number;

    constructor(name: string, description: string, vegetarian: boolean, price: number) {
        this.name = name;
        this.description = description;
        this.vegetarian = vegetarian;
        this.price = price;
    }

    getName() {
        return this.name;
    }

    getDescription() {
        return this.description;
    }

    isVegetarian() {
        return this.vegetarian;
    }

    getPrice() {
        return this.price;
    }
}

interface Menu {
    createIterator(): Iterator<MenuItem>;
}

class DinerMenu {
    static MAX_ITEMS = 6;

    numberOfItems = 0;

    menuItems: MenuItem[] = [];

    constructor() {
        this.addItem('blt', 'Bacon, lettuce, tomato on whole wheat', false, 2.99);
        this.addItem('veggie blt', 'Bacon, lettuce, tomato on whole wheat', true, 2.99);
    }

    addItem(name: string, description: string, vegetarian: boolean, price: number) {
        const menuItem = new MenuItem(name, description, vegetarian, price);
        if (this.numberOfItems >= DinerMenu.MAX_ITEMS) {
            console.error('Sorry, menu is full! Can\'t add item to menu');
        } else {
            this.menuItems.push(menuItem);
            this.numberOfItems += 1;
        }
    }

    createIterator() {
        return this.menuItems[Symbol.iterator]();
    }

}

class Witress {
    dinerMenu: Menu;

    constructor(dinerMenu: Menu) {
        this.dinerMenu = dinerMenu;
    }

    printMenu() {
        const iterator = this.dinerMenu.createIterator();
        let next = iterator.next();

        while (!next.done) {
            const menuItem = next.value;
            console.log(`${menuItem.getName()}, ${menuItem.getPrice()} -- ${menuItem.getDescription()}`);
            next = iterator.next();
        }
    }
}

반복자 패턴의 정의

컬렉션의 구현 방법을 노출하지 않으면서 집합체 내의 모든 항목에 접근하는 방법을 제공한다.

단일 역할 원칙

어떤 클래스가 바뀌는 이유는 하나뿐이어야 한다.

종업원 코드 개선하기

메뉴 안에 sub menu가 들어갈 수 있는 형태로 개선해야 한다.

컴포지트 패턴의 정의

객체를 트리구조로 구성해서 부분-전체 계층구조를 구현한다. 컴포지트 패턴을 사용하면 개별 객체와 복합 객체를 같은 방법으로 다룰 수 있다.

class MenuComponent {
    add(menuComponent: MenuComponent) {
        throw new Error('Unsupported Operation');
    }

    remove(menuComponent: MenuComponent) {
        throw new Error('Unsupported Operation');
    }

    getChild(i: number) {
        throw new Error('Unsupported Operation');
    }

    getName() {
        throw new Error('Unsupported Operation');
    }

    getDescription() {
        throw new Error('Unsupported Operation');
    }

    getPrice() {
        throw new Error('Unsupported Operation');
    }

    isVegetarian() {
        throw new Error('Unsupported Operation');
    }

    print() {
        throw new Error('Unsupported Operation');
    }

}

class MenuItem extends MenuComponent {
    name: string;
    descriptipn: string;
    vegetarian: boolean;
    price: number;

    constructor(name: string, description: string, vegetarian: boolean, price: number) {
        super();
        this.name = name;
        this.descriptipn = description;
        this.vegetarian = vegetarian;
        this.price = price;
    }

    getName() {
        return this.name;
    }

    getDescription() {
        return this.descriptipn;
    }

    getPrice() {
        return this.price;
    }

    isVegetarian() {
        return this.vegetarian;
    }

    print() {
        console.log(`${this.getName()}, ${this.getPrice()} -- ${this.getDescription()}`);
    }
}

class Menu extends MenuComponent {
    menuComponents: MenuComponent[] = [];
    name: string;
    description: string;

    constructor(name: string, description: string) {
        super();
        this.name = name;
        this.description = description;
    }

    add(menuComponent: MenuComponent): void {
        this.menuComponents.push(menuComponent);
    }

    remove(menuComponent: MenuComponent): void {
        this.menuComponents = this.menuComponents.filter((m) => m !== menuComponent);
    }

    getChild(i: number): MenuComponent {
        return this.menuComponents[i];
    }

    getName() {
        return this.name;
    }

    getDescription() {
        return this.description;
    }

    print() {
        console.log(`${this.getName()}, ${this.getDescription()}`);

        for (const m of this.menuComponents) {
            m.print();
        }
    }
}

class Witress {
    allMenus: MenuComponent;

    constructor(allMenus: MenuComponent) {
        this.allMenus = allMenus;
    }

    printMenu() {
        this.allMenus.print();
    }
}

컴포지트 패턴에서는 두가지 역할을 하고 있다?

단일 역할 원칙 대신에 투명성을 확보하는 패턴이다. 어떤 원소가 복합객체인지 잎인지가 클라이언트에게 투명하게보인다.

여러 원칙을 상황에 따라 적절하게 사용해야 한다.

kkirico commented 4 months ago

반복자 패턴

두 가게가 합병

배열, 리스트, 해시테이블 등 리턴하는 형태가 다를 때, 반복자 패턴을 사용해서, 중복되는 부분을 합쳐줄 수 있다.

interface Iterator {
    hasNext: () => boolean;
    next: () => MenuItem;
}

class DinerMenuIterator implements Iterator {
    items: MenuItem[];
    position: number;

    constructor(items: MenuItem[]) {
        this.items = items;
    }

    next() {
        menuItem = items[position];
        position += 1;
        return menuItem;
    }

    hasNext() {
        return !(position >= items.length || items[position] == null)
    }
}

class DinerMenu {
    MAX_ITEMS = 6;
    numberOfItems = 0;
    menuItems;

    // 생성자

    // addItem 메소드

    createIterator() {
        return new DinerMenuIterator(menuItems);
    }
}

class Waitress {
    dinerMenu: DinerMenu;

    constructor(dinerMenu: DinerMenu) {
        this.dinerMenu = dinerMenu;
    }

    printMenu() {
        const iterator = this.dinerMenu.createIterator();
        while (iterator.hasNext()) {
            const menuItem = iterator.next();
            console.log(menuItem.getName());
            console.log(menuItem.getDescription());
            console.log(menuItem.getPrice());
        }
    }
}

반복자 패턴 정의

컬렉션의 구현 방법을 노출하지 않으면서 집합체 내의 모든 항목에 접근하는 방법을 제공합니다.

단일 역할 원칙

어떤 클래스가 바뀌는 이유는 하나뿐이어야한다. : 높은 응집도

컴포지트 패턴

메뉴 아이템 내부에 메뉴 아이템을 추가한다면?

컴포지트 패턴 정의

객첼르 트리구조로 구성해서 부분-전체 계층 구조를 구현합니다. 컴포지트 패턴을 사용하면 개별 객체와 복합 객체를 똑같은 방법으로 다룰 수 있게 됩니다.

[컴포넌트] 에서 복합 객체 내부의 모든 객체 인터페이스를 정의합니다.

[컴포지트] 에서 자식이 있는 구성요소의 행동을 정의하고 자식 구성요소를 저장합니다. add(children), remove(children), getChild(int) 와 같은 구성요소 입니다.

[리프] 에서 자식 과 관련된 기능을 상속받긴 합니다.

한클래스에서 한 역할을 해야하지 않나요?

컴포지트 패턴에서는 컴포넌트가 계층구조를 관리하는 일과 메뉴 관련 작업을 하고있어 보입니다.

컴포지트 패턴은 단일 책임 원칙을 깨는 대신 투명성을 확보합니다.

투명성이란 클라이언트가 복합 객체와 잎을 똑같은 방식으로 처리할 수 있게 하는 것입니다. ← 상황에 따라 원칙을 적절하게 사용해야 합니다.

j03y14 commented 4 months ago

투명하다 = 인터페이스가 같아서 똑같은 방식으로 처리할 수 있게 하는 것