Sensors 클래스 안에서 객체 유형을 관리하고 변환한다.
또한 Sensors 클래스는 프로그램에 필요한 인터페이스만 제공한다.
그래서 코드는 이해하기 쉽지만 오용하기는 어렵다.
Sensors 클래스는 (나머지 프로그램이) 설계규칙과 비즈니스 규칙을 따르도록 강제할 수 있다.
8장에서 소개하고 있는 Sensors 클래스는 일급 클래스의 일종이다.
Sensors는 Map을 내부에 가지고 있고, 이를 조작할 수 있는 메서드를 제한적으로 제공한다.
이를 통해 Sensors 내부에 있는 Map 자료구조를 보호할 수 있다.
public class Player {
private final String name;
private List<Card> cardList;
public static Player of(Name name) {
return new Player(name);
}
private Player(Name name) {
this.name = name;
this.cardList = new ArrayList<>();
}
public List<Card> getCardList() {
return cardList;
}
}
@Test
@DisplayName("플레이어가 자신의 카드 리스트에 새로운 카드를 추가")
void addCard() {
// given
player = Player.of("Tom");
Card card = Card.of(CardNumber.TWO, CardType.CLOVER);
// when
player.getCardList().add(card);
// then
assertThat(player.getCardList().contains(card)).isTrue();
}
위 코드를 살펴보면, getter()를 통해 플레이어의 카드목록에 자유롭게 접근하고, 해당 컬렉션의 객체를 자유롭게 추가/삭제할 수 있다.
위와 같이 작성할 경우 ArrayList의 다양한 메서드를 자유롭게 사용할 수 있다.
하지만 어디에서 플레이어의 카드 리스트가 수정될지 모른다는 위험성이 존재한다.
비즈니스 로직에서 필요한 add(), contains() 외에도, 카드 내의 요소를 삭제하는 remove() 등의 메서드가 다른 곳에서 무분별하게 사용될 수 있다.
이로 인해 예상치 못하게 side effect가 발생할 수 있고, 엉뚱한 곳에서 카드리스트가 수정될 가능성이 있다.
따라서 CardList는 일급 컬렉션으로 포장하는 것이 적절하다.
public class Cards {
private final List<Card> cardList;
public static Cards newInstance() {
return new Cards();
}
private Cards() {
cardList = new ArrayList<>();
}
public void add(Card card) {
cardList.add(card);
}
public boolean contains(Card card) {
return cardList.contains(card);
}
}
public class Player {
private final String name;
private Cards cards;
... // 생성 메서드
public Cards getCards() {
return cards;
}
}
위와 같이 수정할 경우, Cards를 이용해 수행할 수 있는 메서드는 contains, add로 제한된다.
이를 통해 예측할 수 없는 side effect의 가능성이 줄게 된다.
그러나 객체지향 관점에서는 위 코드도 완전하지 못한 클래스이다.
한 번 Cards 객체가 생성되면, 내부의 데이터는 그 이후로 변하지 않도록 불변 객체로 만드는 것이 적절하다.
위의 Cards 객체는 add() 메서드가 호출될 때마다 내부의 데이터가 변한다.
또한 Player의 getter를 통해 Cards를 직접 꺼내서 메서드를 호출하는 것도 적절치 못하다.
메시지를 보내 처리하도록 수정해야 한다.
아래와 같이 수정하자.
public class Cards {
private final List<Card> cardList;
... // newInstance() 생성 메서드
public static Cards of(List<Card> cards) {
return new Cards(cards);
}
private Cards(List<Card> cards) {
cardList = cards;
}
public Cards add(Card card) {
ArrayList<Card> newCardList = new ArrayList<>(cardList);
newCardList.add(card);
return Cards.of(newCardList);
}
}
public class Player {
private final String name;
private Cards cards;
... // 생성 메서드
public void addCard(Card card) {
cards = cards.add(card);
}
public boolean hasCard(Card card) {
return cards.contains(card);
}
}
@Test
@DisplayName("플레이어가 자신의 카드 리스트에 새로운 카드를 추가")
void addCard() {
// given
player = Player.of("Tom");
Card card = Card.of(CardNumber.TWO, CardType.CLOVER);
// when
player.addCard(card);
// then
assertThat(player.hasCard(card)).isTrue();
}