Composit Pattern
1.컴포짓 패턴의 개념
1) 정의
- 객체들의 트리를 구성하여 부분-전체 계층 구조를 나타내는 패턴이다.
- Component : 공통 인터페이스를 정의하여 단일 객체와 복합 객체가 동일한 방식으로 처리될 수 있도록 한다.
- Leaf : 트리의 말단 요소로 더이상 하위 요소를 가지지 않는 객체를 나타낸다.
- Composite : 하위 요소를 가지는 복합 객체로, 하위 요소들을 관리하고 해당 요소들에게 작업을 전달한다.
2) UML으로 이해하기
- 패턴 적용 전 : 여러객체에서 중복된 코드가 발생하며, 하나의 객체가 여러개의 역할을 한다.
- 패턴 적용 후 : 기능별/역할별로 분리
2.MyApp에 적용하기
1) Menu생성하기
- 메뉴 인터페이스만들기
- 추상메서드 execute()가 필요하다.
- 메뉴들의 title을 불러올 규칙이 필요하다.
public interface Menu {
void execute();
String getTitle();
}
- 메뉴 추상클래스 만들기
- 메뉴그룹과 메뉴아이템의 제목이 들어 올때 저장 할 필드 값 title 필요하다.
- title을 불러올 getter가 필요하다.
- 트리노드에서 부모노드의 객체와 같은 확인하는 메서드 equals()와 hash()가 필요하다.
코드 접기/펼치기
import java.util.Objects;
public abstract class AbstractMenu implements Menu {
protected String title;
public AbstractMenu(String title) {
this.title = title;
}
@Override
public boolean equals(Object object) {
if (this == object)
return true;
if (!(object instanceof AbstractMenu that))
return false;
return Objects.equals(title, that.title);
}
@Override
public int hashCode() {
return Objects.hashCode(title);
}
@Override
public String getTitle() {
return title;
}
}
- 메뉴 그룹만들기
- 추상메서드 execute()를 구현하는 메서드가 필요하다.
- root menuGroup
- root는 parent가 필요없다.
- 메뉴아이템의 값들을 List로 담을 addMenu()메서드가 필요하다.
- 메뉴아이템의 값들을 List에서 제거할 removeMenu()메서드가 필요하다.
- 메뉴아이템의 값들을 List에서 얻어올 getMenu()메서드가 필요하다.
- 메뉴아이템의 갯수를 세는 countMenu()메서드가 필요하다.
- tree menuGroup
- tree는 parent가 필요하여 부모의 parent와 menuPath를 가져오는 메서드가 필요하다.
- addMenu()에서 tree가 상위 그룹이면 setParent를 실행하는 로직을 추가한다.
코드 접기/펼치기
import bitcamp.myapp.util.Prompt;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
public class MenuGroup extends AbstractMenu {
private String exitMenuTitle = "이전";
private MenuGroup parent;
private Stack<String> menuPath;
private List<Menu> children = new ArrayList<>();
public MenuGroup(String title) {
super(title);
this.menuPath = new Stack<>();
}
public void setParent(MenuGroup parent) {
this.parent = parent;
this.menuPath = parent.menuPath;
}
public void setExitMenuTitle(String title) {
exitMenuTitle = title;
}
@Override
public void execute() {
menuPath.push(title);
printMenus();
while (true) {
String command = Prompt.input("%s>", getMenuPathTitle(menuPath));
if (command.equals("menu")) {
printMenus();
continue;
} else if (command.equals("0")) { // 이전 메뉴 선택
menuPath.pop();
return;
}
try {
int menuNo = Integer.parseInt(command);
Menu menu = getMenu(menuNo - 1);
if (menu == null) {
System.out.println("유효한 메뉴 번호가 아닙니다.");
continue;
}
menu.execute();
} catch (NumberFormatException ex) {
System.out.println("숫자로 메뉴 번호를 입력하세요.");
}
}
}
private void printMenus() {
System.out.printf("[%s]\n", title);
int i = 1;
for (Menu child : children) {
System.out.printf("%d. %s\n", i++, child.getTitle());
}
System.out.printf("0. %s\n", exitMenuTitle);
}
private String getMenuPathTitle(Stack<String> menuPath) {
StringBuilder strBuilder = new StringBuilder();
for (String s : menuPath) {
if (!strBuilder.isEmpty()) {
strBuilder.append("/");
}
strBuilder.append(s);
}
return strBuilder.toString();
}
public void add(Menu child) {
if (child instanceof MenuGroup) {
((MenuGroup) child).setParent(this);
}
children.add(child);
}
public void remove(Menu child) {
children.remove(child);
}
public Menu getMenu(int index) {
if (index < 0 || index >= children.size()) {
return null;
}
return children.get(index);
}
public int countMenu() {
return children.size();
}
}
- 메뉴 아이템만들기
- 메뉴아이템은 가장 마지막 메뉴이므로 command를 실행해야한다.
- command인터페이스의 인스턴스를 받을 필드값이 필요하다.
- 종류에 따른 command를 구현체를 설정하는 setCommand()가 필요하다.
- 추상메서드 execute()를 구현하는 메서드가 필요하다.
코드 접기/펼치기
import bitcamp.myapp.command.Command;
public class MenuItem extends AbstractMenu {
Command command;
public MenuItem(String title) {
super(title);
}
public MenuItem(String title, Command command) {
super(title);
this.command = command;
}
@Override
public void execute() {
if (command != null) {
command.execute(title);
} else {
System.out.println(title);
}
}
}
2) Command수정하기
- 커맨드 인터페이스 수정하기
- 커맨드 인터페이스의 execute()의 매개변수를 String 타입으로 수정한다.
public interface Command {
void execute(String menuName);
}
- 커맨드 추상클래스 삭제하기
- 커맨드의 추상클래스의 역할은 menu에서 수행 하므로 더이상 추상클래스가 필요없다.
- 각 커맨드 수정하기
- 커맨드가 필드값으로 메뉴값을 가질 필요가 없다. -> 필드 및 생성자 메뉴값 삭제
- 커맨드가 메서드로 메뉴를 가져올 필요가 없다. -> getMenus() 삭제
코드 접기/펼치기
public class BoardCommand implements Command {
private List<Board> boardList;
public BoardCommand(List<Board> list) {
this.boardList = list;
}
@Override
public void execute(String menuName) {
System.out.printf("[%s]\n", menuName);
switch (menuName) {
case "등록":
this.addBoard();
break;
case "조회":
this.viewBoard();
break;
case "목록":
this.listBoard();
break;
case "변경":
this.updateBoard();
break;
case "삭제":
this.deleteBoard();
break;
}
}
/* 이하 생략 */
}
3) App수정하기
- 생성자 수정하기
- 기존 생성자 Map구조에서 ArrayList를 추가하는 생성자로 바꾼다.
코드 접기/펼치기
MenuGroup mainMenu = new MenuGroup("메인");
UserCommand userCommand;
BoardCommand boardCommand;
ProjectCommand projectCommand;
HelpCommand helpCommand;
HistoryCommand historyCommand;
public App() {
List<User> userList = new ArrayList<>();
List<Project> projectList = new LinkedList<>();
List<Board> boardList = new LinkedList<>();
userCommand = new UserCommand(userList);
boardCommand = new BoardCommand(boardList);
projectCommand = new ProjectCommand(projectList, userList);
helpCommand = new HelpCommand();
historyCommand = new HistoryCommand();
MenuGroup userMenu = new MenuGroup("회원");
userMenu.add(new MenuItem("등록", userCommand));
userMenu.add(new MenuItem("목록", userCommand));
userMenu.add(new MenuItem("조회", userCommand));
userMenu.add(new MenuItem("변경", userCommand));
userMenu.add(new MenuItem("삭제", userCommand));
mainMenu.add(userMenu);
MenuGroup projectMenu = new MenuGroup("프로젝트");
projectMenu.add(new MenuItem("등록", projectCommand));
projectMenu.add(new MenuItem("목록", projectCommand));
projectMenu.add(new MenuItem("조회", projectCommand));
projectMenu.add(new MenuItem("변경", projectCommand));
projectMenu.add(new MenuItem("삭제", projectCommand));
mainMenu.add(projectMenu);
MenuGroup boardMenu = new MenuGroup("게시판");
boardMenu.add(new MenuItem("등록", boardCommand));
boardMenu.add(new MenuItem("목록", boardCommand));
boardMenu.add(new MenuItem("조회", boardCommand));
boardMenu.add(new MenuItem("변경", boardCommand));
boardMenu.add(new MenuItem("삭제", boardCommand));
mainMenu.add(boardMenu);
mainMenu.add(new MenuItem("도움말", helpCommand));
mainMenu.add(new MenuItem("명령내역", historyCommand));
mainMenu.setExitMenuTitle("종료");
}
- execute()수정하기
- execute()의 기능은 menu로 이관되어 더이상 필요없다.
void execute() {
try {
mainMenu.execute();
} catch (NumberFormatException ex) {
System.out.println("숫자로 메뉴 번호를 입력하세요.");
}
System.out.println("종료합니다.");
Prompt.close();
}
- 기타 필요없는 메서드 삭제
- 기타 메뉴 출력 관련 메서드는 menu로 이관되어 삭제한다.
4.결과 확인하기
- Menu그룹을 나눔으로써 메뉴의 기능과 커맨드의 기능을 분리 하였다.
- Menu를 출력하는 UI는 Menu에서 실행되어 콘솔창에 출력되고, tree노드에 접근 할때 까지 Menu에서 실행된다.
- tree에 접근하면 커맨드 기능이 실행되어 결과값들을 담고, 다시 Menu로 넘어간다.
Command 복제 패턴
1. 복제패턴의 필요성
MenuGroup userMenu = new MenuGroup("회원");
userMenu.add(new MenuItem("등록", userCommand));
userMenu.add(new MenuItem("목록", userCommand));
userMenu.add(new MenuItem("조회", userCommand));
userMenu.add(new MenuItem("변경", userCommand));
userMenu.add(new MenuItem("삭제", userCommand));
mainMenu.add(userMenu);
- 코드를 보면 모두 같은 command를 사용하고 있다.
- 등록에서는 command에서 add기능만 필요로 하기 때문에 command의 전체를 호출하는 것은 비 효율적이다.
- MenuItem에 새로운 메뉴가 추가되면 userCommand 전체를 수정해야한다.
2. 복제패턴의 구현
- 기존의 메서드들을 각 클래스 파일로 분할 하면 된다.
public class UserAddCommand implements Command {
private List<User> userList;
public UserCommand(List<User> list) {
this.userList = list;
}
@Override
public void execute(String menuName) {
User user = new User();
user.setName(Prompt.input("이름?"));
user.setEmail(Prompt.input("이메일?"));
user.setPassword(Prompt.input("암호?"));
user.setTel(Prompt.input("연락처?"));
user.setNo(User.getNextSeqNo());
userList.add(user);
}
}
//동일 한 방법으로 클래스를 나눈다.
기존의 App 클래스의 생성자는 다음과 같이 변경된다.
public App() {
List<User> userList = new ArrayList<>();
List<Project> projectList = new LinkedList<>();
List<Board> boardList = new LinkedList<>();
MenuGroup userMenu = new MenuGroup("회원");
userMenu.add(new MenuItem("등록", new UserAddCommand(userList)));
userMenu.add(new MenuItem("목록", new UserListCommand(userList)));
userMenu.add(new MenuItem("조회", new UserViewCommand(userList)));
userMenu.add(new MenuItem("변경", new UserUpdateCommand(userList)));
userMenu.add(new MenuItem("삭제", new UserDeleteCommand(userList)));
mainMenu.add(userMenu);
MenuGroup projectMenu = new MenuGroup("프로젝트");
ProjectMemberHandler memberHandler = new ProjectMemberHandler(userList);
projectMenu.add(new MenuItem("등록", new ProjectAddCommand(projectList, memberHandler)));
projectMenu.add(new MenuItem("목록", new ProjectListCommand(projectList)));
projectMenu.add(new MenuItem("조회", new ProjectViewCommand(projectList)));
projectMenu.add(new MenuItem("변경", new ProjectUpdateCommand(projectList, memberHandler)));
projectMenu.add(new MenuItem("삭제", new ProjectDeleteCommand(projectList)));
mainMenu.add(projectMenu);
MenuGroup boardMenu = new MenuGroup("게시판");
boardMenu.add(new MenuItem("등록", new BoardAddCommand(boardList)));
boardMenu.add(new MenuItem("목록", new BoardListCommand(boardList)));
boardMenu.add(new MenuItem("조회", new BoardViewCommand(boardList)));
boardMenu.add(new MenuItem("변경", new BoardUpdateCommand(boardList)));
boardMenu.add(new MenuItem("삭제", new BoardDeleteCommand(boardList)));
mainMenu.add(boardMenu);
mainMenu.add(new MenuItem("도움말", new HelpCommand()));
mainMenu.add(new MenuItem("명령내역", new HistoryCommand()));
mainMenu.setExitMenuTitle("종료");
}