모르지 않다는 것은 아는것과 다르다.

객체지향

행위 패턴 (디자인 패턴)

채마스 2022. 5. 15. 01:20

행위 패턴

  • 반복적으로 사용되는 객체들의 상호작용을 패턴화한 것으로, 쿨래스나 객체들이 상호작용하는 방법과 책임을 분산하는 방법을 제공한다.
  • 행위 패턴은 행위 관련 패턴을 사용하여 독립적으로 일을 처리하고자 할때 사용
  • 종료
    • Strategy
    • Template Method
    • Observer
    • Chain of responsibility
    • Interpreter
    • Iterator
    • Visitor
    • Command
    • Mediator
    • State
    • Memento

 

Strategy Pattern

  • 정책이나 알고리즘을 교체하여 사용한다.
  • 보통 실무에서 많이 사용되는 패턴이다.
  • 같은 메서드를 경우에 따라 다르게 구현해서 사용하는 방식이다.
  • 다양한 알고리즘이 존재하면 이들 각각을 하나의 클래스로 캡슐화하여 알고리즘의 대체가 가능하도록 한다.

 

class diagram

  • Strategy: 정책이 수행해야 하는 기능들을 인터페이스로 선언한다.
  • ConcreteStrategy: Strategy에 선언된 여러 기능들을 구현한다.
  • Context
    • 어떤 ConcreteStrategy 가 수행 될 것인지에 따라 정책을 선택한다.
    • Strategy에 선언된 메서드 기반으로 접근한다.
    • Strategy 클래스와 Context 클래스는 선택한 알고리즘이 동작하도록 협력한다.

 

예제

public interface EmailProvider {
    String getEmail(User user);
}
  • 위의 interface 는 Abstract 메소드 1개만을 가지고 있기 때문에 Functional Interface 이다.
  • 다음으로는 위의 인터페이스를 구현한 2개의 Provider 를 구현한다.
public class VerifyYourEmailAddressEmailProvider implements EmailProvider {

    @Override
    public String getEmail(User user) {
        return "'Verify Your Email Address' email for " + user.getName();
    }

}
public class MakeMoreFriendsEmailProvider implements EmailProvider {
    @Override
    public String getEmail(User user) {
        return "'Make More Friends' email for " + user.getName();
    }
}
  • 이제 아래와 같이 Strategy Pattern 을 적용할 수 있다. (User 와 관련된 코드는 생략한다.)
emailSender.setEmailProvider(verifyYourEmailAddressEmailProvider); // 전략 주입
users.stream()
    .filter(user -> !user.isVerified())
    .forEach(emailSender::sendEmail);

emailSender.setEmailProvider(makeMoreFriendsEmailProvider); // 전략 변경
users.stream()
    .filter(User::isVerified)
    .filter(user -> user.getFriendUserIds().size() <= 5)
    .forEach(emailSender::sendEmail);
  • 전략이 변경됨에 따라 sendEmail 메소드의 결과 값이 달라진다.
  • 하지만 이렇게 전략을 만들때마다 클래스를 만들어 줘야할까? -> EmailProvider 가 Functional Interface 이기 때문에 클래스를 따로 구현하지 않고 아래와 같이 람다식으로 대체할 수 있다.

 

Template Method Pattern

  • 상위 클래스에서 전체적인 흐름을 구현하고 상세한 처리는 하위 클래스에게 위임한다.
  • 템플릿 메소드는 코드 재사용을 위한 기본 기술이다.
  • 프레임워크에서 가장 많이 사용되는 패턴 중 하나이다.

 

class diagram

  • AbstractClass: 서브 클래스들이 반드시 구현해야 하는 알고리즘 처리 단계 내의 기본 오퍼레이션이 무엇인지를 정의한다. 서브 클래스에서 이들 오퍼레이션들을 구현한다.
  • ConcreteClass: 상위 클래스에서 선언된 추상 메서드를 구현하거나 이미 구현된 메서드를 재정의한다

 

예제

public abstract class PlayerLevel {

    public abstract void run();
    public abstract void jump();
    public abstract void turn();
    public abstract void showLevelMessage();

    final public void go(int count)
    {
        run();
        for(int i=0; i<count; i++){
            jump();
        }
        turn();
    }

    public void fly() {}
}
  • template method 인 go 메서드는 하위 클래스에서 재정의 할 수 없도록 final 로 선언한다.
  • fly() 메서드는 {} 가 있기 때문에 abstract 메소드가 아니다.
  • 그렇기 때문에 하위 클래스에서 재정의 할 수 있고, 이러한 메서드는 훅메서드라 한다.
  • 훅 메서드는 하위 클래스에서 구현해도 되고 안해도 되는 경우에 사용된다.
public class BeginnerLevel extends PlayerLevel{

    @Override
    public void run() {
        System.out.println("천천히 달립니다.");

    }

    @Override
    public void jump() {
        System.out.println("Jump 할 줄 모르지롱.");
    }

    @Override
    public void turn() {
        System.out.println("Turn 할 줄 모르지롱.");        
    }

    @Override
    public void showLevelMessage() {
        System.out.println("***** 초보자 레벨 입니다. *****");
    }

}
public class AdvancedLevel extends PlayerLevel{
    @Override
    public void run() {
        System.out.println("빨리 달립니다.");

    }

    @Override
    public void jump() {
        System.out.println("높이 jump 합니다.");
    }

