디자인패턴 - 행동 패턴

김상운(개발둥이)·2022년 10월 25일
0

디자인패턴

목록 보기
3/3
post-thumbnail

행동 패턴이란

객체나 클래스 사이의 알고리즘이나 책임 분배에 관련된 패턴이다.
한 객체가 수행할 수 없는 작업을 여러 개의 객체로 어떻게 분배하며 객체 사이의 결합도 최소화에 중점을 둔다.

행동 패턴은 알고리즘과 객체 간 책임 할당과 관련이 있습니다. 행동 패턴은 객체와 클래스의 패턴뿐만 아니고 객체, 클래스 간에 의사소통 패턴도 설명하는데요, 이러한 패턴은 런타임에서 처리하기 어려운 복잡한 control flow들이라는 특징을 갖습니다. 이렇게 행동 패턴을 사용해서 개발자가 control flow보다는 객체가 연결되는 방식에 집중할 수 있도록 해줍니다.
출처: https://icksw.tistory.com/249 [PinguiOS:티스토리]

행동 패턴의 종류

  • 책임 연쇄 패턴
  • 커맨드 패턴
  • 인터프리터 패턴
  • 이터레이터 패턴
  • 중재자 패턴
  • 메멘토 패턴
  • 옵저버패턴
  • 상태 패턴
  • 전략 패턴
  • 템플릿 메소드 패턴
  • 비지터 패턴

책임 연쇄 패턴

  • 클라이언트로부터의 요청을 처리할 수 있는 처리객체를 집합(Chain)으로 만들어 부여함으로 결합을 느슨하기 위해 만들어진 디자인 패턴입니다.
  • 요청을 보내는 쪽(sender)과 요청을 처리하는 쪽(receiver)의 분리하는 패턴
  • 핸들러 체인을 사용해서 요청을 처리한다.

구현

Handler

public abstract class RequestHandler {

    private RequestHandler nextHandler;

    public RequestHandler(RequestHandler nextHandler) {
        this.nextHandler = nextHandler;
    }

    public void handle(Request request) {
        if (nextHandler != null) {
            nextHandler.handle(request);
        }
    }
}

ConcreteHandler

AuthRequestHandler

public class AuthRequestHandler extends RequestHandler {

    public AuthRequestHandler(RequestHandler nextHandler) {
        super(nextHandler);
    }

    @Override
    public void handle(Request request) {
        System.out.println("인증이 되었는가?");
        super.handle(request);
    }
}

LoggingRequestHandler

public class LoggingRequestHandler extends RequestHandler {

    public LoggingRequestHandler(RequestHandler nextHandler) {
        super(nextHandler);
    }

    @Override
    public void handle(Request request) {
        System.out.println("로깅");
        super.handle(request);
    }
}

PrintRequestHandler

public class PrintRequestHandler extends RequestHandler {

    public PrintRequestHandler(RequestHandler nextHandler) {
        super(nextHandler);
    }

    @Override
    public void handle(Request request) {
        System.out.println(request.getBody());
        super.handle(request);
    }
}

client 코드

public class Client {

    private RequestHandler requestHandler;

    public Client(RequestHandler requestHandler) {
        this.requestHandler = requestHandler;
    }

    public void doWork() {
        Request request = new Request("이번 놀이는 뽑기입니다.");
        requestHandler.handle(request);
    }

    public static void main(String[] args) {
        RequestHandler chain = new AuthRequestHandler(new LoggingRequestHandler(new PrintRequestHandler(null)));
        Client client = new Client(chain);
        client.doWork();
    }
}

클라이언트로 부터 요청을 처리하기 위해 체인을 사용하며 느슨한 결합을 유지한다.
요청을 처리할 수 있는 객체가 여러 개이고 처리객체가 특정적이지 않을 경우 권장되는 패턴.

장단점

  • 장점
    • 클라이언트 코드를 변경하지 않고 새로운 핸들러를 체인에 추가할 수 있다.
    • 각각의 체인은 자신이 해야하는 일만 한다.
    • 체인을 다양한 방법으로 구성할 수 있다.
  • 단점
    • 디버깅이 조금 어렵다

