[F-Lab 모각코 챌린지 18일차] 디자인 패턴 (continue)

부추·2023년 6월 18일
0

F-Lab 모각코 챌린지

목록 보기
18/66

TIL

  1. 커맨드 패턴 : 요청과 처리 분리
  2. 어댑터 패턴 : 외부 인터페이스를 클라이언트에 맞게 사용 (+퍼사드 패턴)
  3. 템플릿 메소드 패턴 : 상위 클래스의 로직 흐름을 하위 클래스에서 구현
  4. Iterator 패턴, 컴포지트 패턴 : 구성 요소에 일관성있는 접근 인터페이스 제공

1. 커맨드 패턴

외부에서 제공하는 기능을 일정한 형식의 인터페이스 아래에서 사용하고 싶다. 책에서 나온 예제를 빌리자면, 여러가지 IoT 기능을 리모컨 하나에 담고 싶은 상황이다.

외부 전자기기는 CeilingFan, Light, GarageDoor, Sprinkler 등이 있다.

public class Light {
    public void on() {
        System.out.println("불 켭니다!");
    }

    public void off() {
        System.out.println("불 끕니다!");
    }
}

public class CeilingFan {
    public void high() {
        System.out.println("천장 선풍기를 세게 틉니다!");
    }
    public void low() {
        System.out.println("천장 선풍기를 낮게 틉니다!");
    }

    public void off() {
        System.out.println("천장 선풍기를 끕니다!");
    }
}

public class Sprinkler {
    public void waterOn() {
        System.out.println("물을 틉니다!");
    }
    public void waterOff() {
        System.out.println("물을 끕니다!");
    }
}

public class GarageDoor {
    public void up() {
        System.out.println("차고 문 엽니다!");
    }

    public void down() {
        System.out.println("차고 문 닫습니다!");
    }
}

리모컨 객체 RemoteControl을 만들 것인데, 특정 인덱스의 버튼을 누르는 것으로 위 객체들의 메소드들을 전부 이용하고 싶다.

일단 리모컨을 눌렀을 때 일정한 기능이 수행되는 여러 개의 Command를 Composition으로 둘 것이다. 인터페이스로 작성했다.

@FunctionalInterface
public interface Command {
    public void execute();
}

execute() 메소드 하나만 들어있어 @FunctionalInterface 어노테이션을 추가했다. execute() 메소드를 호출하는 것이 곧, 리모컨 버튼(slot)을 누르는 것이다.

public class RemoteControl {
    private final List<Command> commands = new ArrayList<>();

    public void addCommand(Command command) {
        commands.add(command);
    }
    public int getCommandsNum() {
        return commands.size();
    }

    public void pressCommandOfIndex(int slot) {
        if (slot<0 || slot>=commands.size()) {
            throw new RuntimeException("인덱스에 없는 커맨드입니다!");
        }
        commands.get(slot).execute();
    }

    public String getCommandOfIndex(int slot) {
        if (slot<0 || slot>=commands.size()) {
            throw new RuntimeException("인덱스에 없는 커맨드입니다!");
        }
        return commands.get(slot).getClass().getName();
    }
}

commands에 들어갈 객체를 어떻게 구성해야 할까? 커맨드 패턴을 구성하기 위해 메인으로 고민해야 하는 문제다.


개인적으로 이 다다음에 정리할 어댑터 패턴과 비슷하다 생각하는데, 외부 객체 각각의 동작을 execute() 안에 두는 것으로 구현할 수 있다. Command로 담고 싶은 메소드/기능의 개수만큼 Command를 구현하는 객체를 만든다. 가령, Light 클래스의 on(), off() 메소드를 호출하는 각각의 인터페이스 구현체를 만드는 것이다.

@RequiredArgsConstructor
public class LightOnCommand implements Command {
    private final Light light;

    @Override
    public void execute() {
        light.on();
    }
}

@RequiredArgsConstructor
public class LightOffCommand implements Command {
    private final Light light;

    @Override
    public void execute() {
        light.off();
    }
}

클래스가 폭발했군요!!

작성한 두 개의 클래스를 리모컨 객체인RemoteControl에 추가하고, 각 인덱스의 버튼을 눌러보자.

