객체지향의 사실과 오해 - 07장. 함께 모으기
객체지향의 사실과 오해 7장
07장. 함께 모으기
코드와 모델을 밀접하게 연관시키는 것은 코드에 의미를 부여하고 모델을 적절하게 한다.
개념 관점에서 설계는 도메인 안 개념과 그 사이의 관계를 표현한다.
- 사용자가 도메인을 바라보는 관점을 반영
- 실제 도메인의 규칙과 제약을 유사하게 반영
명세 관점에서는 도메인을 벗어나 개발자 영역인 소프트웨어로 초점을 이동.
- 소프트웨어 안 객체들의 책임에 초점을 맞춤
- 객체의 인터페이스
- 객체가 무엇을 할 수 있는지
"구현이 아니라 인터페이스에 대해 프로그래밍하라."
구현 관점에서는 실제 작업을 수행하는 코드에 연관되어 있음.
-
객체들이 책임을 수행하는데 필요한 동작하는 코드를 작성하는 것.
-
객체가 어떻게 책임을 수행할 것인지
-
클래스가 은유하는 개념은 도메인 관점을 반영
-
클래스의 공용 인터페이스는 명세 관점을 반영
-
클래스 속성, 메서드는 구현 관점을 반영
커피 전문점 도메인
커피 주문
손님
- 메뉴를 보고 커피를 주문
바리스타 - 주문받은 커피 제조
커피 전문점이라는 세상
대충 커피 전문점에는 메뉴가 있고
- 아메리카노
- 카푸치노
- 카라멜 마키아또
- 에스프레소
가 있다
메뉴판은 이 4개로 이뤄져있는데
메뉴, 메뉴 항목들 모두 객체이다.
그래서 메뉴는 4개의 항목 객체를 포함하는 객체라 볼 수 있다.
주문 흐름으론 손님이 메뉴판을 보고 바리스타에게 커피를 주문한다.
이 때
- 손님
- 바리스타
- 메뉴판
- 선택한 메뉴 항목
모두 객체이다.
여기서 손님은 메뉴판에서 커피를 선택할 수 있어야한다.
- 손님은 메뉴판을 알아야 함
- 이는 두 객체 사이의 관계가 존재한다는 것
손님은 바리스타에게 주문을 해야한다.
- 이는 두 객체 사이의 관계가 존재한다는 것
- 손님과 바리스타 사이에도 관계가 존재함
바리스타는 커피를 제조한다. - 바리스타와 커피 사이에도 관계가 존재함
여기서 같은 타입의 객체는 하나의 인스턴스로 분류 가능한데
-
손님 타입 인스턴스
-
바리스타 타입 인스턴스
-
커피 타입 인스턴스
-
메뉴판 타입 인스턴스
가 된다. -
메뉴판에는 메뉴항목 4개가 합성 관계
-
손님과 메뉴판 사이는 연관 관계
이런 관계들로 도메인을 단순화해 표현한 모델을 도메인 모델이라 한다.
설계하고 구현하기
커피를 주문하기 위한 협력 찾기
휼륭한 객체를 설계하는 것이 아니라 휼륭한 협력을 설계하는 것
협력을 설계할 때는 객체가 메시지를 선택하는 것이 아니라 메시지가 객체를 선택하게 해야 한다.
선 메시지 선택 후 객체 결정
위 커피 전문점에서는 첫 메시지는 '커피를 주문하라'
이 메시지에는 부가적으로 특정 메뉴를 포함할 것이다.
여기서 객체를 선택해야하는데
주문할 책임은? 손님
그러면 손님은 메뉴를 모르니까 '메뉴 항목을 찾아라' 메시지가 나온다.
그리고 응답으로 메뉴 항목을 반환 받아야 한다.
메뉴 항목을 찾는 책임은? 메뉴판
메뉴판은 메뉴항목을 손님에게 반환한다.
그러면 다음으론 '커피를 제조하라'
당근 이 책임은? 바리스타
바리스타에게 특정 메뉴항목을 부가적인 인자로 전달하고
커피를 반환 받는다.
인터페이스 정리하기
위에서 말한 내용들은 객체의 인터페이스다.
- 메시지가 객체를 선택
- 선택한 객체는 메시지를 인터페이스로 받음
그래서
손님 객체
- 커피를 주문하라
메뉴판 - 메뉴 항목을 찾아라
바리스타 - 커피를 제조하라
커피 - 생성하라
class Customer {
public void order(String menuName) {}
}
class MenuItem {
}
class Menu {
public MenuItem choose(String name) {}
}
class Barista {
public Coffee makeCoffee(MenuItem menuItem) {}
}
class Coffee {
public Coffee(MenuItem menuItem) {}
}
구현하기
Customer
는 Menu
에게 menuName
에 해당하는 MenuItem
을 찾아달라 요청
그리고 MenuItem
을 받아 Barista
에게 전달한다.
여기서 Customer
가 Menu
, Barista에
접근하려면 객체에 대한 참조를 얻어야 한다.
따라서 Customer
의 order()
메서드 인자로 Menu
, Barista
의 객체를 얻는 방식을 사용한다.
class Customer {
public void order(String menuName, Menu menu, Barista barista) {
MenuItem menuItem = menu.choose(menuName);
Coffee cofee = barista.makeCoffee(menuItem);
...
}
}
요런식
다음으로
Menu
는 menuName
에 해당하는 MenuItem
을 찾아야 한다.
따라서 Menu
안 MenuItem
을 관리하고 있어야한다.
class Menu {
private List<MenuItem> items;
public Menu(List<MenuItem> items) {
this.items = items;
}
public MenuItem choose(String name) {
for (MenuItem each : items) {
if (each.getName().equals(name)) {
return each;
}
}
return null;
}
}
Barista
는 MenuItem
을 이용해 커피를 제조한다.
class Barista {
public Coffee makeCoffee(MenuItem menuItem) {
Coffee coffee = new Coffee(menuItem);
return coffee;
}
}
Coffee
는 자기자신을 생성하기 위한 생성자를 제공.
내부 커피 이름과 가격을 속성으로 가진다.
class Coffee {
private String name;
private int price;
public Coffee(MenuItem menuItem) {
this.name = menuItem.getName();
this.price = menuItem.cost();
}
}
MenuItem
는 getName()
, cost()
를 메서드로 가져야한다.
class MenuItem {
private String name;
private int price;
public MenuItem(String name, int price) {
this.name = name;
this.price = price;
}
public int cost() {
return price;
}
public String getName() {
return name;
}
}
코드와 세 가지 관점
코드는 세 가지 관점을 모두 제공해야 한다
개념 관점에서
Customer
, Menu
, MenuItem
, Barista
, Coffee
클래스가 있다.
이름에서 알 수 있듯 소프트웨어 클래스와 도메인 클래스 사이 간격이 좀아 기능을 변경하기 위해 뒤적거리는 코드의 양이 줄어든다.
면세 관점에서
클래스의 인터페이스를 본다.
클래스의 public 메서드는 공용 인터페이스를 드러낸다.
인터페이스를 통해 구현과 관련된 세부 사항이 드러나지 않게 해야 한다.
구현 관점에서
내부 구현을 바라본다.
클래스의 메서드와 속성은 구현에 속하고 공용 인터페이스가 아니다.
메서드의 구현과 속성은 외부의 객체에게 영향을 끼치면 안된다.
도메인 개념을 참조하는 이유
메시지가 있을 때 수신할 객체를 찾는 방법??
- 도메인 개념 중 가장 적절한 것을 선택
도메인 개념 안에서 선택은 도메인 지식을 기반으로 코드의 구조와 의미를 쉽게 유추할 수 있게 해준다.
소프트웨어는 항상 변한다.
설계는 변경을 위해 존재한다.
여러 클래스로 기능을 분할 인터페이스와 구현을 분리하는 것은 변경 시 코드를 수월하게 수정하고 싶기 때문.
인터페이스와 구현을 분리하라
인터페이스와 구현을 분리하라.!!!
명세 관점과 구현 관점을 섞지 마라.
명세 관점은 클래스의 안정적인 측면을
구현 관점은 클래스의 불안정한 측면을 드러내야 한다.
중요한 것은 클래스를 명세 관점, 구현 관점으로 나눠볼수 있어야 한다는 것이다.