커맨드 패턴

  • 요청을 캡슐화 하여 호출자(invoker)와 수신자(receiver)를 분리하는 패턴.
  • 요청을 처리하는 방법이 바뀌더라도, 호출자의 코드는 변경되지 않는다.
  • 커맨드 패턴은 해당 요청에 따라야하는 기능들을 캡슐화한 객체에 정리하여 실행할 수 있게 해주는 디자인 패턴입니다.

Receiver

Game

public class Game {

    private boolean isStarted;

    public void start() {
        System.out.println("게임을 시작합니다.");
        this.isStarted = true;
    }

    public void end() {
        System.out.println("게임을 종료합니다.");
        this.isStarted = false;
    }

    public boolean isStarted() {
        return isStarted;
    }
}

Light

public class Light {

    private boolean isOn;

    public void on() {
        System.out.println("불을 켭니다.");
        this.isOn = true;
    }

    public void off() {
        System.out.println("불을 끕니다.");
        this.isOn = false;
    }

    public boolean isOn() {
        return this.isOn;
    }
}

Command

Command - 인터페이스

public interface Command {

    void execute();
    
}

GameEndCommand

public class GameEndCommand implements Command {

    private Game game;

    public GameEndCommand(Game game) {
        this.game = game;
    }

    @Override
    public void execute() {
        game.end();
    }
}

Game 타입의 객체 end 기능을 캡슐화한 객체이다.

GameStartCommand

public class GameStartCommand implements Command {

    private Game game;

    public GameStartCommand(Game game) {
        this.game = game;
    }

    @Override
    public void execute() {
        game.start();
    }
}

Game 타입의 객체 start 기능을 캡슐화한 객체이다.

LightOffCommand


public class LightOffCommand implements Command {

    private Light light;

    public LightOffCommand(Light light) {
        this.light = light;
    }

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

Light 타입의 객체 off 기능을 캡슐화한 객체이다.

LightOnCommand

public class LightOnCommand implements Command {

    private Light light;

    public LightOnCommand(Light light) {
        this.light = light;
    }

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

Light 타입의 객체 on 기능을 캡슐화한 객체이다.

Invoker

public class Button {

    private Command command;

    public Button(Command command) {
        this.command = command;
    }

    public void press() {
        command.execute();
    }

    public static void main(String[] args) {
        Button button = new Button(new GameStartCommand(new Game()));
        button.press();
        button.press();
        button.press();
    }

}

위 구조는 협력이라는 문맥에서 키고 끈다는 문맥이다.
따라서 Button 클래스는 객체들의 협력을 위해 키고/끈다는 기능을 처리해줄 수 있는 추상화된 객체를 사용한다.

장단점

  • 장점
    • 기존 코드를 변경하지 않고 새로운 커맨드를 만들 수 있다.
    • 수신자의 코드가 변경되어도 호출자의 코드는 변경되지 않는다.
    • 커맨드 객체를 로깅, DB에 저장, 네트워크로 전송 하는 등 다양한 방법으로 활용할 수도 있다.
  • 단점
    • 코드가 복잡하고 클래스가 많아진다

인터프리터 패턴

  • 문법 규칙을 클래스화 한 구조로, 일련의 규칙으로 정의된 문법적 언어를 해석하는 패턴입니다.(SQL, SHELL...) 인터프리터 패턴은 SQL과 같은 계층적 언어를 해석하기 위해 계층 구조를 표현할 수 있습니다.

  • 자주 등장하는 문제를 간단한 언어로 정의하고 재사용하는 패턴.

  • 반복되는 문제 패턴을 언어 또는 문법으로 정의하고 확장할 수 있다.

구현

Expression

PostfixExpression

public interface PostfixExpression {

    int interpret(Map<Character, Integer> context);

}

TerminalExpression

public class VariableExpression implements PostfixExpression {

    private Character variable;

    public VariableExpression(Character variable) {
        this.variable = variable;
    }

    @Override
    public int interpret(Map<Character, Integer> context) {
        return context.get(variable);
    }
}

NonTerminalExpression

PlusExpression

public class PlusExpression implements PostfixExpression {

    private PostfixExpression left;
    private PostfixExpression right;

    public PlusExpression(PostfixExpression left, PostfixExpression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public int interpret(Map<Character, Integer> context) {
        return left.interpret(context) + right.interpret(context);
    }
}

MinusExpression

public class MinusExpression implements PostfixExpression {

    private PostfixExpression left;
    private PostfixExpression right;