    @Override
    public void turn() {
        System.out.println("Turn 할 줄 모르지롱.");        
    }

    @Override
    public void showLevelMessage() {
        System.out.println("***** 중급자 레벨 입니다. *****");
    }
}
public class SuperLevel extends PlayerLevel{
    @Override
    public void run() {
        System.out.println("엄청 빨리 달립니다.");

    }

    @Override
    public void jump() {
        System.out.println("아주 높이 jump 합니다.");
    }

    @Override
    public void turn() {
        System.out.println("한 바퀴 돕니다.");        
    }

    @Override
    public void showLevelMessage() {
        System.out.println("***** 고급자 레벨 입니다. *****");
    }

}
public class Player {

    private PlayerLevel level;

    public Player()
    {
        level= new BeginnerLevel();
        level.showLevelMessage();
    }

    public PlayerLevel getLevel() {
        return level;
    }

    public void upgradeLevel(PlayerLevel level) {
        this.level = level;
        level.showLevelMessage();
    }

    public void play(int count){
        level.go(count);
    }
}
  • Player 객체는 생성될때, beginnerLevel 로 시작할 것이기 때문에 생성자에서 beginnerLevel로 생성해준다.
public class MainBoard {

    public static void main(String[] args) {

        Player player = new Player();
        player.play(1);
        AdvancedLevel aLevel = new AdvancedLevel();
        player.upgradeLevel(aLevel);
        player.play(2);
        SuperLevel sLevel = new SuperLevel();
        player.upgradeLevel(sLevel);
        player.play(3);

    }
}
  • 다음과 같이 AdvancedLevel, SuperLevel 로 업그래드하면 다른 결과값이 나오는 것을 확인할 수 있다.

 

Observer Pattern

  • 객체 사이에 일대다의 의존 관계가 있고, 어떤 객체의 상태변하게 되면 그 객체에 의존성을 가진 다른 객체들이 변화의 통지(notify or update)를 받고 자동으로 갱신될 필요가 있을때 사용하면 좋은 패턴이다.
  • 하나의 객체에 연동되는 여러 객체 집합이 있을 때 변화에 대한 일관성은 유지하고, 객체간의 결합도는 낮게하기 위한 패턴이다.
  • 변화에 관심이 있는 객체에 대한 가정없이 통보될 수 있어야 한다.

 

Class diagram

  • Subject: Observer를 알고 있는 주체, Observer를 더하거나 뺄 수 있다.
  • Observer: Subject의 변화에 관심을 가지는 객체, 갱신에 필요한 인터페이스 정의, 객체들의 일관성을 유지한다.
  • ConcreteSubject: ConcreteObserver에게 알려주어야하는 상태가 변경될때 통보한다. (주로 List로 Observer관리)
  • ConcreteObserver: 객체에 대한 참조자를 관리하고, Subject의 일관성 유지하며, Subject가 변경될 때 갱신되는 인터페이스를 구현한다.

 

예제 코드

public interface Observer {
    public abstract void update(NumberGenerator generator);
}
  • class diagram 에서 Observer 에 해당된다.
public abstract class NumberGenerator {
    private List<Observer> observers = new ArrayList<Observer>();        // Observer들을 보관
    public void addObserver(Observer observer) {    // Observer를 추가
        observers.add(observer);
    }
    public void deleteObserver(Observer observer) { // Observer를 삭제
        observers.remove(observer);
    }
    public void notifyObservers() {               // Observer에 통지
        Iterator<Observer> it = observers.iterator();
        while (it.hasNext()) {
            Observer o = it.next();
            o.update(this);
        }
    }
    public abstract int getNumber();                // 수를 취득한다.
    public abstract void execute();                 // 수를 생성한다.
}
  • class diagram 에서 Subject에 해당된다.
  • 위와 같이, Subject와 Observer간의 추상적인 결합만이 존재한다.
  • 위와 같이 Observer 를 리스트 형태로 가지고 있는 것을 확인할 수 있다.
  • notifyObservers 메서드에서 Observer 를 순회하면서 update 를 친다.
public class RandomNumberGenerator extends NumberGenerator {
    private Random random = new Random();   // 난수발생기
    private int number;                     // 현재의 수
    public int getNumber() {                // 수를 취득한다.
        return number;
    }
    public void execute() {
        for (int i = 0; i < 20; i++) {
            number = random.nextInt(50);
            notifyObservers();
        }
    }
}
  • class diagram 에서 ConcreteSubject에 해당된다.
  • execute 메서드를 호출해서 notifyObservers 를 호출하고 notifyObservers 메서드에서 Observer 를 순회하며 update 를 친다.
public class DigitObserver implements Observer {
    public void update(NumberGenerator generator) {
        System.out.println("DigitObserver:" + generator.getNumber());
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
    }
}
  • class diagram 에서 ConcreteObserver에 해당된다.
  • NumberGenerator(Subject) 를 매개변수로 가지고 있는 것을 확인할 수 있다.
public class GraphObserver implements Observer {
    public void update(NumberGenerator generator) {
        System.out.print("GraphObserver:");
        int count = generator.getNumber();
        for (int i = 0; i < count; i++) {
            System.out.print("*");
        }
        System.out.println("");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
    }
}
  • class diagram 에서 ConcreteObserver에 해당된다.
public class Main {
    public static void main(String[] args) {
        NumberGenerator generator = new RandomNumberGenerator();
        Observer observer1 = new DigitObserver();
        Observer observer2 = new GraphObserver();
        generator.addObserver(observer1);
        generator.addObserver(observer2);
        generator.execute();
    }
}

 

