객체나 클래스 사이의 알고리즘이나 책임 분배에 관련된 패턴이다.
한 객체가 수행할 수 없는 작업을 여러 개의 객체로 어떻게 분배하며 객체 사이의 결합도 최소화에 중점을 둔다.
행동 패턴은 알고리즘과 객체 간 책임 할당과 관련이 있습니다. 행동 패턴은 객체와 클래스의 패턴뿐만 아니고 객체, 클래스 간에 의사소통 패턴도 설명하는데요, 이러한 패턴은 런타임에서 처리하기 어려운 복잡한 control flow들이라는 특징을 갖습니다. 이렇게 행동 패턴을 사용해서 개발자가 control flow보다는 객체가 연결되는 방식에 집중할 수 있도록 해줍니다.
출처: https://icksw.tistory.com/249 [PinguiOS:티스토리]
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);
}
}
}
public class AuthRequestHandler extends RequestHandler {
public AuthRequestHandler(RequestHandler nextHandler) {
super(nextHandler);
}
@Override
public void handle(Request request) {
System.out.println("인증이 되었는가?");
super.handle(request);
}
}
public class LoggingRequestHandler extends RequestHandler {
public LoggingRequestHandler(RequestHandler nextHandler) {
super(nextHandler);
}
@Override
public void handle(Request request) {
System.out.println("로깅");
super.handle(request);
}
}
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);
}
}
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();
}
}
클라이언트로 부터 요청을 처리하기 위해 체인을 사용하며 느슨한 결합을 유지한다.
요청을 처리할 수 있는 객체가 여러 개이고 처리객체가 특정적이지 않을 경우 권장되는 패턴.
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;
}
}
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;
}
}
public interface Command {
void execute();
}
public class GameEndCommand implements Command {
private Game game;
public GameEndCommand(Game game) {
this.game = game;
}
@Override
public void execute() {
game.end();
}
}
Game 타입의 객체 end 기능을 캡슐화한 객체이다.
public class GameStartCommand implements Command {
private Game game;
public GameStartCommand(Game game) {
this.game = game;
}
@Override
public void execute() {
game.start();
}
}
Game 타입의 객체 start 기능을 캡슐화한 객체이다.
public class LightOffCommand implements Command {
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.off();
}
}
Light 타입의 객체 off 기능을 캡슐화한 객체이다.
public class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.on();
}
}
Light 타입의 객체 on 기능을 캡슐화한 객체이다.
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 클래스는 객체들의 협력을 위해 키고/끈다는 기능을 처리해줄 수 있는 추상화된 객체를 사용한다.
문법 규칙을 클래스화 한 구조로, 일련의 규칙으로 정의된 문법적 언어를 해석하는 패턴입니다.(SQL, SHELL...) 인터프리터 패턴은 SQL과 같은 계층적 언어를 해석하기 위해 계층 구조를 표현할 수 있습니다.
자주 등장하는 문제를 간단한 언어로 정의하고 재사용하는 패턴.
반복되는 문제 패턴을 언어 또는 문법으로 정의하고 확장할 수 있다.
public interface PostfixExpression {
int interpret(Map<Character, Integer> context);
}
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);
}
}
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);
}
}
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);
}
}
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 한다.
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 을 전달하여 결과를 반환받는다.
집합 객체 내부 구조를 노출시키지 않고 순회 하는 방법을 제공하는 패턴. (List, Set 등의 자료형을 노출하지 않고 Iterator 만 노출시킨다.)
집합 객체를 순회하는 클라이언트 코드를 변경하지 않고 다양한 순회 방법을 제공할 수 있다.
다이어그램만 보면 어려울 수 있다. 코드로 보자(간단하다.)
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);
}
}
인터페이스 없이 구현하였다.
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(일) 는 다대일 관계와 같다.
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();
}
}
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 패턴을 적용하면 어떠한 자료구조를 사용하는지 노출시키지 않고 순회할 수 있다.
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);
}
}
public class Restaurant {
private CleaningService cleaningService = new CleaningService();
public void dinner(Guest guest) {
System.out.println("dinner " + guest);
}
public void clean() {
cleaningService.clean(this);
}
}
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);
}
}
public class Gym {
private CleaningService cleaningService;
public void clean() {
cleaningService.clean(this);
}
}
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개의 객체들 사이에 중재자를 추가하여 중재자가 모든 객체들의 통신을 담당하도록 변경하면 중재자 패턴이라 볼 수 있다.
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);
}
}
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;
}
}
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);
}
}
public class Restaurant {
public void dinner(Integer id, LocalDateTime dateTime) {
...
}
}
DIP 가 지켜지지 않고 의존성 주입을 사용하지 않았지만 M:N 관계에서 M:1 로 복잡도를 낮추는걸 표현하였다.
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 에 해당하는 필드로 변환할 수 있다.
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 키워드를 붙였다.
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 를 저장할 수 있는 외부 저장소와 같다.
장점
단점
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) 를 관리하며 등록, 삭제, 메시지 전송의 기능을한다.
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 역할을 한다.
public interface Observer {
void handleMessage(String message);
}
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 의 상태 변화를 관리하게 한다.
장점
단점
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 하는 행동이 바뀐다.
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() 메서드를 사용하여 객체의 해동을 바꿀 수 있다.
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));
}
}
}
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("프라이빛 코스를 수강할 수 없습니다.");
}
}
}
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);
}
}
장점
단점
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();
}
}
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 에 따라 알고리즘을 교체하는 방식이다.
public class FastSpeed implements SpeedStrategy {
@Override
public void blueLight() {
System.out.println("무궁화꽃이");
}
@Override
public void redLight() {
System.out.println("피었습니다.");
}
}
public class NormalSpeed implements SpeedStrategy {
@Override
public void blueLight() {
System.out.println("무 궁 화 꽃 이");
}
@Override
public void redLight() {
System.out.println("피 었 습 니 다.");
}
}
장점
단점
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);
}
구체적인 로직, 알고리즘을 서브 클래스에 위임하며 전체적인 구조는 슈퍼 클래스를 사용한다.
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;
}
}
public class PlusFileProcessor extends FileProcessor {
public PlusFileProcessor(String path) {
super(path);
}
@Override
protected int getResult(int result, int number) {
return result += number;
}
}
장점
단점
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 타입의 구체적인 타입에 맞게 행동한다.
public interface Shape {
void accept(Device device);
}
객체 구조에 해당한다.
public class Rectangle implements Shape {
@Override
public void accept(Device device) {
device.print(this);
}
}
public class Circle implements Shape {
@Override
public void accept(Device device) {
device.print(this);
}
}
public class Triangle implements Shape {
@Override
public void accept(Device device) {
device.print(this);
}
}
public interface Shape {
void accept(Device device);
}
알고리즘에 해당하는 타입이다.
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("동그라미 폰");
}
}
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("동그라미 시계");
}
}
장점
단점