    public MinusExpression(PostfixExpression left, PostfixExpression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public int interpret(Map<Character, Integer> context) {
        return left.interpret(context) - right.interpret(context);
    }
}

PostfixParser

public class PostfixParser {

    //x y z + -
    public static PostfixExpression parse(String expression) {

        Stack<PostfixExpression> stack = new Stack<>();
        for (char c : expression.toCharArray()) {
            stack.push(getExpression(c, stack));
        }

        return stack.pop();
    }

    private static PostfixExpression getExpression(char c, Stack<PostfixExpression> stack) {
        switch (c) {
            case '+':
                return new PlusExpression(stack.pop(), stack.pop());
            case '-':
                PostfixExpression right = stack.pop();
                PostfixExpression left = stack.pop();
                return new MinusExpression(left, right);
            default:
                return new VariableExpression(c);
        }
    }

}

PostfixParser 클래스의 스태틱 메서드인 parse 를 통해 context 를 expression 타입의 트리구조로 표현하며, 조상 노드를 반환한다. 이후 client 코드에서 재귀적으로 호출하여 interpret 한다.

client 코드

public class App {

    public static void main(String[] args) {
        PostfixExpression expression = PostfixParser.parse("xyz+-");
        int result = expression.interpret(Map.of('x', 1, 'y', 2, 'z', 3));
        System.out.println(result);
    }

}

parser 클래스에 context 인 "xyz+-"를 전달 하여 트리구조로 된 expression 객체를 client 가 반환받는다.
그 이후 이를 재귀적으로 처리하기 위해 interpret 를 호출하는데 문자를 해석할 수 있는 map 을 전달하여 결과를 반환받는다.

장단점

  • 장점
    • 자주 등장하는 문제 패턴을 언어와 문법으로 정의할 수 있다.
    • 기존 코드를 변경하지 않고 새로운 Expression을 추가할 수 있다.
    • 단점
  • 복잡한 문법을 표현하려면 Expression과 Parser가 복잡해진다.

이터레이터 패턴

  • 집합 객체 내부 구조를 노출시키지 않고 순회 하는 방법을 제공하는 패턴. (List, Set 등의 자료형을 노출하지 않고 Iterator 만 노출시킨다.)

  • 집합 객체를 순회하는 클라이언트 코드를 변경하지 않고 다양한 순회 방법을 제공할 수 있다.

다이어그램만 보면 어려울 수 있다. 코드로 보자(간단하다.)

구현

Aggregate

Board

public class Board {

    private List<Post> posts = new ArrayList<>();

    public List<Post> getPosts() {
        return posts;
    }

    public void addPost(String content) {
        this.posts.add(new Post(content));
    }

    public Iterator<Post> getRecentPostIterator() {
        return new RecentPostIterator(this.posts);
    }

}

인터페이스 없이 구현하였다.

Post

public class Post {

    private String title;

    private LocalDateTime createdDateTime;

    public Post(String title) {
        this.title = title;
        this.createdDateTime = LocalDateTime.now();
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public LocalDateTime getCreatedDateTime() {
        return createdDateTime;
    }

    public void setCreatedDateTime(LocalDateTime createdDateTime) {
        this.createdDateTime = createdDateTime;
    }
}

post(다) 와 board(일) 는 다대일 관계와 같다.

Iterator

public class RecentPostIterator implements Iterator<Post> {

    private Iterator<Post> internalIterator;

    public RecentPostIterator(List<Post> posts) {
        Collections.sort(posts, (p1, p2) -> p2.getCreatedDateTime().compareTo(p1.getCreatedDateTime()));
        this.internalIterator = posts.iterator();
    }

    @Override
    public boolean hasNext() {
        return this.internalIterator.hasNext();
    }

    @Override
    public Post next() {
        return this.internalIterator.next();
    }
}

client 코드

public class Client {