public class RemoteControlTest {
    public static void main(String[] args) {
        // make externals
        Light light = new Light();

        // buy 리모컨
        RemoteControl remoteControl = new RemoteControl();

        // set commands
        remoteControl.addCommand(new LightOnCommand(light));
        remoteControl.addCommand(new LightOffCommand(light));

        // execute!!!!!
        for (int i=0; i<remoteControl.getCommandsNum(); i++) {
            remoteControl.pressCommandOfIndex(i);
        }
    }
}

결과는?

불 켭니다!
불 끕니다!

뭐.. 잘 됐다. 위 예시에서 Command 객체는 함수형 인터페이스이므로 addCommand 메소드를 다음과 같이 호출해도 된다.

remoteControl.addCommand(light::on);




2. 어댑터 패턴

외부 인터페이스 코드를 클라이언트 프로젝트 인터페이스에 맞게 이용하고 싶을 때 사용하는 디자인 패턴이다.

구현 아이디어 자체는 간단하다. 어댑터 클래스는 클라이언트 인터페이스를 구현하면서 구성요소로 외부 인터페이스 객체를 가진다. 그리고 인터페이스에서 구현한 메소드에 외부 인터페이스 객체의 메소드를 호출하면 된다.

어댑터 패턴을 이미 안다면 고개를 끄덕이겠지만.. 이런건 실제 예시를 들어야한다. 필기 도구가 있다. 보통은 연필을 이용해 필기하거나, 문명의 이기를 즐기는 애들은 아이패드 필기를 이용하기도 한다. 그런 필기구들은 write() 메소드를 공통으로 가지며, 이는 SchoolUtil이라는 인터페이스를 구현한다고 생각해보자.

public interface SchoolUtil {
    public void write();
}

public class Pencil implements SchoolUtil {
    @Override
    public void write() {
        System.out.println("연필로 필기합니다.");
    }
}

public class IPad implements SchoolUtil {
    @Override
    public void write() {
        System.out.println("아이패드로 필기합니다.");
    }
}

흠잡을 곳 없이 간단한 구조의 필기도구들이다. SchoolUtil은 아래와 같이 사용한다.

public class SchoolStudy {
    public static void main(String[] args) {
        List<SchoolUtil> utils = new ArrayList<>();
        utils.add(new IPad());
        utils.add(new Pencil());

        for (SchoolUtil util : utils) {
            util.write();
        }
    }
}

그런데 이럴 수가! 엄마가 맥북을 사줬다. 이제 아픈 팔을 붙잡고 글자를 일일이 쓰는 날들은 안녕이다. 타이핑으로 교수의 말을 하나도 빠짐없이 다 필기하고자 한다.

public class MacBook {
    public void typing() {
        System.out.println("타자로 타이핑합니다.");
    }
}

하지만 기존의 내 utils 리스트에 MacBook을 추가할 방법이 없다. 맥북을 위해서 내 모든 것을 바꿔야 할 지경이다. 어디서부터 손대야할지 감도 안잡힌다. 맥북을 SchoolUtil에 욱여넣는 작업이 필요할 것 같다. 어댑터 패턴의 초반에 했던 설명을 그대~로 코드로 옮긴 결과다.

@RequiredArgsConstructor
public class MacBookAdapter implements SchoolUtil {
    private final MacBook macBook;

    @Override
    public void write() {
        macBook.typing();
    }
}

MacBookAdapter 구성요소로 MacBook 객체를 집어넣으면, SchoolUtil 인터페이스가 가진 write() 메소드를 사용할 수 있다. 나의 main() 코드는 다음과 같이 바뀐다.

public class SchoolStudy {
    public static void main(String[] args) {
        List<SchoolUtil> utils = new ArrayList<>();
        utils.add(new IPad());
        utils.add(new Pencil());

        MacBook macBook = new MacBook();
        utils.add(new MacBookAdapter(macBook));

        for (SchoolUtil util : utils) {
            util.write();
        }
    }
}

macBook 인스턴스를 MacBookAdapter로 감싸 리스트에 넣어주면, write()를 그대로 사용할 수 있다!




3. 템플릿 메소드 패턴

여러 하위 클래스가 가진 중복된 알고리즘이 있다. 예를 들어서, 아이돌 팬들은 콘서트에 가기 위해 다음과 같은 과정을 거친다. BTS의 팬이든지, 블랙핑크의 팬이든지, 아래의 "템플릿"은 크게 바뀌지 않을 것이다.

티켓팅을 한다
응원봉을 준비한다
콘서트장에 간다
큰 소리로 응원한다

