행위 디자인패턴(1) - 책임연쇄 패턴, 커맨드 패턴, 인터프리터 패턴
책임연쇄 패턴
단일책임원칙의 책임이라고 생각하면 된다.
public static void main(String[] args) {
Request request = new Request("무궁화 꽃이 피었습니다.");
RequestHandler requestHandler = new RequestHandler();
requestHandler.handler(request);
}
public class Request {
private String body;
public Request(String body) {
this.body = body;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
}
public class RequestHandler {
public void handler(Request request) {
System.out.println(request.getBody());
}
}
다음과 같은 코드가있을떄 인증/인가를 하려고한다.
public class RequestHandler {
public void handler(Request request) {
System.out.println("인증이 된건가");
System.out.println("이 핸들러를 사용할수있는 유저인가");
// 단일책임의 원칙을 위반한다 .
System.out.println(request.getBody());
}
}
첫번째방식으로 handler 의 코드를 바꾸는 방법이있다.
그러나 이방법은 SRP를 위반한다
public class AuthRequestHandler extends RequestHandler {
public void handler(Request request) {
System.out.println("인증이 되었나?");
System.out.println("이 핸들러를 사용할 수 있는 유저인가?");
super.handler(request);
}
}
두번째방식은 새로운 핸들러를 만드는방법이다
public static void main(String[] args) {
Request request = new Request("무궁화 꽃이 피었습니다.");
RequestHandler requestHandler = new AuthRequestHandler(); //변화
requestHandler.handler(request);
}
변화되는 클라이언트의 코드
SRP를 지킬순있게되었지만 ,여전히 남아있는문제는
클라이언트가 AuthRequestHandler 를 선택해야한다.
그리고 만약 기능이 추가된다면
public class LoggingRequestHandler extends RequestHandler {
@Override
public void handler(Request request) {
System.out.println("로깅");
super.handler(request);
}
}
다음과같이 로깅기능이 추가된다면
public static void main(String[] args) {
Request request = new Request("무궁화 꽃이 피었습니다.");
RequestHandler requestHandler = new LoggingRequestHandler(); //변경
requestHandler.handler(request);
}
클라이언트는 또다시 로깅을 선택해야만한다.
클라이언트가 사용해야되는 핸들러를 직접 알아야된다는점이 문제다.
만약 로깅도 하고 인증도 하고싶으면어떡하지?
여기에 책임연쇄패턴을 적용한다.
클라이언트는 구체적인 핸들러타입을 모르게끔 변경하는것이 목표다.
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 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 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{
@Override
public void handle(Request request) {
System.out.println("로깅");
super.handle(request);
}
public LoggingRequestHandler(RequestHandler nextHandler) {
super(nextHandler);
}
}
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 static void main(String[] args) {
Filter filter = new Filter() {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 전처리
chain.doFilter(request, response);
// 후처리
}
};
}
서블릿 필터에서 사용될수있다.
요청이 서블릿에 가기전에 여러개의 필터를 거치면서
최종적으로 서블릿까지 가게된다.
@WebFilter(urlPatterns = "/hello")
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("게임에 참하신 여러분 모두 진심으로 환영합니다.");
chain.doFilter(request, response);
System.out.println("꽝!");
}
}
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
@ServletComponentScan
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
커맨드 패턴
요청을 캡슐화 하여 호출자(invoker)와 수신자를 분리하는 패턴
- 요청을 처리하는 방법이 바뀌더라도 호출자의 코드는 변경되지않는다.
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 class Button {
private Light light;
public Button(Light light) {
this.light = light;
}
public void press() {
light.off();
//버튼을 켜야한다면 이부분을 변경해야한다.
}
public static void main(String[] args) {
Button button = new Button(new Light());
button.press();
button.press();
button.press();
button.press();
}
}
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 MyApp {
private Game game;
public MyApp(Game game) {
this.game = game;
}
public void press() {
game.start();
}
public static void main(String[] args) {
Button button = new Button(new Light());
button.press();
button.press();
button.press();
button.press();
}
}
불을끄거나, 게임클래스를쓰거나 변경될때마다 코드를 고쳐줘야한다.
Light, Game 이 Receiver에해당
버튼, MyApp 의코드들 이 Invoker에 해당한다.
커맨드패턴을 적용해보자
public interface Command {
void execute();
}
public class GameStartCommand implements Command{
private Game game;
public GameStartCommand(Game game) {
this.game = game;
}
@Override
public void execute() {
game.start();
}
}
public class GameEndCommand implements Command{
private Game game;
public GameEndCommand(Game game) {
this.game = game;
}
@Override
public void execute() {
game.end();
}
}
public class LightOnCommand implements Command{
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.on();
}
}
public class LightOffCommand implements Command{
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.off();
}
}
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 LightOnCommand(new Light())); //불을키고싶다.
//게임을 시작하고싶다면
//new GameStartCommand(new Game())));
button.press();
button.press();
}
}
invoker 쪽의 코드는 바뀌지않지만
command 쪽의 코드만 변경하면된다.
버튼쪽코드가 변경되다가 커맨드쪽의 코드가 변경되는
조삼모사아닌가?
특정 조건에서 커맨드들은 바뀔수밖에 없다.
invoker 들의 코드가 변경되지않는것이 중요하고
변화되는부분이 최소화된다고 생각하면 될듯하다.
장점
- 기존코드를 변경하지않고 새로운 커맨드를 만들수있다.
- 수신자의 코드가 변경되어도 호출자의 코드는 변경되지않는다.
- 커맨드 객체를 로깅,DB저장, 네트워크로 전송하는등 다양한 방법으로 활용할수도있다.
단점
- 코드가 복잡하고 클래스가 많아진다.
인터프리터 패턴
어원은 통역하는사람 혹은 연주자의 뜻을 가질수도있다.
public class PostfixNotation {
private final String expression;
public PostfixNotation(String expression) {
this.expression = expression;
}
public static void main(String[] args) {
PostfixNotation postfixNotation = new PostfixNotation("123+-");
postfixNotation.calculate();
}
private void calculate() {
Stack<Integer> numbers = new Stack<>();
for (char c : this.expression.toCharArray()) {
switch (c) {
case '+':
numbers.push(numbers.pop() + numbers.pop());
break;
case '-':
int right = numbers.pop();
int left = numbers.pop();
numbers.push(left - right);
break;
default:
numbers.push(Integer.parseInt(c + ""));
}
}
System.out.println(numbers.pop());
}
}
후위표기식을 계산해주는 코드가있다
후위표기식이라는것을 자주쓰고 123+- 라는 수치만 바뀐다면 우린 다른숫자를 넣기만하고
계산을 해주게끔 하는 식이 필요할수도있다.
Context = 공통된 정보가 들어있는곳. 글로벌한 값들이 모여있는곳
Expression = 그러한 값들을 참조
TerminalExpression = x,y,z 값
public interface PostfixExpression {
int interpret(Map<Character, Integer> context);
}
public class PlusExpression implements PostfixExpression{
private PostfixExpression left, 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, 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 {
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);
}
}
}
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 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);
}
}
인터프리터 패턴의 장점
- 기존코드를 변경하지않고 Expression을 추가할수있다.
단점
- 복잡도가 증가한다
구현비용과 자주쓸만한 패턴인지 신경써서 적용을 고려해야한다.
Author And Source
이 문제에 관하여(행위 디자인패턴(1) - 책임연쇄 패턴, 커맨드 패턴, 인터프리터 패턴), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@dudwls0505/행위-디자인패턴1-책임연쇄-패턴저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)