    public static void main(String[] args) {
        Board board = new Board();
        board.addPost("디자인 패턴 게임");
        board.addPost("선생님, 저랑 디자인 패턴 하나 학습하시겠습니까?");
        board.addPost("지금 이 자리에 계신 여러분들은 모두 디자인 패턴을 학습하고 계신 분들입니다.");

        // TODO 들어간 순서대로 순회하기
        List<Post> posts = board.getPosts();

        for (int i = 0 ; i < posts.size() ; i++) {
            Post post = posts.get(i);
            System.out.println(post.getTitle());
        }

        // TODO 가장 최신 글 먼저 순회하기
        Iterator<Post> recentPostIterator = board.getRecentPostIterator();
        while(recentPostIterator.hasNext()) {
            System.out.println(recentPostIterator.next().getTitle());
        }
    }

}

board.getPosts() 를 사용하면 Collection 의 구체 타입인 List 를 사용한다는 것을 노출 시킨다.
하지만 iterator 패턴을 적용하면 어떠한 자료구조를 사용하는지 노출시키지 않고 순회할 수 있다.

장단점

  • 장점
    • 집합 객체가 가지고 있는 객체들에 손쉽게 접근할 수 있다.
    • 일관된 인터페이스를 사용해 여러 형태의 집합 구조를 순회할 수 있다.
  • 단점
    • 클래스가 늘어나고 복잡도가 증가한다.

중재자 패턴

  • 여러 객체들이 소통하는 방법을 캡슐화하는 패턴.
  • 여러 컴포넌트간의 결합도를 중재자를 통해 낮출 수 있다.
  • M:N 관계를 해당 패턴을 사용하면 M:1 관계로 만들어 복잡도를 내리므로 유지 보수 및 확장성에 유리합니다.

적용 전

Guest

public class Guest {

    private Restaurant restaurant = new Restaurant();

    private CleaningService cleaningService = new CleaningService();

    public void dinner() {
        restaurant.dinner(this);
    }

    public void getTower(int numberOfTower) {
        cleaningService.getTower(this, numberOfTower);
    }

}

Restaurant

public class Restaurant {

    private CleaningService cleaningService = new CleaningService();
    public void dinner(Guest guest) {
        System.out.println("dinner " + guest);
    }

    public void clean() {
        cleaningService.clean(this);
    }
}

CleaningService

public class CleaningService {
    public void clean(Gym gym) {
        System.out.println("clean " + gym);
    }

    public void getTower(Guest guest, int numberOfTower) {
        System.out.println(numberOfTower + " towers to " + guest);
    }

    public void clean(Restaurant restaurant) {
        System.out.println("clean " + restaurant);
    }
}

Gym

public class Gym {

    private CleaningService cleaningService;

    public void clean() {
        cleaningService.clean(this);
    }
}

Hotel

public class Hotel {

    public static void main(String[] args) {
        Guest guest = new Guest();
        guest.getTower(3);
        guest.dinner();

        Restaurant restaurant = new Restaurant();
        restaurant.clean();
    }
}

객체들의 관계가 M:N 으로 써 중구난방이다.
M개의 객체들 사이에 중재자를 추가하여 중재자가 모든 객체들의 통신을 담당하도록 변경하면 중재자 패턴이라 볼 수 있다.

적용 후

Mediator

public class FrontDesk {

    private CleaningService cleaningService = new CleaningService();

    private Restaurant restaurant = new Restaurant();

    public void getTowers(Guest guest, int numberOfTowers) {
        cleaningService.getTowers(guest.getId(), numberOfTowers);
    }

    public String getRoomNumberFor(Integer guestId) {
        return "1111";
    }

    public void dinner(Guest guest, LocalDateTime dateTime) {
        restaurant.dinner(guest.getId(), dateTime);
    }
}

Colleague

Guest

public class Guest {

    private Integer id;

    private FrontDesk frontDesk = new FrontDesk();

    public void getTowers(int numberOfTowers) {
        this.frontDesk.getTowers(this, numberOfTowers);
    }

    private void dinner(LocalDateTime dateTime) {
        this.frontDesk.dinner(this, dateTime);
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }
}

CleaningService

public class CleaningService {

    private FrontDesk frontDesk = new FrontDesk();

    public void getTowers(Integer guestId, int numberOfTowers) {
        String roomNumber = this.frontDesk.getRoomNumberFor(guestId);
        System.out.println("provide " + numberOfTowers + " to " + roomNumber);
    }
}

Restaurant

public class Restaurant {
    public void dinner(Integer id, LocalDateTime dateTime) {
		...
    }
}

DIP 가 지켜지지 않고 의존성 주입을 사용하지 않았지만 M:N 관계에서 M:1 로 복잡도를 낮추는걸 표현하였다.

장단점