반복이다. 객체 지향에서 반복은 최대한 묶어서 따로 빼는게 정석이다. 저 4단계의 "알고리즘"을 하나로 묶었다.

public void goConcert() {
    ticketing();
    preparefanStick();
    goArena();
    fanChatting();
 }

이것이 템플릿 메소드다.

그치만 BTS 팬은 BTS 콘서트의 티켓팅을, 블랙핑크의 팬은 블랙핑크의 티켓팅을 할 것이다. 준비할 응원봉 종류도 다를 것이고, 콘서트장도 다르다. 응원법도 당연히 다를 것! 위와 같은 템플릿은 같지만, 상세한 구현 내용은 다르다. 이는 하위 메소드에서 구현토록 한다.

대충 어떻게 설계해야할지 감이 잡힌다. 아이돌 팬을 뜻하는 클래스 Fan, BTS와 블랙핑크 각각의 팬을 뜻하는 Army, Blink 클래스를 다음과 같이 구현했다.

public abstract class Fan {
    public void goConcert() {
        ticketing();
        preparefanStick();
        goArena();
        fanChatting();
    }

    protected abstract void ticketing() {
    	System.out.println("티켓팅을 합니다.");
    }
    protected abstract void preparefanStick();
    protected void goArena() {
        System.out.println("공연장에 갑니다.");
    }

    protected abstract void fanChatting();
}

public class Blink extends Fan {
    @Override
    protected void preparefanStick() {
        System.out.println("블링봉을 준비합니다.");
    }

    @Override
    protected void fanChatting() {
        System.out.println("블랙핑크 짱!! 외칩니다!!");
    }
}

public class Army extends Fan {
    @Override
    protected void preparefanStick() {
        System.out.println("아미밤을 준비합니다.");
    }

    @Override
    protected void fanChatting() {
        System.out.println("방탄소년단 짱!!! 외칩니다!!");
    }
}

일부 메소드는 상위 클래스에서 역시 구현할 수 있음을 알리기 위해 goArena()는 상위 클래스에 그냥 작성했다. Fan 추상 클래스를 구현하면서, 전체적인 동작의 흐름은 같지만 상세 구현은 다른 "콘서트 가기"라는 행위를 템플릿 메소드로 구현해보았다.

public class FanClient {
    public static void main(String[] args) {
        Fan army = new Army();
        Fan blink = new Blink();

        army.goConcert();
        blink.goConcert();
    }
}

같은 Fan 객체로서 goConcert()를 각각 호출했지만, 둘의 결과는 다르다.




4. Iterator 패턴

책에서 말하길, "반복자" 패턴이라는데 사실 반복자보다 Iterator라는 단어가 더 직관적이며 이해하기 쉽고 실용적이어서 원어 그대로 Iterator 패턴이라고 하겠다.

Iterator 패턴에서는 객체 컬렉션에 대한 편의를 제공한다. 클래스 내부 구성요소로 Collection, 혹은 array를 담고있을 때 이를 편하게 iterate할 수 있게 해준다. 좀 더 풀어 설명하자면 각 요소들을 돌면서 일련의 로직을 수행할 수 있도록 한다는 뜻이다.

자바에서 Iterator 패턴을 구현하는 법은 간단하다. Iterable 인터페이스의 iterator() 메소드를 구현하기만 하면 된다. 이건 예제를 만들기가 좀 애매해서.. Iterable을 구현하는 자바 컬렉션 프레임워크를 살펴보도록 하자.

Iterable<T>Iterator 객체를 반환한다. Iterator 인터페이스는 간단히 표현하면 아래 구조를 갖는다.

public interface Iterator<T> {
	boolean hasNext();
    E next();
}

대충 어떤 구조인지 알 것이다. hasNext() 결과가 false가 될 때까지 Iterator을 구현한 객체 내부의 컬렉션 요소 각각의 값을 반환하도록 할 수 있다.

Iterable 인터페이스를 구현한 객체는 자바의 향상된 for문을 사용할 수 있다.

for (int i : intArrayList) {
	// do something ...
}

저 코드 내부적으로 Iterator 객체의 hasNext()next()를 반복적으로 호출하고 있는 것이다.



REFERENCE

헤드퍼스트 디자인패턴 6, 7, 8, 9장

profile
부추튀김인지 부추전일지 모를 정도로 빠싹한 부추전을 먹을래

0개의 댓글