Chain of Responsibility Pattern

  • 어떠한 객체의 책임만으로 해결하기 힘든 문제를 책임을 떠넘겨서 해결할 수 있을 경우 사용하면 좋은 패턴이다.
  • 다수의 객체를 사슬처럼 연결한다.
  • 요청을 처리할 수 있는 기회를 하나 이상의 객체에게 부여한다.
  • 요청을 해결할 객체를 만날 때까지 객체 고리를 따라서 요청을 전달한다.
  • 메세지를 보내는 객체와 이를 받아서 처리하는 객체들 간의 결합도를 줄일 수 있다.
  • 연결순서는 상황에 따라 바뀌거나 추가 삭제될 수 있다. 즉 객체의 책임을 추가, 변경, 확장할 수 있다.
  • 하지만, 메세지가 항상 수신된다는것을 보장할 수 없다.

 

Class diagram

  • Handler
    • 요청을 처리하는 인터페이스를 정의하고, 다음 번 처리자와의 연결을 구현한다.
    • 연결고리에 연결된 다음 객체에게 다시 메세지를 보낸다.
  • ConcreteHandler
    • 책임져야 할 메세지를 처리한다.
    • 처리못하는 메세지는 다음 수신자에게 전달한다.
  • Client
    • ConcreteHandler 객체에게 필요한 요청을 보낸다.

 

예제 코드

public abstract class Support {
    private String name;                    // 트러블 해결자의 이름
    private Support next;                   // 떠넘기는 곳
    public Support(String name) {           // 트러블 해결자의 생성
        this.name = name;
    }
    public Support setNext(Support next) {  // 떠넘길 곳을 설정
        this.next = next;
        return next;
    }
    public final void support(Trouble trouble) {  // 트러블 해결 순서
        if (resolve(trouble)) {
            done(trouble);
        } else if (next != null) {
            next.support(trouble);
        } else {
            fail(trouble);
        }
    }
    public String toString() {              // 문자열 표현
        return "[" + name + "]";
    }
    protected abstract boolean resolve(Trouble trouble); // 해결용 메소드
    protected void done(Trouble trouble) {  // 해결
        System.out.println(trouble + " is resolved by " + this + ".");
    }
    protected void fail(Trouble trouble) {  // 미해결
        System.out.println(trouble + " cannot be resolved.");
    }
}
  • class diagram 에서 Handler 에 해당된다.
  • Chain 을 구현하기 위해서 setNext 의 반환값을 Support 자기자신으로 설정한다.
  • support 는 오버라이드 할 수 없도록 final 로 구현했다. (Template Method Pattern)
  • support 메서드는 문제를 해결하면 종료하고 해결하지 못하면 next 의 support 를 호출한다.
  • next 가 없을 때까지 진행했음에도 해결하지 못하면 실패한다.
public class LimitSupport extends Support {
    private int limit;                              // 이 번호 미만이면 해결 할수 있다.
    public LimitSupport(String name, int limit) {   // 생성자
        super(name);
        this.limit = limit;
    }
    protected boolean resolve(Trouble trouble) {         // 해결용 메소드
        if (trouble.getNumber() < limit) {
            return true;
        } else {
            return false;
        }
    }
}
  • class diagram 에서 ConcreteHandler에 해당된다.
public class SpecialSupport extends Support {
    private int number;                                 // 이 번호만 해결할 수 있다.
    public SpecialSupport(String name, int number) {    // 생성자
        super(name);
        this.number = number;
    }
    protected boolean resolve(Trouble trouble) {     // 해결용 메소드 
        if (trouble.getNumber() == number) {
            return true;
        } else {
            return false;
        }
    }
}
public class OddSupport extends Support {
    public OddSupport(String name) {                // 생성자
        super(name);
    }
    protected boolean resolve(Trouble trouble) {    // 해결용 메소드
        if (trouble.getNumber() % 2 == 1) {
            return true;
        } else {
            return false;
        }
    }
}
public class NoSupport extends Support {
    public NoSupport(String name) {
        super(name);
    }
    protected boolean resolve(Trouble trouble) {     // 해결용 메소드
        return false; // 자신은 아무 처리도 하지 않는다.
    }
}
public class Main {
    public static void main(String[] args) {
        Support alice   = new NoSupport("Alice");
        Support bob     = new LimitSupport("Bob", 100);
        Support charlie = new SpecialSupport("Charlie", 429);
        Support diana   = new LimitSupport("Diana", 200);
        Support elmo    = new OddSupport("Elmo");
        Support fred    = new LimitSupport("Fred", 300);
        // 연쇄의 형성
        alice.setNext(bob).setNext(charlie).setNext(diana).setNext(elmo).setNext(fred);
        // 다양한 트러블 발생
        for (int i = 0; i < 500; i += 33) {
            alice.support(new Trouble(i));
        }
    }
}
  • chain 을 형성해서 여러 객체가 하나의 일을 처리하도록 구현한다.
  • 초기값은 NoSupport 인 alice 로 시작한다.

 

Interpreter Pattern

 

Class diagram

 

예제 코드