  • 장점
    • 컴포넌트 코드를 변경하지 않고 새로운 중재자를 만들어 사용할 수 있다.
    • 각각의 컴포넌트 코드를 보다 간결하게 유지할 수 있다.
  • 단점
    • 중재자 역할을 하는 클래스의 복잡도와 결합도가 증가한다

메멘토 패턴

  • 캡슐화를 유지하면서 객체 내부 상태를 외부에 저장하는 방법.
  • 객체 상태를 외부에 저장했다가 해당 상태로 다시 복구할 수 있다.

구현

Originator

public class GameOriginator implements Serializable {

    private int redTeamScore;

    private int blueTeamScore;

    public int getRedTeamScore() {
        return redTeamScore;
    }

    public void setRedTeamScore(int redTeamScore) {
        this.redTeamScore = redTeamScore;
    }

    public int getBlueTeamScore() {
        return blueTeamScore;
    }

    public void setBlueTeamScore(int blueTeamScore) {
        this.blueTeamScore = blueTeamScore;
    }

    public GameSaveMemento save() {
        return new GameSaveMemento(blueTeamScore, redTeamScore);
    }

    public void restore(GameSaveMemento gameSaveMemento) {
        this.redTeamScore = gameSaveMemento.getRedTeamScore();
        this.blueTeamScore = gameSaveMemento.getBlueTeamScore();
    }

}

red, bule 팀의 스코어를 필드로 가질 수 있으며, save 메서드를 호출하여 score 에 해당하는 필드를 memento 객체로 반환할 수 있으며, restore 를 호출하여 memento 객체로 부터 score 에 해당하는 필드로 변환할 수 있다.

Memento

public final class GameSaveMemento {

    private final int blueTeamScore;

    private final int redTeamScore;

    public GameSaveMemento(int blueTeamScore, int redTeamScore) {
        this.blueTeamScore = blueTeamScore;
        this.redTeamScore = redTeamScore;
    }

    public int getBlueTeamScore() {
        return blueTeamScore;
    }

    public int getRedTeamScore() {
        return redTeamScore;
    }
}

game 의 정보를 저장할 수 있는 객체이다. 내부 필드를 final로 하고 상속을 막기 위해 클래스 레벨에 final 키워드를 붙였다.

CareTaker

public class CareTaker {

    private List<GameSaveMemento> gameSaveMementoList = new ArrayList<>();

    public void addMemento(GameSaveMemento memento) {
        this.gameSaveMementoList.add(memento);
    }

    public GameSaveMemento getMemento(int idx) {
        return gameSaveMementoList.get(idx);
    }
    
    public static void main(String[] args) {
        CareTaker careTaker = new CareTaker();
        GameOriginator originator = new GameOriginator();

        originator.setBlueTeamScore(10);
        originator.setRedTeamScore(20);

        GameSaveMemento memento1 = originator.save();

        originator.setBlueTeamScore(100);
        originator.setRedTeamScore(200);

        GameSaveMemento memento2 = originator.save();

        careTaker.addMemento(memento1);
        careTaker.addMemento(memento2);

        originator.restore(careTaker.getMemento(0));

        System.out.println(originator.getBlueTeamScore() + " " + originator.getRedTeamScore());
    }
}

Memento 를 저장할 수 있는 외부 저장소와 같다.

장단점

  • 장점

    • 캡슐화를 지키면서 상태 객체 상태 스냅샷을 만들 수 있다.
    • 객체 상태 저장하고 또는 복원하는 역할을 CareTaker에게 위임할 수 있다.
    • 객체 상태가 바뀌어도 클라이언트 코드는 변경되지 않는다.
  • 단점

    • 많은 정보를 저장하는 Mementor를 자주 생성하는 경우 메모리 사용량에 많은 영향을 줄 수 있다.

옵저버 패턴

  • 다수의 객체가 특정 객체 상태 변화를 감지하고 알림을 받는 패턴.
  • 발행(publish)-구독(subscribe) 패턴을 구현할 수 있다.

구현

client 코드

public class Client {

