상태(State) 패턴
객체 내부 상태 변경에 따라 객체의 행동이 달라지는 패턴
상태에 특화된 행동을 분리해 낼 수 있으며, 새로운 행동을 추가하더라도 다른 행동에 영향을 주지 않는다. OCP
상태(State) 패턴 before
Student 클래스
public class Student {
private String name;
public Student(String name){
this.name = name;
}
private List<OnlineCourse> privateCourses = new ArrayList<>();
public boolean isEnabledForPrivateClass(OnlineCourse onlineCourse){
return this.privateCourses.contains(onlineCourse);
}
public void addCourse(OnlineCourse onlineCourse){
this.privateCourses.add(onlineCourse);
}
}
OnlineCourse 클래스
public class OnlineCourse {
public enum State{
DRAFT, PUBLISHED, PRIVATE
}
private State state = State.DRAFT;
private List<String> reviews = new ArrayList<>();
private List<Student> students = new ArrayList<>();
public void addReview(String review, Student student) {
if (this.state == State.PUBLISHED) {
this.reviews.add(review);
}
else if (this.state == State.PRIVATE && this.students.contains(student)) {
this.reviews.add(review);
}
else {
throw new UnsupportedOperationException("리뷰를 작성할 수 없습니다.");
}
}
public void addStudent(Student student) {
if (this.state == State.DRAFT || this.state == State.PUBLISHED) {
this.students.add(student);
}
else if (this.state == State.PRIVATE && availableTo(student)) {
this.students.add(student);
}
else {
throw new UnsupportedOperationException("학생을 해당 수업에 추가할 수 없습니다.");
}
if (this.students.size() > 1) {
this.state = State.PRIVATE;
}
}
public void changeState(State newState) {
this.state = newState;
}
public State getState() {
return state;
}
public List<String> getReviews() {
return reviews;
}
public List<Student> getStudents() {
return students;
}
private boolean availableTo(Student student) {
return student.isEnabledForPrivateClass(this);
}
}
강의가 있고 강의를 듣는 학생들은 현재 강의 상태에 따라서 동작이 달라진다.
만약 강의 상태가 추가된다면?
리뷰 등록 / 학생 추가 등등... 로직에 변화가 생긴다.
또한 이렇게 상태에 따라서 if else 로 분기 처리는 가독성이 떨어진다.
즉, 현재 코드는 가독성이 떨어지고 OCP가 지켜지지 않고 있다.
확장을 하려면 기존 로직에 변경이 생기기 때문이다.
이런 문제를 State Pattern을 통해서 해결해 보자.
상태(State) 패턴 after
State 인터페이스
public interface State {
void addReview(String review, Student student);
void addStudent(Student student);
}
Student 클래스
@ToString
public class Student {
private String name;
public Student(String name) {
this.name = name;
}
private Set<OnlineCourse> onlineCourses = new HashSet<>();
public boolean isAvailable(OnlineCourse onlineCourse){
return onlineCourses.contains(onlineCourse);
}
public void addCourse(OnlineCourse onlineCourse){
this.onlineCourses.add(onlineCourse);
}
}
State Implementations
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){ // 수강 인원 최대 2명
this.onlineCourse.changeState(new Private(this.onlineCourse));
}
}
}
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 Private implements State{
private OnlineCourse onlineCourse;
public Private(OnlineCourse onlineCourse) {
this.onlineCourse = onlineCourse;
}
/**
* 현재 코드를 보면 onlineCourse.getStudents().contains(); 이렇게 체이닝을 2번 사용해서 가져온다.
* 즉, 내부 컬렉션에 직접 접근하는데 이는 캡슐화를 위반한다.
* 이는 리팩토링 관점에서 변화가 필요해 보인다.
* OnlineCourse 클래스에서 객체의 내부 컬렉션 상태를 직접 노출하는 대신, 의미 있는 행위를 캡슐화한 메서드를 제공하는 것이 더 좋은 설계이다.
*/
@Override
public void addReview(String review, Student student) {
if(this.onlineCourse.getStudents().contains(student)){
this.onlineCourse.getReviews().add(review);
return;
}
throw new UnsupportedOperationException("프라이빗 코스를 수강하는 학생만 리뷰를 남길 수 있습니다.");
}
@Override
public void addStudent(Student student) {
if(student.isAvailable(this.onlineCourse)){
this.onlineCourse.getStudents().add(student);
return;
}
throw new UnsupportedOperationException("프라이빗 코스를 수강할 수 없습니다.");
}
}
Online Course 클래스
@Getter
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 void changeState(State state) {
this.state = state;
}
}
현재 OnlineCourse 클래스의 리뷰 추가와 수강생 추가는 State 인터페이스 구현체로 위임한다.
State Pattern 적용 전에는 상태에 따라 분기 처리로 가독성이 많이 떨어졌지만
적용 후에는 상태에 따라 동작 확인을 각 State 클래스를 확인하면 돼서 가독성이 좋아졌다.
또한 새로운 상태가 추가하려면 새로운 클래스만 정의하면 된다.
이를 통해 OCP가 지켜지게 되었다.
상태(State) 패턴 장단점
장점
상태에 따른 동작을 개별 클래스로 옮겨서 관리할 수 있다
기존의 특정 상태에 따른 동작을 변경하지 않고 새로운 상태에 다른 동작을 추가할 수 있다.
코드 복잡도를 줄일 수 있다
단점
복잡도가 증가한다. (클래스가 늘어난다)
'Computer Sience > Desgin Pattern' 카테고리의 다른 글
[Design Pattern] 템플릿 메소드, 템플릿 콜백 (Template method, Template-Callback) 패턴 (1) | 2024.11.25 |
---|---|
[Design Pattern] 전략 (Strategy) 패턴 (1) | 2024.11.25 |
[Design Pattern] 옵저버(Observer) 패턴 (0) | 2024.11.25 |
[Design Pattern] 메멘토(Memento) 패턴 (1) | 2024.11.07 |
[Design Pattern] 중재자(Mediator) 패턴 (1) | 2024.11.04 |