콜렉션을 포함한 클래스는 반드시 다른 맴버 변수가 없어야 한다.
— The ThourghtWorks Anthology
객체지향 생활체조 파트 규칙 8에서 언급
객체지향 생활체조 파트 규칙 8에서 언급
as-is
public class User {
private List<Email> emails;
}
public class Email {
private String local;
private String domain;
}
to-be
public class User {
private Emails emails;
}
/* first class collection */
public class Emails {
private List<Email> emails;
}
publc class Email {
private String local;
private String domain;
}
이점
비지니스에 종속적인 자료구조
이메일에 대해서 아래와 같은 조건이 있을 경우 구현은 다음과 같다.
-
이메일은 중복 값이 없어야 한다.
-
이메일은 최대 10개만 저장할 수 있다.
as-is
public class User {
private static final int MAX_EMAIL = 10;
private List<Email> emails;
private void addEmail(Email email) {
if (emails.size() < MAX_EMAIL) {
throw new InvaliedException("A maximum of " + MAX_EMAIL + " Email addresses are allowed");
}
if (members.stream().anyMatch(email::equals)) {
throw new InvalidException(email.getContent() + " is already in ter emails");
}
emails.add(email);
}
}
이처럼 이메일 관련 검증 로직과 최대 갯수에 대한 비지니스 요구사항까지 User
가 가지게 된다.
일급 컬렉션을 사용하면 이메일 관련 요구사항은 Emails
에서만 관리하게 되면서 응집도(Cohesion)를 높히고 User
와 Email
에 대한 커플링(Coupling)을 낮출 수 있다.
to-be
public class User {
private Emails emails;
}
public class Emails {
private static final int MAX_EMAIL = 10;
private List<Email> emails;
private void addEmail(Email email) {
if (emails.size() < MAX_EMAIL) {
throw new InvaliedException("A maximum of " + MAX_EMAIL + " Email addresses are allowed");
}
if (members.stream().anyMatch(email::equals)) {
throw new InvalidException(email.getContent() + " is already in ter emails");
}
emails.add(email);
}
}
Collection의 불변성을 보장
일급 컬렉션은 컬렉션의 불변을 보장하는데, 단순히 final
을 사용하는 것이 아니라 캡슐화를 통해 이뤄진다. final
은 재할당만 금지할 뿐이다.
Emails
클래스에 생성자와 getter 외에 다른 메소드가 없다. 즉, 아래와 같이 setter를 구현하지 않으면 불변 컬렉션이 된다.
public class Emails {
private final List<Email> emails;
public Emails(List<Email> emails) {
this.emails = emails;
}
public Emails getEmail() {
return new Email(emails.stream()...);
}
private Optional<Email> getRepresentEmail() {
return new Email(emails.stream().filter(Email::isRepresent).findFirst());
}
}
상태와 행위를 한 곳에서 관리
일급 컬렉션은 값과 로직이 함께 존재하기 때문에 응집도가 높아진다. 즉, Emails
컬렉션을 사용하면 똑같은 기능을 중복 생성하지 않고, 히스토리를 한곳에서 관리할 수 있다.
as-is
public class User {
private List<Email> emails;
private Optional<Email> getRepresentEmail() {
return emails.stream().filter(Email::isRepresent).findFirst();
}
}
to-be
public class User {
private Emails emails;
}
public class Group {
private Emails emails;
}
public class Emails {
private List<Email> emails;
private Optional<Email> getRepresentEmail() {
return emails.stream().filter(Email::isRepresent).findFirst();
}
}
이름이 있는 컬렉션
예를 들어, NAVER Emails에 대한 요구사항을 검색하거나 선언할 경우 아래와 같은 문제점을 겪을 수 있다.
-
담당자마다 변수명이 다르다.
-
중요한 값이지만 명확하게 표현해둔 단어/변수명이 없다.
일급 컬렉션을 사용한다면 NAVER Email에 대한 요구사항이 바뀌었을 경우 NaverEmails
만 검색하면 사용 코드를 모두 찾을 수 있다.
as-is
@Test
public void 이름이_있는_컬렉션() {
List<Email> googleEmails = createGoogleEmails();
List<Email> naverEmails = createNaverEmails();
}
to-be
@Test
public void 이름이_있는_컬렉션() {
private GoogleEmails googleEmails = new GoogleEmails(createGoogleEmails());
private NaverEmails naverEmails = new NaverEmails(createNaverEmails());
}