    public static void main(String[] args) {
        ChatServer chatServer = new ChatServer();
        User user1 = new User("keesun");
        User user2 = new User("whiteship");

        chatServer.register("오징어게임", user1);
        chatServer.register("오징어게임", user2);

        chatServer.register("디자인패턴", user1);

        chatServer.sendMessage(user1, "오징어게임", "아.. 이름이 기억났어.. 일남이야.. 오일남");
        chatServer.sendMessage(user2, "디자인패턴", "옵저버 패턴으로 만든 채팅");

        chatServer.unregister("디자인패턴", user2);

        chatServer.sendMessage(user2, "디자인패턴", "옵저버 패턴 장, 단점 보는 중");
    }
}

chatServer(publisher) 에서 관심있는 주제에 대해 user(subscriber) 를 관리하며 등록, 삭제, 메시지 전송의 기능을한다.

Subject

public class ChatServer {

    private Map<String, List<Observer>> subscribers = new HashMap<>();

    public void register(String subject, Observer observer) {
        if (this.subscribers.containsKey(subject)) {
            this.subscribers.get(subject).add(observer);
        } else {
            List<Observer> list = new ArrayList<>();
            list.add(observer);
            this.subscribers.put(subject, list);
        }
    }

    public void unregister(String subject, Observer observer) {
        if (this.subscribers.containsKey(subject)) {
            this.subscribers.get(subject).remove(observer);
        }
    }

    public void sendMessage(User user, String subject, String message) {
        if (this.subscribers.containsKey(subject)) {
            String userMessage = "보낸 사람:" + user.getName() + ", 송신 내용: " + message + " ";
            this.subscribers.get(subject).forEach(s -> s.handleMessage(userMessage));
        }
    }

}

publisher 역할을 한다.

Observer

Observer

public interface Observer {

    void handleMessage(String message);
}

User

public class User implements Observer {

    private String name;

    public User(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @Override
    public void handleMessage(String message) {
        System.out.println(message + "수신자: " + name);
    }
}

subscriber 역할을 하여, publisher 에게 다른 subscriber 의 상태 변화를 관리하게 한다.

장단점

  • 장점

    • 상태를 변경하는 객체(publisher)와 변경을 감지하는 객체(subsriber)의 관계를 느슨하게 유지할 수 있다.
    • Subject의 상태 변경을 주기적으로 조회하지 않고 자동으로 감지할 수 있다.
    • 런타임에 옵저버를 추가하거나 제거할 수 있다.
  • 단점

    • 복잡도가 증가한다.
    • 다수의 Observer 객체를 등록 이후 해지 않는다면 memory leak이 발생할 수도 있다.

상태 패턴

  • 객체 내부 상태 변경에 따라 객체의 행동이 달라지는 패턴.
  • 상태에 특화된 행동들을 분리해 낼 수 있으며, 새로운 행동을 추가하더라도 다른 행동에 영향을 주지 않는다.

구현

client 코드

public class Client {

    public static void main(String[] args) {
        OnlineCourse onlineCourse = new OnlineCourse();
        
        Student student = new Student("whiteship");
        Student keesun = new Student("keesun");
        
        keesun.addPrivate(onlineCourse);

        onlineCourse.addStudent(student);
        
        onlineCourse.changeState(new Private(onlineCourse));
        
        onlineCourse.addReview("hello", student);
        onlineCourse.addStudent(keesun);

        System.out.println(onlineCourse.getState());
        System.out.println(onlineCourse.getReviews());
        System.out.println(onlineCourse.getStudents());
    }
}

OnlineCourse 의 내부 상태(state)에 따라 students 와 reviews 를 add 하는 행동이 바뀐다.

Context

public class OnlineCourse {

    private State state = new Draft(this);

    private List<Student> students = new ArrayList<>();

    private List<String> reviews = new ArrayList<>();

    public void addStudent(Student student) {
        this.state.addStudent(student);
    }

    public void addReview(String review, Student student) {
        this.state.addReview(review, student);
    }

    public State getState() {
        return state;
    }

    public List<Student> getStudents() {
        return students;
    }

    public List<String> getReviews() {
        return reviews;
    }

    public void changeState(State state) {
        this.state = state;
    }
}

Context(문맥)에 해당하며 state 에 따라 객체의 해동이 바뀐다. changeStage() 메서드를 사용하여 객체의 해동을 바꿀 수 있다.

State

Draft

public class Draft implements State {

    private OnlineCourse onlineCourse;

    public Draft(OnlineCourse onlineCourse) {
        this.onlineCourse = onlineCourse;
    }