public abstract class Node {
    public abstract void parse(Context context) throws ParseException;
}
public class ProgramNode extends Node {
    private Node commandListNode;
    public void parse(Context context) throws ParseException {
        context.skipToken("program");
        commandListNode = new CommandListNode();
        commandListNode.parse(context);
    }
    public String toString() {
        return "[program " + commandListNode + "]";
    }
}
public class CommandListNode extends Node {
    private Vector list = new Vector();
    public void parse(Context context) throws ParseException {
        while (true) {
            if (context.currentToken() == null) {
                throw new ParseException("Missing 'end'");
            } else if (context.currentToken().equals("end")) {
                context.skipToken("end");
                break;
            } else {
                Node commandNode = new CommandNode();
                commandNode.parse(context);
                list.add(commandNode);
            }
        }
    }
    public String toString() {
        return "" + list;
    }
}
public class CommandNode extends Node {
    private Node node;
    public void parse(Context context) throws ParseException {
        if (context.currentToken().equals("repeat")) {
            node = new RepeatCommandNode();
            node.parse(context);
        } else {
            node = new PrimitiveCommandNode();
            node.parse(context);
        }
    }
    public String toString() {
        return node.toString();
    }
}
public class RepeatCommandNode extends Node {
    private int number;
    private Node commandListNode;
    public void parse(Context context) throws ParseException {
        context.skipToken("repeat");
        number = context.currentNumber();
        context.nextToken();
        commandListNode = new CommandListNode();
        commandListNode.parse(context);
    }
    public String toString() {
        return "[repeat " + number + " " + commandListNode + "]";
    }
}
public class PrimitiveCommandNode extends Node {
    private String name;
    public void parse(Context context) throws ParseException {
        name = context.currentToken();
        context.skipToken(name);
        if (!name.equals("go") && !name.equals("right") && !name.equals("left")) {
            throw new ParseException(name + " is undefined");
        }
    }
    public String toString() {
        return name;
    }
}
public class Context {
    private StringTokenizer tokenizer;
    private String currentToken;
    public Context(String text) {
        tokenizer = new StringTokenizer(text);
        nextToken();
    }
    public String nextToken() {
        if (tokenizer.hasMoreTokens()) {
            currentToken = tokenizer.nextToken();
        } else {
            currentToken = null;
        }
        return currentToken;
    }
    public String currentToken() {
        return currentToken;
    }
    public void skipToken(String token) throws ParseException {
        if (!token.equals(currentToken)) {
            throw new ParseException("Warning: " + token + " is expected, but " + currentToken + " is found.");
        }
        nextToken();
    }
    public int currentNumber() throws ParseException {
        int number = 0;
        try {
            number = Integer.parseInt(currentToken);
        } catch (NumberFormatException e) {
            throw new ParseException("Warning: " + e);
        }
        return number;
    }
}
public class ParseException extends Exception {
    public ParseException(String msg) {
        super(msg);
    }
}
public class Main {
    public static void main(String[] args) {
        try {
            BufferedReader reader = new BufferedReader(new FileReader("program.txt"));
            String text;
            while ((text = reader.readLine()) != null) {
                System.out.println("text = \"" + text + "\"");
                Node node = new ProgramNode();
                node.parse(new Context(text));
                System.out.println("node = " + node);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

Iterator Pattern

  • 다양한 순회방법이 제공하고 싶을 때 사용하면 좋은 패턴이다.
  • 객체 요소들의 내부 표현방식을 공개하지 않는다.
  • 내부에서 객체의 순차적인 제공을 하지 않는다.
  • 외부에서, 여러 리스트 객체에 대한 동일한 방식으로 순회하는 방법을 제공하기 위해 순회하는 객체를 따로만듬
  • 순회 구현 방식이 다르더라도 동일한 방식(메서드)로 순회 할 수 있게 제공한다.

 

class diagram

  • Iterator
    • 요소에 접근하고 순회하는데 필요한 메서드 제공한다.
  • CocreteIterator
    • Iterator에 정의된 인터페이스를 구현하는 클래스다.
    • ConcreteIterator는 리스트를 순회하면서 각 리스트의 요소를 반환하는 메서드도 제공한다.
  • Aggregate
    • Iterator 객체를 생성하는 인터페이스 정의한다.
    • 동일한 Aggregate를 구현한 클래스들은 동일한 방식으로 순회할 수 있다.
  • ConcreteAggregate
    • 해당하는 ConcreteIteratir의 인스턴스를 반환하도록 Iterator 생성 인터페이스를 구현한다.

 

예제 코드

public interface Iterator {
    public abstract boolean hasNext();
    public abstract Object next();

}
public interface Aggregate {
    public abstract Iterator iterator(int type);
    public int getLength();

}
public class Book {
    private String name = "";
    public Book(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}
public class BookShelf implements Aggregate {
    private Book[] books;
    private int last = 0;
    Factory f = IteratorFactory.getInstance();

    public BookShelf(int maxsize) {
        this.books = new Book[maxsize];
    }
    public Book getBookAt(int index) {
        return books[index];
    }
    public void appendBook(Book book) {
        this.books[last] = book;
        last++;
    }
    public int getLength() {
        return last;
    }
    public Iterator iterator(int type) {
        Iterator i = f.create(this, type);
        return i;
    }

}
  • IteratorFactory 이 싱글톤으로 구현되어 있기 때문에 static method 로 인스턴스를 가져온다.
  • iterator 메소드에서 IteratorFactory 의 상위 클래스인 Factory의 create 를 호출한다.
public class Constant {
    public static final int FORWARD = 0;
    public static final int REVERSE = 1;

}
public abstract class Factory {
    public final Iterator create(Aggregate list, int type) {
        Iterator p = createProduct(list, type);
        return p;
    }
    protected abstract Iterator createProduct(Aggregate list, int type);
}
  • Template method 패턴으로 Factory 를 구현한다.
public class IteratorFactory extends Factory {

    private static IteratorFactory ifactory = new IteratorFactory();
    private IteratorFactory(){}

    public static IteratorFactory getInstance(){

        if(ifactory == null)
            ifactory = new IteratorFactory();
        return ifactory;
    }

    @Override
    protected Iterator createProduct(Aggregate bookShelf, int type) {
        if(type == Constant.FORWARD)
            return new BookShelfIterator(bookShelf);
        else if(type == Constant.REVERSE)
            return new ReverseIterator(bookShelf);
        else 
            return null;
    }

}
  • 다음과 같이, IteratorFactory 를 싱글톤으로 구성했다.
public class BookShelfIterator implements Iterator{
    private BookShelf bookShelf;
    private int index;

    BookShelfIterator(Aggregate bookShelf) {
        this.bookShelf = (BookShelf)bookShelf;
        this.index = 0;
    }
    public boolean hasNext() {
        if (index < bookShelf.getLength()) {
            return true;
        } else {
            return false;
        }
    }
    public Object next() {
        Book book = bookShelf.getBookAt(index);
        index++;
        return book;
    }

}
  • 생성자로 Aggregate 를 받는데, 현재는 bookShelf 를 받는다.
public class ReverseIterator  implements Iterator {
    private BookShelf bookShelf;
    private int index;

    ReverseIterator(Aggregate bookShelf) {
        this.bookShelf = (BookShelf)bookShelf;
        this.index = bookShelf.getLength() -1;
    }
    public boolean hasNext() {
     if (index >= 0 ) {
            return true;
        } else {
            return false;
        }

    }
    @Override
    public Object next() {
         Book book = bookShelf.getBookAt(index);
         index--;
         return book;
    }

}
public class Main {
    public static void main(String[] args) {
        BookShelf bookShelf = new BookShelf(4);
        bookShelf.appendBook(new Book("Around the World in 80 Days"));
        bookShelf.appendBook(new Book("Bible"));
        bookShelf.appendBook(new Book("Cinderella"));
        bookShelf.appendBook(new Book("Daddy-Long-Legs"));

        Iterator it = bookShelf.iterator(Constant.FORWARD);
        while (it.hasNext()) {
            Book book = (Book)it.next();
            System.out.println("" + book.getName());
        }

        System.out.println("============");

        it = bookShelf.iterator(Constant.REVERSE);
        while (it.hasNext()) {
            Book book = (Book)it.next();
            System.out.println("" + book.getName());
        }
    }
}
  • 위의 결과는 FORWARD 일때와 REVERSE 일때가 반대로 동작하는 것을 확인할 수 있다.



Visitor Pattern

  • SOILD 원칙에서는 클래스가 해야될 일은 해당 클래스 내에서 해야된다고 나온다.
  • 하지만 Visitor Pattern 은 클래스 밖에서 일을 처리한다.
  • element에서 처리할 일을 visitor 가 처리한다.
  • 여러 element에 대해 유사한 기능의 메서드를 한곳에 모아서 관리하게 되고, 여러 클래스에 걸친 메서드를 추가하기 용이하다.
  • 각 클래스에 대한 기능이 자주 변경되거나 알고리즘의 변화가 많을때 사용하는것이 효율적이다.
  • 각 객체의 오퍼레이션이 외부에 노출되는 것이므로 객체지향 프로그래밍의 캡슐화 전략에 위배된다.
  • 주로 Composite 패턴에서 자주 사용될 수 있다.

 

class diagram

  • Visitor
    • 각 객체에서 수행해야 할 기능을 선언한다.
    • 메서드의 이름은 요청을 받을 객체를 명시한다.
  • ConcreteVisitor
    • Visitor 클래스에 선언된 메서드를 구현한다.
    • 각 메서드는 객체에 해당하는 알고리즘을 구현한다.
  • Element
    • Visitor가 수행될 수 있는 Accept() 메서드를 선언한다.
  • ConcreteElement
    • 매개변수로 Visitor를 받아주는 Accept()메서드를 구현한다.
  • ConcreteAggregate
    • 해당하는 ConcreteIteratir의 인스턴스를 반환하도록 Iterator 생성 인터페이스를 구현한다.
  • ConcreteElement 들이 하는 일을 ConcreteVisitor 가 방문하면서 대신 처리한다.
  • Visitor 가 추가될 때 사용하면 좋은 패턴이나, Element 가 추가되는 경우에는 사용하면 좋지 않은 패턴이다.
    • 새로운 요소 클래스를 추가하게 되면 그에 해당되는 기능을 모든 Visitor에서 구현해야 하기 때문이다.

 

예제 코드

public abstract class Visitor {
    public abstract void visit(File file);
    public abstract void visit(Directory directory);
}
public class ListVisitor extends Visitor {
    private String currentdir = "";                         // 현재 주목하고 있는 디렉토리명

    public void visit(File file) {                  // 파일을 방문했을 때 호출된다.
        System.out.println(currentdir + "/" + file);
    }

    public void visit(Directory directory) {   // 디렉토리를 방문했을 때 호출된다.
        System.out.println(currentdir + "/" + directory);
        String savedir = currentdir;
        currentdir = currentdir + "/" + directory.getName();
        Iterator<Entry> it = directory.iterator();
        while (it.hasNext()) {
            Entry entry = (Entry)it.next();
           // if(entry.getName() == "tmp")
           //     continue;

            entry.accept(this);

        }
        currentdir = savedir;
    }
}
  • class diagram 에서 ConcreteVisitor에 해당된다.
  • 위와같은 Visitor 들이 늘어나는 경우에 사용하면 좋은 패턴이다.
public interface Acceptor {
    public abstract void accept(Visitor v);
}
public abstract class Entry implements Acceptor {
    public abstract String getName();                                   // 이름을 얻는다.
    public abstract int getSize();                                      // 사이즈를 얻는다.
    public Entry add(Entry entry) throws FileTreatmentException {       // 엔트리를 추가
        throw new FileTreatmentException();
    }
    public Iterator iterator() throws FileTreatmentException {    // Iterator의 생성
        throw new FileTreatmentException();
    }
    public String toString() {                                          // 문자열 표현
        return getName() + " (" + getSize() + ")";
    }
}
  • Acceptor 의 accept 메소드를 구현하고 있지 않기 때문에 abstract 클래스로 구현한다.
public class File extends Entry {
    private String name;
    private int size;
    public File(String name, int size) {
        this.name = name;
        this.size = size;
    }
    public String getName() {
        return name;
    }
    public int getSize() {
        return size;
    }
    public void accept(Visitor v) {
        v.visit(this);
    }
}
  • class diagram 에서 ConcreteElement 에 해당된다.
  • accept 메서드에서 visit 메서드를 호출하면서 인자로 this 를 넘긴다.
  • ConcreteElement 가 추가되는 경우 사용하면 좋지 않은 패턴이다.
public class Directory extends Entry {
    private String name;                    // 디렉토리의 이름
    private ArrayList<Entry> dir = new ArrayList<Entry>();      // 디렉토리 엔트리의 집합
    public Directory(String name) {         // 생성자
        this.name = name;
    }
    public String getName() {               // 이름을 얻는다.
        return name;
    }
    public int getSize() {                  // 사이즈를 얻는다.
        int size = 0;
        Iterator<Entry> it = dir.iterator();
        while (it.hasNext()) {
            Entry entry = (Entry)it.next();
            size += entry.getSize();
        }
        return size;
    }
    public Entry add(Entry entry) {         // 엔트리의 추가
        dir.add(entry);
        return this;
    }
    public Iterator<Entry> iterator() {      // Iterator의 생성
        return dir.iterator();
    }
    public void accept(Visitor v) {         // 방문자를 받아들임
        v.visit(this);
    }
}
public class Main {
    public static void main(String[] args) {
        try {
            System.out.println("Making root entries...");
            Directory rootdir = new Directory("root");
            Directory bindir = new Directory("bin");
            Directory tmpdir = new Directory("tmp");
            Directory usrdir = new Directory("usr");
            rootdir.add(bindir);
            rootdir.add(tmpdir);
            rootdir.add(usrdir);
            bindir.add(new File("vi", 10000));
            bindir.add(new File("latex", 20000));
            rootdir.accept(new ListVisitor());              

            System.out.println("");
            System.out.println("Making user entries...");
            Directory Kim = new Directory("Kim");
            Directory Lee = new Directory("Lee");
            Directory Kang = new Directory("Kang");
            usrdir.add(Kim);
            usrdir.add(Lee);
            usrdir.add(Kang);
            Kim.add(new File("diary.html", 100));
            Kim.add(new File("Composite.java", 200));
            Lee.add(new File("memo.tex", 300));
            Kang.add(new File("game.doc", 400));
            Kang.add(new File("junk.mail", 500));
            rootdir.accept(new ListVisitor());             
        } catch (FileTreatmentException e) {
            e.printStackTrace();
        }
    }
}
  • 다음과 같이, new ListVisitor() 를 인자로 accept 를 호출하게 되면 ListVisitor 가 요소를 방문하면서 일을 처리한다.
  • ListVisitor 의 visit 을 호출할때 요소(this) 를 넘기기 때문이다.
public class FileTreatmentException extends RuntimeException {
    public FileTreatmentException() {
    }
    public FileTreatmentException(String msg) {
        super(msg);
    }
}

 

Command Pattern

  • 동일한 코맨드로 만들면 모을수 있어서 히스토리 관리를 할 수 있다. 객체 Array에 담을수 있기때문, 롤백도 할 수 있다.
  • 코맨드를 한꺼번에 쭉 실행할 수도 있다.

 

Class diagram

  • Command: 각 명령이 수행할 메서드 선언
  • CocreteCommand: 실제 명령이 호출되도록 execute 구현
  • Client: ConcreteCommand 객체를 생성하고 처리 객체로 정의
  • Invoker: Command 처리를 수행할 것을 요청
  • Receiver: Command를 처리함

 

예제 코드

public interface Command {
    public abstract void excute();
}
public class DrawCommand implements Command {
    protected Drawable drawable;

    private Point position;

    public DrawCommand(Drawable drawable, Point position) {
        this.drawable = drawable;
        this.position = position;
    }

    public void execute() {
        drawable.draw(position.x, position.y);
    }
}
public class MacroCommand implements Command {
    private Stack commands = new Stack();

    public void execute() {
        Iterator it = commands.iterator();
        while (it.hasNext()) {
            ((Command) it.next()).execute();
        }
    }

    public void append(Command cmd) {
        if (cmd != this) {
            commands.push(cmd); 
        }
    }

    public void undo() {
        if (!commands.empty()) {
            commands.pop();
        }
    }

    public void clear() {
        commands.clear();
    }
}
  • 위와 같이 command 를 쌓아 두었다가 처리 할 수 있다.
public interface Drawable {
    public abstract void draw(int x, int y);
}
public class DrawCanvas extends Canvas implements Drawable {
    private Color color = Color.red;
    private int radius = 6;
    private MacroCommand history;

    public DrawCanvas(int width, int height, MacroCommand history) {
        setSize(width, height);
        setBackground(Color.white);
        this.history = history;
    }

    public void paint(Graphics g) {
        history.execute();
    }

    public void draw(int x, int y) {
        Graphics g = getGraphics();
        g.setColor(color);
        g.fillOval(x - radius, y - radius, radius * 2, radius * 2);
    }
}
public class Main extends JFrame implements ActionListener,
        MouseMotionListener, WindowListener {

    private MacroCommand history = new MacroCommand();
    private DrawCanvas canvas = new DrawCanvas(400, 400, history);
    private JButton clearButton = new JButton("clear");

    public Main(String title) {
        super(title);
        // 리스너 등록하기
        this.addWindowListener(this);
        canvas.addMouseMotionListener(this);
        clearButton.addActionListener(this);

        Box buttonBox = new Box(BoxLayout.X_AXIS);
        buttonBox.add(clearButton);
        Box mainBox = new Box(BoxLayout.Y_AXIS);
        mainBox.add(buttonBox);
        mainBox.add(canvas);
        getContentPane().add(mainBox);

        pack();
        setVisible(true);
    }


    public void actionPerformed(ActionEvent e) {
        if (e.getSource() == clearButton) {
            history.clear(); 
            canvas.repaint();
        }
    }

    public void mouseMoved(MouseEvent e) {
    }

    public void mouseDragged(MouseEvent e) {
        Command cmd = new DrawCommand(canvas, e.getPoint());  
        history.append(cmd); 
        cmd.execute(); 
    }

    public void windowClosing(WindowEvent e) {
        System.exit(0);
    }
    public void windowActivated(WindowEvent e) {
    }

    public void windowClosed(WindowEvent e) {
    }

    public void windowDeactivated(WindowEvent e) {
    }

    public void windowDeiconified(WindowEvent e) {
    }

    public void windowIconified(WindowEvent e) {
    }

    public void windowOpened(WindowEvent e) {
    }

    public static void main(String[] args) {
        new Main("Command Pattern Sample");
    }
}

 

State Pattern

  • 상태에 따른 기능을 분리하여 구현하는 패턴이다.
  • 객체의 기능은 상태에 따라 달라질 수 있는데, 이러한 상태가 여러가지이고, 클래스 전반의 모든 기능이 상태에 의존적이라 하면, 상태를 클래스로 표현하는 것이 적절할때 사용하는 패턴이다.
  • 클래스로 분리하지 않게 되면 상태가 여러가지인 경우 많은 if-else 문이 사용되고 추후 상태가 추가되거나 삭제될 때 수정해야 하는 사항이 너무 많아진다.

 

Class diagram

  • Context: ConcreteState의 인스턴스를 관리하고 서로 상태가 바뀌는 순간을 구현할 수 있다.
  • State: Context 가 사용할 메서드를 선언한다.
  • ConcreateState: 각 상태 클래스가 수행할 State에 선언된 메서드를 구현한다.

 

예제 코드

public abstract class PlayerLevel {

    public abstract void run();
    public abstract void jump();
    public abstract void turn();
    public abstract void showLevelMessage();

    final public void go(int count)
    {
        run();
        for(int i=0; i<count; i++){
            jump();
        }
        turn();
    }
}
  • class diagram 에서 State에 해당한다.
public class BeginnerLevel extends PlayerLevel{

    @Override
    public void run() {
        System.out.println("천천히 달립니다.");

    }

    @Override
    public void jump() {
        System.out.println("Jump 할 줄 모르지롱.");
    }

    @Override
    public void turn() {
        System.out.println("Turn 할 줄 모르지롱.");        
    }

    @Override
    public void showLevelMessage() {
        System.out.println("***** 초보자 레벨 입니다. *****");
    }

}
  • class diagram 에서 ConcreteState에 해당한다.
public class AdvancedLevel extends PlayerLevel{
    @Override
    public void run() {
        System.out.println("빨리 달립니다.");

    }

    @Override
    public void jump() {
        System.out.println("높이 jump 합니다.");
    }

    @Override
    public void turn() {
        System.out.println("Turn 할 줄 모르지롱.");        
    }

    @Override
    public void showLevelMessage() {
        System.out.println("***** 중급자 레벨 입니다. *****");
    }
}
  • class diagram 에서 ConcreteState에 해당한다.
public class SuperLevel extends PlayerLevel{
    @Override
    public void run() {
        System.out.println("엄청 빨리 달립니다.");

    }

    @Override
    public void jump() {
        System.out.println("아주 높이 jump 합니다.");
    }

    @Override
    public void turn() {
        System.out.println("한 바퀴 돕니다.");        
    }

    @Override
    public void showLevelMessage() {
        System.out.println("***** 고급자 레벨 입니다. *****");
    }

}
  • class diagram 에서 ConcreteState에 해당한다.
public class Player {

    private PlayerLevel level;

    public Player()
    {
        level= new BeginnerLevel();
        level.showLevelMessage();
    }

    public PlayerLevel getLevel() {
        return level;
    }

    public void upgradeLevel(PlayerLevel level) {
        this.level = level;
        level.showLevelMessage();
    }

    public void play(int count){
        level.go(count);
    }
}
  • class diagram 에서 Context 에 해당한다.
public class MainBoard {

    public static void main(String[] args) {

        Player player = new Player();
        player.play(1);
        AdvancedLevel aLevel = new AdvancedLevel();
        player.upgradeLevel(aLevel);
        player.play(2);
        SuperLevel sLevel = new SuperLevel();
        player.upgradeLevel(sLevel);
        player.play(3);

    }
}
  • 위의 코드와 같이 다른 레벨을 인자로 넣어주면, 그 레벨에 해당되는 메서드가 실행된다.
  • State 가 추가되면, 새로운 클래스를 구현해 주면된다.

 

Memento Pattern

  • 히스토리를 보관할때 사용되는 패턴이다.
  • 만약 어느 시점으로 롤백을 해야되는 상황이 생긴다면 고려해볼만 패턴이다.

 

Class diagram

  • Memento
    • Originator 객체의 내부 상태를 필요한 만큼 저장한다.
    • Originator만이 Memento에 접근할 수 있다.
    • 저장하고 싶은 변수 state 를 가지고 있다.
  • Originator
    • Memento를 생성하여 현재 객체의 상태를 저장하고 내부 상태를 복구한다.
    • 저장하고 싶은 변수는 state이다.
  • CareTaker (undo mechanism) : Memento의 보관을 책임지기는 하지만, memento의 내부를 확인할 수 없다.

 

예제 코드

public class Main {
    public static void main(String[] args) {
        Gamer gamer = new Gamer(100);               // 처음의 돈은 100
        Memento memento = gamer.createMemento();    // 처음의 상태를 보존해 둔다.
        ArrayList<Memento> history = new ArrayList<Memento>();
        for (int i = 0; i < 100; i++) {
            System.out.println("==== " + i);        // 횟수 표시
            System.out.println("현 상태:" + gamer);    // 현재의 주인공의 상태 표시

            gamer.bet();    // 게임을 진행 시킨다.

            System.out.println("돈은" + gamer.getMoney() + "원이 되었습니다.");

            // Memento의 취급 결정
            if (gamer.getMoney() > memento.getMoney()) {
                System.out.println("    (많이 증가했으니 현재의 상태를 보존해두자)");
                memento = gamer.createMemento();
                history.add(memento);
            } else if (gamer.getMoney() < memento.getMoney() / 2) {
                System.out.println("    (많이 줄었으니 이전의 상태로 복귀하자)");
                gamer.restoreMemento(memento);

            }

            // 시간을 기다림
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            System.out.println("");
        }
    }
}
  • Gamer 의 돈이 memento 가 가진 돈보다 많다면 상태를 저장한다.
  • 또한, 돈이 줄었다면 restoreMemento 를 통해서 저장된 상태를 가져온다.
  • Memento 객체를 관리하고 있는 history 가 Caretaker 가 된다.
public class Gamer {
    private int money;
    private ArrayList<String> fruits = new ArrayList<>();
    private Random random = new Random();
    private static String[] fruitsname = {
        "사과", "포도", "바나나", "귤",
    };
    public Gamer(int money) {
        this.money = money;
    }
    public int getMoney() {
        return money;
    }
    public void bet() {
        int dice = random.nextInt(6) + 1;
        if(dice == 1) {
            money += 100;
        } else if (dice == 2) {
            money /= 2;
        } else if (dice == 6) {
            String f = getFruit();
            fruits.add(f);
        } else {
            System.out.println("아무일도 일어나지 않았습니다");
        }
    }
    public Memento createMemento() {
        Memento memento = new Memento(money);
        Iterator<String> it = fruits.iterator();
        while (it.hasNext()) {
            String f = it.next();
            if (f.startsWith("good~ ")) {         // 과일은 맛있는 것만 보존
                m.addFruit(f);
            }
        }
        return m;
    }

    public void restoreMemento(Memento memento) {       // undo를 실행한다.
        this.money = memento.money;
        this.fruits = memento.fruits;
    }
    public String toString() {                      // 문자열 표현
        return "[money = " + money + ", fruits = " + fruits + "]";
    }
    private String getFruit() {                     // 과일을 1개 얻는다.
        String prefix = "";
        if (random.nextBoolean()) {
            prefix = "good~ ";
        }
        return prefix + fruitsname[random.nextInt(fruitsname.length)];
    }
}
  • createMemento 메서드에서 좋은 과일만 복원을 시키도록 저장한다.
public class Memento {
    int money;                              // 돈
    ArrayList<String> fruits;                          // 과일
    public int getMoney() {                 // 돈을 얻는다.(narrow interface)
        return money;
    }
    Memento(int money) {                    // 생성자(wide interface)
        this.money = money;
        this.fruits = new ArrayList<String>();
    }
    void addFruit(String fruit) {           // 과일을 추가한다.(wide interface)
        fruits.add(fruit);
    }
}
  • Memento 에는 Gamer(Originator) 의 정보 중에서 기억하고 싶은 money, fruits 변수만 가지고 있다.




References

  • 박은종 님의 디자인 패턴