    @Override
    public void addReview(String review, Student student) {
        throw new UnsupportedOperationException("드래프트 상태에서는 리뷰를 남길 수 없습니다.");
    }

    @Override
    public void addStudent(Student student) {
        this.onlineCourse.getStudents().add(student);
        if (this.onlineCourse.getStudents().size() > 1) {
            this.onlineCourse.changeState(new Private(this.onlineCourse));
        }
    }
}

Private

public class Private implements State {

    private OnlineCourse onlineCourse;

    public Private(OnlineCourse onlineCourse) {
        this.onlineCourse = onlineCourse;
    }

    @Override
    public void addReview(String review, Student student) {
        if (this.onlineCourse.getStudents().contains(student)) {
            this.onlineCourse.getReviews().add(review);
        } else {
            throw new UnsupportedOperationException("프라이빗 코스를 수강하는 학생만 리뷰를 남길 수 있습니다.");
        }
    }

    @Override
    public void addStudent(Student student) {
        if (student.isAvailable(this.onlineCourse)) {
            this.onlineCourse.getStudents().add(student);
        } else {
            throw new UnsupportedOperationException("프라이빛 코스를 수강할 수 없습니다.");
        }
    }
}

Published

public class Published implements State {

    private OnlineCourse onlineCourse;

    public Published(OnlineCourse onlineCourse) {
        this.onlineCourse = onlineCourse;
    }

    @Override
    public void addReview(String review, Student student) {
        this.onlineCourse.getReviews().add(review);
    }

    @Override
    public void addStudent(Student student) {
        this.onlineCourse.getStudents().add(student);
    }
}

  • 장점

    • 상태에 따른 동작을 개별 클래스로 옮겨서 관리할 수 있다.
    • 기존의 특정 상태에 따른 동작을 변경하지 않고 새로운 상태에 다른 동작을 추가할 수 있다.
    • 코드 복잡도를 줄일 수 있다.
  • 단점

    • 복잡도가 증가한다.

전략 패턴

  • 여러 알고리듬을 캡슐화하고 상호 교환 가능하게 만드는 패턴.
  • 컨텍스트에서 사용할 알고리듬을 클라이언트 선택한다.

구현

client 코드

public class Client {

    public static void main(String[] args) {
        BlueLightRedLight blueLightRedLight = new BlueLightRedLight(new NormalSpeed());

        blueLightRedLight.blueLight();
        blueLightRedLight.redLight();

        blueLightRedLight.setSpeedStrategy(new FastSpeed());

        blueLightRedLight.blueLight();
        blueLightRedLight.redLight();
    }

}

Context

public class BlueLightRedLight {

    private SpeedStrategy speedStrategy;

    public BlueLightRedLight(SpeedStrategy speedStrategy) {
        this.speedStrategy = speedStrategy;
    }

    public void blueLight() {
        speedStrategy.blueLight();
    }

    public void redLight() {
        speedStrategy.redLight();
    }

    public void setSpeedStrategy(SpeedStrategy speedStrategy) {
        this.speedStrategy = speedStrategy;
    }
}

상태 패턴과 유사하다. 다만 내부 상태에 따라 기능을 변경하는 것이 아닌, strategy 에 따라 알고리즘을 교체하는 방식이다.

Strategy

FastSpeed

public class FastSpeed implements SpeedStrategy {

    @Override
    public void blueLight() {
        System.out.println("무궁화꽃이");
    }

    @Override
    public void redLight() {
        System.out.println("피었습니다.");
    }
}

NormalSpeed

public class NormalSpeed implements SpeedStrategy {

    @Override
    public void blueLight() {
        System.out.println("무 궁 화 꽃 이");
    }

    @Override
    public void redLight() {
        System.out.println("피 었 습 니 다.");
    }
}

장단점

  • 장점

    • 새로운 전략을 추가하더라도 기존 코드를 변경하지 않는다.
    • 상속 대신 위임을 사용할 수 있다.
    • 런타임에 전략을 변경할 수 있다.
  • 단점

    • 복잡도가 증가한다.
    • 클라이언트 코드가 구체적인 전략을 알아야 한다.

템플릿 메서드 패턴

  • 알고리듬 구조를 서브 클래스가 확장할 수 있도록 템플릿으로 제공하는 방법.
  • 추상 클래스는 템플릿을 제공하고 하위 클래스는 구체적인 알고리듬을 제공한다.

구현

client 코드

AbstractClass

public abstract class FileProcessor {

    private String path;
    public FileProcessor(String path) {
        this.path = path;
    }

    public int process() {
        try(BufferedReader reader = new BufferedReader(new FileReader(path))) {
            int result = 0;
            String line = null;
            while((line = reader.readLine()) != null) {
                result = getResult(result, Integer.parseInt(line));
            }
            return result;
        } catch (IOException e) {
            throw new IllegalArgumentException(path + "에 해당하는 파일이 없습니다.", e);
        }
    }

    protected abstract int getResult(int result, int number);
}

구체적인 로직, 알고리즘을 서브 클래스에 위임하며 전체적인 구조는 슈퍼 클래스를 사용한다.

ConcreteClass

MultiplyFileProcessor

public class MultiplyFileProcessor extends FileProcessor {

    public MultiplyFileProcessor(String path) {
        super(path);
    }

    @Override
    protected int getResult(int result, int number) {
        if (result == 0) result = 1;
        return result *= number;
    }
}

PlusFileProcessor

public class PlusFileProcessor extends FileProcessor {

    public PlusFileProcessor(String path) {
        super(path);
    }

    @Override
    protected int getResult(int result, int number) {
        return result += number;
    }
}

장단점

  • 장점

    • 템플릿 코드를 재사용하고 중복 코드를 줄일 수 있다.
    • 템플릿 코드를 변경하지 않고 상속을 받아서 구체적인 알고리듬만 변경할 수 있다.
  • 단점

    • 리스코프 치환 원칙을 위반할 수도 있다.
    • 알고리듬 구조가 복잡할 수록 템플릿을 유지하기 어려워진다.

비지터 패턴

  • 알고리즘을 객체 구조에서 분리시키는 디자인 패턴이다.
  • 기존 코드를 변경하지 않고 새로운 기능을 추가하는 방법.

구현

Client 코드

public class Client {

    public static void main(String[] args) {
        Device device1 = new Phone();
        Device device2 = new Watch();

        Shape rectangle = new Rectangle();
        rectangle.accept(device1);
        rectangle.accept(device2);

        Shape triangle = new Triangle();
        triangle.accept(device1);
        triangle.accept(device2);

        Shape circle = new Circle();
        circle.accept(device1);
        circle.accept(device2);

    }
}

Shape 타입의 객체의 구조에서 알고리즘을 분리하여 Element 에서 각 Shape 타입의 구체적인 타입에 맞게 행동한다.

Element

Shape

public interface Shape {

    void accept(Device device);

}

객체 구조에 해당한다.

Rectangle

public class Rectangle implements Shape {

    @Override
    public void accept(Device device) {
        device.print(this);
    }
}

Circle

public class Circle implements Shape {

    @Override
    public void accept(Device device) {
        device.print(this);
    }
}

Triangle

public class Triangle implements Shape {

    @Override
    public void accept(Device device) {
        device.print(this);
    }
}

Visitor

Shape

public interface Shape {

    void accept(Device device);

}

알고리즘에 해당하는 타입이다.

Phone

public class Phone implements Device {

    @Override
    public void print(Triangle triangle) {
        System.out.println("삼각형 폰");
    }

    @Override
    public void print(Rectangle rectangle) {
        System.out.println("사각형 폰");
    }

    @Override
    public void print(Circle circle) {
        System.out.println("동그라미 폰");
    }
}

Watch

public class Watch implements Device {

    @Override
    public void print(Triangle triangle) {
        System.out.println("삼각형 시계");
    }

    @Override
    public void print(Rectangle rectangle) {
        System.out.println("사각형 시계");
    }

    @Override
    public void print(Circle circle) {
        System.out.println("동그라미 시계");
    }
}

장단점

  • 장점

    • 기존 코드를 변경하지 않고 새로운 코드를 추가할 수 있다.
    • 추가 기능을 한 곳에 모아둘 수 있다.
  • 단점

    • 복잡하다.
    • 새로운 Element를 추가하거나 제거할 때 모든 Visitor 코드를 변경해야 한다.
profile
공부한 것을 잊지 않기 위해, 고민했던 흔적을 남겨 성장하기 위해 글을 씁니다.

0개의 댓글