데이터 체계화
Updated:
데이터 체계화
- 읽기/쓰기 메서드를 통해 접근해야 할때는 필드 자체 캡슐화를 실시한다.
- 데이터 값을 객체로 전환 기법을 실시하면 불명확한 데이터를 뚜렷한 객체로 바꿀 수 있다.
- 이런 객체가 여러 부분에 사용될 인스턴스라면 값을 참조로 전환을 실시해서 참조 객체로 만들면 된다.
- 배열이 데이터 구조 역할을 할 때는 배열을 객체로 전환을 실시하면 그 데이터 구조가 더 선명해진다.
- 상수, 배열크기 같은 숫자들은 기호 상수로 전환 기법을 실시한다.
- 단방향 연결, 양방향 연결 전환 기법.
- 관측 데이터 복제 기법.
- public 데이터를 정리할 대는 필드 캡슐화
- 데이터가 컬렉션이라면 컬렉션 캡슐화
- 하나의 레코드가 통째로 공개되어 있으면 레코드를 데이터 클래스로 전환을 실시하자.
- 분류부호란, 일종의 인스턴스에 관한 특별한 내용을 나타내는 특수한 값이다.
- 분류부호가 정보만 제공하고 클래스의 기능을 변경하진 않을 때는 분류 분호를 클래스로 전환기법을 실시.
- 클래스의 기능이 분류 부호에 영향을 미칠 땐 가능하다면 분류 부호를 하위클래스로 전환 기법을 실시한다.
- 이 기법을 적용하기 불가능할 땐 더욱 복잡하지만 유연한 분류 부호를 상태/전략 패턴으로 전환 기법을 실시한다.
1. 필드 자체 캡슐화
- 필드에 직접 접근하던 중 그 필드로의 결합에 문제가 생길 땐 그 필드용 읽기/쓰기 메서드를 작성해서 두 메서드를 통해서만 필드에 접근하게 만들자.
private int low;
private int high;
boolean includes(int arg) {
return arg >= low && arg <= high;
}
private int low;
private int high;
public int getLow() {
return low;
}
public int getHigh() {
return high;
}
boolean includes(int arg) {
return arg >= getLow() && arg <= getHigh();
}
1.1 동기
- 기본적으로 변수 간접 접근 방식을 사용하면 하위 클래스가 메서드에 해당 정보를 가져오는 방식을 재정의할 수 있다.
- 데이터 관리가 더 유연해진다.
- 직접 접근 방식은 코드를 알아보기 쉬움.
- 팀원끼리 정해서 하는 편. 혼자 남았을 경우 직접 접근 방식을 우선적으로 고려.
- 그러다가 이상이 생기면 간접 접근으로 바꾼다.
- 상위클래스 안의 필드에 접근하되 이 변수 접근을 하위클래스에서 계산된 값으로 재정의해야 할 때다.
- 먼저 필드를 자체 캡슐화한 후 필요할 때 읽기 메서드와 쓰기 메서드를 재정의하면 된다.
1.2 방법
- 필드 읽기, 쓰기 메서드를 작성하자.
- 그 필드 참조 부분을 전부 찾아서 읽기 메서드와 쓰기 메서드로 고치자.
- 필드 읽기 코드를 읽기 메서드 호출로 수정하고, 대입문을 쓰기 메서드 호출로 수정하자.
- 컴파일러를 이용하면 필드명을 임시로 변경해서 쉽게 검사할 수 있다.
- 필드를 private로 만들자.
- 참조 코드를 빠짐없이 수정했는지 다시 확인하자.
1.3 예제
- 위의 예제와 비슷
- 자체 캡슐화를 실시할 때는 생성자 안에 쓰기 메서드를 사용할 때 주의해야 한다.
- 대체로 객체가 생성된 후엔 속성을 변경하려고 쓰기 메서드를 사용하므로, 쓰기 메서드에 초기화 시점과 다른 기능이 추가됐을 수 있다고 전제할 때가 많다.
- 이럴 땐 생성자나 별도의 초기화 메서드에서 직접 접근하게 하는 것이 좋다.
public class IntRange {
private int low;
private int high;
IntRange(int low, int high) {
initialize(low, high);
}
private void initialize(int low, int high) {
this.low = low;
this.high = high;
}
public int getLow() {
return low;
}
public int getHigh() {
return high;
}
boolean includes(int arg) {
return arg >= getLow() && arg <= getHigh();
}
}
- 이렇게 해두면 다음과 같은 하위 클래스가 생길 때 편리해진다.
public class CappedRange extends IntRange{
private int cap;
CappedRange(int low, int high, int cap) {
super(low, high);
this.cap = cap;
}
int getCap() {
return cap;
}
int getHigt() {
return Math.min(super.getHigh(), getCap());
}
}
2. 데이터 값을 객체로 전환
- 데이터 항목에 데이터나 기능을 더 추가해야 할 때는 데이터 항목을 객체로 만들자.
2.1 동기
- 주로 개발 초기 단계에서는 단순 정보를 간단한 데이터 항목으로 표현하는 사안에 대해 결정한다.
- 개발이 진행되고 점점 복잡해진다.
- 예를 들어 전화번호의 경우 처음에는 문자열로 표현하다가 형식화, 지역번호 추출 등 특수 기능이 필요해진다.
- 한두 항목은 객체 안에 메서드를 넣어도 되지만 금세 중복되거나 복잡해진다.
- 이때 데이터 값을 객체로 전환한다.
2.2 방법
- 데이터 값을 넣을 클래스를 작성하자. 그 클래스에 원본 클래스 안의 값과 같은 타입의 final 필드를 추가하자. 그 필드를 인자로 받는 생성자와 읽기 메서드를 추가하자.
- 원본 클래스에 든 필드의 타입을 새 클래스로 바꾸자.
- 원본 클래스 안의 읽기 메서드를 새 클래스의 읽기 메서드를 호출하게 수정하자.
- 그 필드가 원본 클래스 생성자 안에 사용된다면 새 클래스의 생성자를 이용해서 필드를 대입하자.
- 새 클래스의 새 인스턴스를 생성하게끔 읽기 메서드를 수정하자.
- 새 객체에 값을 참조로 전환을 적용해야 할 수도 있다.
2.3 예제
- Order 클래스는 주문 고객을 문자열로 저장하며 customer를 객체로 전환해야 한다.
- 이렇게 하면 주소나, 신용등급 같은 데이터 저장 장소와 정보를 이용한 기능이 생긴다.
public class Order {
private String customer;
public Order(String customer) {
this.customer = customer;
}
public String getCustomer() {
return customer;
}
public void setCustomer(String arg) {
this.customer = arg;
}
}
- 앞의 클래스를 사용하는 일부 코드는 다음과 같다.
private static int numberOfOrderFor(Collection<Order> orders, String customer) {
int result = 0;
Iterator<Order> iter = orders.iterator();
while(iter.hasNext()) {
Order each = iter.next();
if(each.getCustomer().equals(customer)) {
result++;
}
}
return result;
}
- 새로 Customer 클래스를 작성한다.
- 문자열 속성의 final 필드에 String 속성을 부여한 이유는 Order 클래스가 현재 String을 사용하기 때문이다.
public class Customer {
private final String name;
public Customer(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
- 이제 customer 필드의 타입을 수정하고 그 필드를 참조하는 메서드를 수정해서 Customer 클래스에 있는 적절한 참조를 사용하게 만든다.
- 메서드, 변수 명들을 더 명확하게 한다.
public class Order {
private Customer customer;
public Order(String customerName) {
this.customer = new Customer(customerName);
}
public String getCustomerName() {
return customer.getName();
}
public void setCustomer(String customerName) {
this.customer = new Customer(customerName);
}
private static int numberOfOrderFor(Collection<Order> orders, String customer) {
int result = 0;
Iterator<Order> iter = orders.iterator();
while(iter.hasNext()) {
Order each = iter.next();
if(each.getCustomerName().equals(customer)) {
result++;
}
}
return result;
}
}
- 여기서 추가적인 리팩토링을 실시하나면 기존 Customer 객체를 매개변수로 받는 쓰기 메서드와 생성자를 추가하는 작업
- Customer에 신용등급, 주소 같을 추가하려면 지금은 불가능하다.
- 왜냐하면 Customer는 값 객체로 취급되기 때문이다.
- 각 Order 인스턴스마다 고유한 Customer 객체가 들어있다.
- Customer에 이런 속성을 추가하려면 Customer에 값을 참조로 전환을 적용해서 한 고객의 모든 주문이 하나의 Customer 객체를 사용하게 해야 한다. 이는 ‘값을 참조로 전환’라는 기법이다.
3. 값을 참조로 전환
- 클래스에 같은 인스턴스가 많이 들어 있어서 이것들을 하나의 객체로 바꿔야 할 땐 그 객체를 참조 객체로 전환하자.
3.1 동기
- 많은 시스템에선 객체를 참조 객체와 값 객체로 분류할 수 있다.
- 참조 객체는 고객이나 계좌 같은 것이다.
- 각 객체는 현실에서의 한 객체에 대응하므로, 둘이 같은지 검사할 때는 객체 ID를 사용한다.
- 값 객체는 날짜나 돈 같은 것이다.
- 값 객체는 전적으로 데이터 값을 통해서만 정의된다.
- 참조 객체와 값 객체 중 어느 것을 사용할지 결정하기가 애매할 때도 있다.
- 처음에는 변경불가 데이터가 들어 있는 값을 사용했다가 나중에 변경 가능 데이터를 추가한다.
- 그 변경으로 객체를 참조하는 모든 곳이 영향 받는지 확인할 때도 있다.
3.2 방법
- 생성자를 팩토리 메서드로 전환을 실시하자.
- 참조 객체로의 접근을 담당할 객체를 정하자.
- 이 기능은 정적 딕셔너리가 레지스트릐 객체가 담당할 수도 있다.
- 참조 객체로의 접근을 둘 이상의 객체가 담당할 수도 있다.
- 객체를 미리 생성할지 사용하기 직전에 생성할지를 정하자.
- 객체를 미리 생성했다가 메모리에서 가져오면 사용 전에 미리 로딩되어 있는지 확인해야 한다.
- 참조 객체를 반환하게 팩토리 메서드를 수정하자.
- 객체를 미리 생성할 경우, 존재하지 않는 객체 요청에 대한 에러 처리를 어떻게 할지 정해야 한다.
- 팩토리 메서드가 원본 객체를 반환함을 한눈에 알 수 있게 팩토리 메서드에 메서드명 변경을 적용해야 할 수도 있다.
3.3 예제
- “데이터 값을 객체로 전환” 절에서 사용한 예제를 그대로 사용
- 이때 Customer 객체는 값 객체이다.
- 각 Order 인스턴스에는 고유의 Customer 객체가 있다.
- 모든 Customer 객체는 여러 주문을 한 사람이 주문했다면 개념상 동일한 고객을 나타내는 객체이다.
- 이럴 때 Customer 객체를 하나만 사용하도록 수정해야 한다.
- 우선 생성자를 팩토리 메서드로 전환을 적용한다.
- 이렇게 하면 생성 절차를 제어할 수 있는데, 이것은 나중에 필요해진다.
- Customer 객체에 다음과 같이 팩토리 메서드를 정의하자.
- 그리고 생성자 호출을 팩토리 메서드 호출로 수정하자.
- 그 생성자 메서드를 private로 만들자.
public class Customer {
private final String name;
private Customer(String name) {
this.name = name;
}
public String getName() {
return name;
}
public static Customer create(String name) {
return new Customer(name);
}
}
public class Order {
public Order(String customerName) {
this.customer = Customer.create(customerName);
}
}
- 이제 Customer 인스턴스에 접근할 방법을 정해야 한다.
- 별도의 객체를 사용하는 것이 좋다.
- 그럴 경우엔 Order에 라인 항목 같은 것을 넣으면 좋다.
- Order는 라인 항목에 접근하는 역할을 한다.
- 그러나 이런 상황에서는 그 정도로 분명한 객체가 존재하지 않는다.
- 이럴 때는 보통 접근거점으로 사용할 레지스트리 객체를 생성한다.
private static Dictionary instances = new Hashtable<>();
- 그 다음, Customer 인스턴스들을 요청이 있을 때 즉석에서 생성할지 아니면 미리 생성해 둘지 결정해야 한다.
- 여기서는 미리 생성해 두는 방식으로 하겠다.
- 이 애플리케이션의 시작 코드 코드 안에 사용되는 Customer 인스턴스들을 로딩하는 코드를 넣는다.
- Customer 인스턴스들은 데이터베이스나 파일에 저장되어 있을 수 있다.
- 간결하게 하고자 여기선 직접 코드에 넣는다.
-
나중에 이 방식을 변경하려면 언제든 알고리즘 전환을 실시한다.
- 팩토리 메서드를 수정해서 미리 생성해둔 Customer 인스턴스를 반환하게 하자.
- Create 메서드는 반드시 미리 생성되어 있던 Customer 인스턴스를 반환하므로 메서드명 변경을 실시해서 이것을 확실하게 나타낸다.
public class Customer {
private final String name;
private static Dictionary<String, Customer> instances = new Hashtable<>();
private Customer(String name) {
this.name = name;
}
public String getName() {
return name;
}
public static Customer getNamed(String name) {
return instances.get(name);
}
static void loadCustomer() {
new Customer("우리 렌터카").store();
new Customer("커피 자판기 운영업 협동조합").store();
new Customer("삼천리 가스 공정").store();
}
private void store() {
instances.put(this.getName(), this);
}
}
4. 참조를 값으로 전환
- 참조 객체가 작고 수정할 수 없고 관리하기 힘들 땐 그 참조 객체를 객체로 만들자.
4.1 동기
- 값을 참조로 전환 기법과 마찬가지로 참조 객체와 값 객체 중 무엇을 사용할지 판단하기가 항상 쉽지가 않다.
- 참조 객체를 사용한 작업이 복잡해지는 순간이 참조를 값으로 바꿔야 할 시점이다.
- 참조 객체는 어떤 식으로든 제어되어야 한다.
- 개발자는 컨트롤러에 항상 적절한 객체를 요청해야 한다.
- 값 객체를 분산 시스템이나 병렬 시스템에 주로 사용된다.
- 값 객체는 변경할 수 없어야 한다는 주요 특성이 있다.
- 하나에 대한 질의를 호출하면 항상 결과가 같아야 한다.
- 그렇기만 하면 같은 것을 나타내는 객체가 많아도 문제가 없다.
- 값을 변경할 수 없다면, 어떤 객체를 수정했을 때 같은 것을 나타내는 다른 객체들도 전부 바뀌는지 확인해야 하는데 너무 힘들다.
- 여기서 변경 불가란 예를 들어 currency(통화)와 value(가치)가 들어 있는 Money 클래스가 있다고 하면 그 클래스는 대체 불가인 값 객체이다.
- 그렇다고 해서 salary(월급)이 변경되는 것은 아니다.
- 월급을 변경하려면 기존 Money 객체에 들어 있는 양을 수정할게 아니라 기존의 Money 객체를 새 Money 객체로 교체해야 한다.
- 관계는 바꿀 수 있지만 Money 객체 자체는 바꿀 수 없다.
4.2 방법
- 전환할 객체가 변경불가인지 변경 가능인지 검사하자.
- 전환할 객체가 변경불가가 아니면, 변경불가가 될 때까지 쓰기 메서드 제거를 실시하자.
- 전환할 객체가 변경불가이면 이 리팩토링은 관두자.
- equlas, hash 메서드를 작성하자.
- 팩토리 메서드를 삭제하고 생성자를 public으로 만들어야 좋을지 생각해보자.
4.3 예제
public class Currency {
private String code;
public Currency (String code) {
this.code = code;
}
public String getCode() {
return code;
}
}
- 이 클래스의 기능은 단순히 code를 저장하고 반환하는 것이다.
- 이 클래스는 참조 객체이므로 사용할 인스턴스를 가져오려면 다음과 같이 주어진 code에 Currency의 동일 인스턴스를 반환하는 메서드를 사용해야 한다.
Currency usd = Currency.get("USD");
- Currency 클래스에는 여러 인스턴스가 들어 있다.
- 생성자만 사용하는 것은 불가능하다.
- 그래서 private인 것이다.
new Curreny("USD").equals(new Curreny("USD"));
- 여기서 참조 객체는 변경불가이므로 이제 다음과 같이 equals 메서드를 재정의 한다.
public class Currency {
private String code;
public Currency (String code) {
this.code = code;
}
public String getCode() {
return code;
}
public boolean equals(Object arg) {
if(!(arg instanceof Currency)) {
return false;
}
Currency other = (Currency) arg;
return code.equals(other.code);
}
}
- equals 메서드를 정의할 때 다음과 같이 hashCode 메서드도 정의해야 한다.
- 간단히 하려면 equals 메서드에 사용되는 모든 필드의 해시 코드를 가져다 XOR 비트 연산을 수행해야 한다.
public int hashCode() {
return code.hashCode();
}
- 이제 Currency 인스턴스를 원하는 수만큼 생성할 수 있다.
- 다음과 같이 Currency 클래스와 팩토리 메서드에 있는 모든 컨트롤러 기능을 삭제하고 생성자만 사용해도 된다.
- 그 생성자를 이제 public으로 만들 수 있다.
5. 배열을 객체로 전환
- 배열을 구성하는 특정 원소가 별의별 의미를 지닐 땐 그 배열을 각 원소마다 필드가 하나씩 든 객체로 전환하자.
5.1 동기
- 배열은 비슷한 객체들의 컬렉션을 일정 순서로 담는 용도로만 사용해야 한다.
5.2 방법
- 배열 안의 정보를 표현할 새 클래스를 작성하자. 그 클래스 안에 배열을 저장할 public 필드를 하나 작성하자.
- 배열 참조 부분을 전부 새 클래스 참조로 수정하자.
- 배열의 각 원소마다 참조 코드에 사용할 읽기 메서드를 하니씩 넣자.
- 배열 원소의 용도를 따서 읽기 메서드 이름을 정하자.
- 참조 부분을 읽기 메서드 호출로 전부 수정하자.
- 배열 참조 부분을 전부 메서드로 교체했으면 배열을 private로 만들자.
- 클래스 안에 배열의 각 원소에 대응되는 하나의 필드를 생성한 후, 그 필드는 사용하게끔 읽기/쓰기 메서드를 수정하자.
- 모든 원소를 필드로 교체했으면 배열을 삭제하자.
5.3 예제
- 스포츠팀 이름, 승수, 패수를 저장할 배열이 있다고 가정
String[] row = new String[3];
row[0] = "리버풀";
row[1] = "15";
String name = row[0];
int wins = Integer.parseInt(row[1]);
- 이 배열을 객체로 바꾸려면 새 클래스로 하나 작성하자.
public class Performance {
public String[] data = new String[3];
}
Performance row = new Performance();
row.data[0] = "리버풀";
row.data[1] = "15";
String name = row.data[0];
int wins = Integer.parseInt(row.data[1]);
- 읽기/쓰기 메서드를 만든다.
public class Performance {
public String[] data = new String[3];
public String getName() {
return data[0];
}
public void setName(String arg) {
data[0] = arg;
}
public int getWins() {
return Integer.parseInt(data[1]);
}
public void setWins(String arg) {
data[1] = arg;
}
}
- row 사용하는 부분마다 읽기/쓰기 메서드를 사용하게 고치자.
Performance row = new Performance();
row.setName("리버풀");
row.setWins("15");
String name = row.getName();
int wins = row.getWins();
- 수정이 완료되면 private으로 바꾸고 배열 원소에 맞게 필드 변수를 추가한다.
public class Performance {
private String name;
private int wins;
private int loses;
public String getName() {
return name;
}
public void setName(String arg) {
name = arg;
}
public int getWins() {
return wins;
}
public void setWins(int arg) {
wins = arg;
}
}
Performance row = new Performance();
row.setName("리버풀");
row.setWins(15);
String name = row.getName();
int wins = row.getWins();
6. 관측 데이터 복제
- 도메인 데이터는 GUI 컨트롤 안에서만 사용 가능한데, 도메인 메서드가 그 데이터에 접근해야 할 땐 그 데이터를 도메인 객체로 복사하고, 양측의 데이터를 동기화하는 관측 인터페이스 observer를 작성하자.
6.1 동기
- 계층구조가 체계적인 시스템은 비즈니스 로직 처리 코드와 사용자 인터페이스 처리 코드가 분리되어 있다.
- 이유는 다음과 같다.
- 비슷한 비즈니스 로직을 여러 인터페이스가 처리해야 하는 경우라서
- 비즈니스 로직까지 처리하려면 사용자 인터페이스가 너무 복잡하니까
- GUI와 분리된 도메인 객체가 더욱 유지보수하기 쉬우니까
- 두 부분을 서로 다른 개발자가 다루게 할 수 있어서
- 기능은 간단히 분리할 수 있어도 데이터는 분리하기 어려울 때가 많다.
- 도메인 모델에 있는 데이터와 같은 의미를 지닌 GUI컨트롤에 넣어야 하기 때문이다.
- MVC 패턴부터 시작해서 그 이후로, 사용자 인터페이스 프레임워크는 이러한 데이터를 제공하고 모든 데이터의 동기화를 유지하는 다층 시스템을 사용했다.
- 비즈니스 로직이 사용자 인터페이스 안에 들어 있는 2계층 방식으로 개발된 코드가 있다면 인터페이스에서 기능을 분리해야 한다.
- 대부분 메서드를 쪼개서 옮기는 작업이다.
- 하지만 데이터는 그저 옮기기만 해선 안 되고 복제하고 동기화 기능까지 작성해야 한다.
6.2 방법
- 표현 클래스를 도메인 클래스의 관측 인터페이스로 만들자.
- 도메인 클래스가 없으면 하나 작성하자.
- 표현 클래스에 도메인 클래스로의 연결 코드가 없으면 표현 클래스의 필드에 도메인 클래스를 대입하자.
- GUI 클래스 안의 도메인 데이터를 대상으로 필드 자체 캡슐화를 실시하자.
- 이벤트 핸들러 메서드 안에 쓰기 메서드 호출 코드를 추가하자. 이 쓰기 메서드는 직접 접근 방식으로 컴포넌트를 현재 값으로 수정한다
- 현재 값에 따라 컴포넌트 값을 수정하는 메서드를 이벤트 핸들러 안에 넣자. 물론 이 단계는 컴포넌트 값을 현재 값으로 설정하는 것뿐이니 필요가 없지만, 쓰기 메서드를 사용하면 어떤 기능이든 실행할 수 있다.
- 이렇게 수정할 때 읽기 메서드를 사용하지 말고 컴포넌트에 직접 접근하자. 나중에 읽기 메서드는 도메인에서 값을 가져오는 데 쓰이는데, 그 값은 쓰기 메서드 실행 전까진 변하지 않는다.
- 테스트 코드로 이벤트 처리 절차를 확인하자.
- 도메인 클래스 안에 데이터와 읽기/쓰기 메서드를 정의하자.
- 도메인에 있는 쓰기 메서드가 반드시 관측 인터페이스 패턴의 통지 절차를 시작하게 만들자.
- 도메인 클래스 안의 데이터 타입을 표현 클래스 안의 데이터 타입과 같게 하자. 주로 문자열 타입이다. 이 데이터 타입을 추가 리팩토링 기법을 실시할 때 변환하자.
- 쓰기 메서드가 도메인 필드에 쓰도록 참조를 수정하자.
- 관측 인터페이스의 update 메서드를 도메인 필드에서 GUI 컨트롤로 데이터를 복사하게 수정하자.
6.3 예제
- 일단 내가 당장 필요한 부분이 아니라 스킵
7. 클래스의 단방향 연결을 양방향으로 전환
- 두 클래스가 서로의 기능을 사용해야 하는데 한 방향으로만 연결되어 있을 땐 역 포인터를 추가하고 두 클래스를 모두 업데이트할 수 있게 접근 한정자를 수정하자.
7.1 동기
- 애초에 두 클래스를 설정할 때 한 클래스가 다른 클래스를 참조하게 해놓은 경우가 있을 수 있다.
- 나중에 참조되는 클래스를 사용하는 부분에서 그 클래스를 참조하는 객체들을 가져와야 할 수도 있다.
- 즉, 포인터를 역방향으로 참조해야 한다.
- 포인터는 단방향 연결이라서 이런 식의 역방향 참조는 불가능하다.
- 다른 경로를 찾아서 해결할 수 있을 때도 있다.
- 그렇게 하면 계산에 비용이 들긴 해도 합리적이며, 이 기능을 사용하는 메서드를 참조되는 클래스에 넣을 수 있다.
- 하지만 이게 쉽지 않을 때도 있으므로 양방향 참조를 설정해야 한다.
7.2 방법
- 역 포인터 참조용 필드를 추가하자.
- 연결 제어 기능을 어느 클래스에 넣을지 정하자.
- 연결 제어 기능이 없는 클래스 안에 헬퍼 메서드를 작성하고, 그 메서드에 제한된 용도가 분명히 드러나는 이름을 붙이자.
- 기존 변경 메서드가 연결 제어 클래스에 들어 있으면 역 포인터를 업데이트하게 변경 메서드를 수정하자.
- 기존 변경 메서드가 연결 제어 클래스에 들어 있으면 제어 클래스 안에 제어 메서드를 작성하고 기존 변경 메서드가 그 메서드를 호출하게 하자.
7.3 예제
- Order 클래스가 Customer 클래스를 참조한다고 하자.
public class Order {
Customer customer;
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer arg) {
this.customer = arg;
}
}
- Customer 클래스에는 Order 클래스를 참조하는 코드가 들어 있지 않다.
- 우선 Customer 클래스에 필드를 하나 추가하자.
- 고객 한 명이 어러 건의 주문을 할 수 있으므로 이 필드는 컬렉션이다.
- 고객은 컬렉션 안에서 두 번 이상 같은 주문을 해서는 안 되므로 다음과 같이 set 컬렉션이 적합하다.
public class Customer {
private Set<Order> orders = new HashSet<Order>();
}
-
이 연결을 제어할 클래스를 정해야 한다.
-
연결을 조작하는 로직을 전부 한 곳에 두기 위해 연결 제어 로직은 하나의 클래스에 넣는 것이 좋다.
-
결정 과정은 다음과 같다.
-
두 객체가 모두 참조 객체이고 연결이 일대다이면 참조가 하나 들어 있는 객체를 제어 객체로 정한다.
(즉, Customer에는 Order 참조가 여러 개이므로 Order 클래스를 연결 제어 객체로 택한다.)
-
한 객체가 다른 객체에 포함될 때는 포함되는 객체를 제어 객체로 정한다.
-
두 객체가 모두 참조 객체이고 연결이 다대다이면, Order나 Customer중 어느 클래스를 연결 제어 객체로 정해도 상관없다.
-
-
Order 클래스에 연결 제어 기능을 구현할 것이므로, 헬퍼 메서드는 주문 컬레견으로 직접 접근 할 수 있게 하는 Customer 클래스에 넣어야 한다.
-
Order 변경자는 이것을 사용해서 양측의 포인터 세트를 동기화하게 된다.
public class Customer {
private Set<Order> orders = new HashSet<Order>();
Set friendOrders() {
/** 연결을 변경할 때 Order에 의해서만 사용되어야 함 */
return orders;
}
}
- 이제 역 포인터를 업데이트하게 변경 메서드를 다음과 같이 수정하자.
public void setCustomer(Customer arg) {
if(customer != null) {
customer.friendOrders().remove(this);
}
this.customer = arg;
if(customer != null) {
customer.friendOrders().add(this);
}
}
- 제어하는 변경 메서드의 정확한 코드는 연결의 다양한 조합에 따라 달라진다.
- customer 변수의 값이 null 되는 게 불가능하다면, null 검사를 취소해도 되지만 null 인자는 꼭 검서해야 한다.
- Customer 클래스를 거쳐서 연결을 변경하려면 다음과 같이 Customer 클래스 안에 제어 메서드 호출 코드를 넣으면 된다.
public class Customer {
private Set<Order> orders = new HashSet<Order>();
Set friendOrders() {
/** 연결을 변경할 때 Order에 의해서만 사용되어야 함 */
return orders;
}
void addOrder(Order arg) {
arg.setCustomer(this);
}
}
- 하나의 주문을 여러 고객이 할 수 있다면 연결이 다대다가 되므로 제어 메서드는 다음과 같다.
class Order {
// 연결 제어 메서드
void addCustomer(Customer arg) {
arg.friendOrders().add(this);
customer.add(arg); // 이게 뭔제 모르겠네
}
void removeCustomer(Customer arg) {
arg.friendOrders().remove(this);
customer.remove(arg);
}
}
public class Customer {
private Set<Order> orders = new HashSet<Order>();
Set friendOrders() {
/** 연결을 변경할 때 Order에 의해서만 사용되어야 함 */
return orders;
}
void addOrder(Order arg) {
arg.setCustomer(this);
}
void removeOrder(Order arg) {
arg.removeCustomer(this);
}
}
8. 클래스의 양방향 연결을 단방향으로 전환
- 두 클래스가 양방향으로 연결되어 있는데 한 클래스가 다른 클래스의 기능을 더 이상 사용하지 않게 됐을 땐 불필요한 방향의 연결을 끊자.
8.1 동기
- 양방향 연결을 유지하고 객체가 적절히 생성되고 제거되는지 확인하는 복잡함이 더해진다.
- 양방향 연결이 익숙하지 않은 대부분의 프로그래머는 에러를 발생시킨다.
- 양방향 연결이 많으면 좀비 객체가 발생하기도 한다.
- 좀비 객체란 참조가 삭제되지 않아 제거되어야 함에도 남아서 떠도는 객체를 뜻한다.
- 양방향 연결로 인해 두 클래스는 서로 종속된다.
- 한 클래스를 수정하면 수정하면 다른 클래스도 변경된다.
- 종속성이 많으면 시스템의 결합력이 강해져서 사소한 수정에도 예기치 못한 각종 문제가 발생한다.
8.2 방법
- 삭제하려는 포인터가 저장된 필드를 읽는 모든 부분을 검사해서 삭제해도 되는지 파악하자.
- 필드를 직접 읽는 메서드와 그 메서드를 호출하는 다른 메서드도 살펴보자.
- 포인터를 사용하지 않고 다른 객체를 알아내는 것이 가능한지 보고, 가능하다면 속성 읽기 메서드에 알고리즘 전환을 실시해서 참조 코드가 포인터 없이 속성 읽기 메서드를 사용할 수 있게 하자.
- 참조 코드가 속성 읽기 메서드를 사용해야 한다면 속성 읽기 메서드에 필드 자체 캡슐화를 적용하고 알고리즘 전환을 적용한 후 테스트를 실시하자.
- 참조코드에 읽기 메서드 호출을 넣을 필요가 없다면, 각 필드 사용 부분을 찾아서 필드 안의 객체를 다른 방법으로 가져오게끔 수정하자.
- 필드 안의 속성 읽기 메서드를 모두 삭제했으면 필드 업데이트 코드 전부와 필드를 삭제하자.
- 필드에 값을 할당하는 여러 군데 있다면 필드 자체 캡슐화를 실시해서 그 모든 부분이 한 개의 속성쓰기 메서드를 사용하게 하자. 테스트를 실시한 후 속성 쓰기 메서드를 빈 메서드로 만들고 다시 테스트한다. 오류가 없으면 필드, 속성 읽기 메서드, 모든 속성 쓰기 메서드 호출 코드를 삭제하자.
8.3 예제
public class Order {
Customer customer;
double getDiscountedPrice() {
return getGrossPrice() * (1 - customer.getDiscount());
}
}
public class Order {
double getDiscountedPrice(Customer customer) {
return getGrossPrice() * (1 - customer.getDiscount());
}
}
- 책에서 자세히 나와 있는데 내가 볼 땐 “클래스 단방향에서 양방향으로” 예제랑 반대 같음.
9. 마법 숫자를 기호 상수로 전환
- 특수 의미를 지닌 리터럴 숫자가 있을 땐 의미를 살린 이름의 상수를 작성한 후 리터럴 숫자를 그 상수로 교체하자.
double potentialEnergy(double mass, double height) {
return mass * 9.81 * height;
}
static final double GRAVITIATONAL_CONSTANT = 9.81;
double potentialEnergy(double mass, double height) {
return mass * GRAVITIATONAL_CONSTANT * height;
}
10. 필드 캡슐화
- public 필드는 private으로 만들고 읽기 /쓰기 메서드를 만들자
11. 컬렉션 캡슐화
- 메서드가 컬렉션을 반환할 땐, 그 메서드가 읽기 전용 뷰를 반환하게 수정하고 추가 메서드와 삭제 메서드를 작성하자.
11.1 동기
- 컬렉션은 다른 종류의 데이터와는 약간 다른 읽기/쓰기 방식을 사용해야 한다.
- 읽기 메서드는 컬렉션 객체 자체를 반환해서는 안된다.
- 왜냐하면 컬렉션 참조 부분이 컬렉션의 내용을 조작해도 그 컬렉션이 든 클래스는 무슨 일이 일어나는지 모르기 때문이다.
- 쓰기 메서드는 절대 있으면 안되므로, 원소를 추가하는 메서드와 삭제하는 메서드를 대신 사용해야 한다.
- 이렇게 하면 컬렉션이 든 객체가 든 객체가 컬렉션의 원소 추가와 삭제를 통제할 수 있다.
- 이렇게 하면, 컬렉션 참조 부분에 대한 종속성이 줄어든다.
11.2 방법
- 컬렉션 원소를 추가, 삭제 메서드를 작성한다.
- 필드를 빈 컬렉션으로 초기화하자.
- 쓰기 메서드 호출 부분을 찾아서 add 메서드오 remove 메서드 호출로 바꾸든지, 그 위치에 직접 컬렉션에 원소를 추가하고 삭제하는 코드를 작성하자.
- 쓰기 메서드가 사용되는 경우는 두 가지다. 첫째는 컬렉션이 비어 있을 때고, 둘째는 비어 있지 않은 컬렉션을 교체할 때다.
- 메서드명 변경을 실시
- set이라는 이름을 initialized나 replace로 변경
- 읽기 메서드를 호출하여 컬렉션을 변경하는 부분을 전부 찾아서 add 메서드, remove 메서드 호출로 바꾸자.
- 컬렉션을 변경하고자 읽기 메서드를 호출하는 부분을 전부 추가/삭제 메서드 호출로 고쳤으면, 컬렉션의 읽기 전용 뷰를 반환하게 읽기 메서드를 수정하자.
- 읽기 메서드 호출부분을 찾고 컬렉션이 컬렉션이 든 객체로 옮겨야 할 코드를 찾아서 메서드 추출과 메서드 이동을 실시해서 컬렉션이 든 객체로 옮기자.
11.2 예제
- 한 사람이 여러 과정을 수강한다.
- 수강 과정을 나타내는 course 클래스는 다음과 같다.
public class Course {
public Course (String name, boolean isAdvanced) {
}
public boolean isAdvance() {
}
}
class Person {
Set<Course> courses;
public Set getCourses() {
return courses;
}
public void setCouses(Set arg) {
courses = arg;
}
}
- 앞의 인터페이스를 이용하여 과정을 추가하는 코드와 고급과정을 알아내는 코드는 다음과 같다.
Person kent = new Person();
Set s = new HashSet();
s.add(new Course("스몰토크 프로그래밍", false));
s.add(new Course("싱글몰트 위스키 음미하기", true));
kent.setCouses(s);
// 여기에 assert 있어야 함
Course refact = new Course("리팩토링", true);
kent.getCourses().add(refact);
kent.getCourses().add(new Course("지독한 빈정거림", false));
// 여기도 assert
kent.getCourses().remove(refact);
// 여기도 assert
public class Course {
String name;
boolean isAdvanced;
public Course (String name, boolean isAdvanced) {
this.name = name;
this.isAdvanced = isAdvanced;
}
public boolean isAdvance() {
return isAdvanced;
}
int countAdvance(Person person) {
Iterator iter = person.getCourses().iterator();
int count = 0;
while(iter.hasNext()) {
Course each = (Course) iter.next();
if(each.isAdvance()) {
count++;
}
}
return count;
}
}
- 이때 다음과 같이 적절한 컬렉션 변경 메서드를 작성한다.
- courses 초기화하면 더 좋다.
class Person {
Set<Course> courses = new HashSet<Course>();
public void addCourse(Course arg) {
courses.add(arg);
}
public void remove(Course arg) {
courses.remove(arg);
}
}
- 다음 쓰기 메서드 호출 부분을 관찰하자.
- 참조 부분이 여러 곳이고 쓰기 메서드가 많이 사용된다면 쓰기 메서드 내용을 추가/삭제 기능의 코드도 바꿔야 한다.
- 이 과정은 쓰기 메서드의 사용 방식에 따라 복잡한 정도가 둘로 나뉜다.
- 간단한 경우는 참조 부분이 쓰기 메서드를 호출해서 값을 초기화할 때다.
- 즉, 쓰기 메서드가 적용되기 전에 과정이 없는 경우다.
- 이럴 땐 쓰기 메서드 내용을 다음과 같이 과정 추가 메서드 호출로 바꾸면 된다.
- 동시에 메서드 이름을 바꾼다. (기능적 의미를 더 분명히 전달)
class Person {
Set<Course> courses = new HashSet<Course>();
public void initializeCourses(Set arg) {
assertTrue(courses.isEmpty());
Iterator iter = arg.iterator();
while(iter.hasNext()) {
addCourse((Course) iter.next());
}
}
}
- 초기화하려고 원소를 추가할 때 추가 기능이 확실히 필요 없다면 다음과 같이 루프를 삭제하고 addAll 메서드 사용한다.
public void initializeCourses(Set arg) {
assertTrue(courses.isEmpty());
courses.addAll(arg);
}
- 이전 세트가 비어 있더라고 그 세트에 직접 대입할 순 없다.
- 참조 부분이 전달 후 그 세트를 수정한다면 캡슐화에 위배된다.
- 그래서 복사해 둬야 한다.
- 참조 부분이 단순히 세트를 생성하고 쓰기 메서드를 사용한다면 다음과 같이 그 참조 부분의 add 메서드와 remove 메서드를 직접 사용하게 수정하고 쓰기 메서드를 삭제하면 된다.
Person kent = new Person();
Set s = new HashSet();
s.add(new Course("스몰토크 프로그래밍", false));
s.add(new Course("싱글몰트 위스키 음미하기", true));
kent.initializeCourses(s);
Person kent = new Person();
kent.addCourse(new Course("스몰토크 프로그래밍", false));
kent.addCourse(new Course("싱글몰트 위스키 음미하기", true));
- 읽기 메서드는 다음과 같다
kent.addCourse(new Course("지독한 빈정거림", false));
- 모든 읽기 메서드 호출 부분을 수정했으면 다음과 같이 읽기 메서드의 내용을 수정불가 뷰를 반환하게 수정해서 읽기 메서드를 통해 수정하는 부분이 없는지 확인하자.
class Person {
Set<Course> courses = new HashSet<Course>();
public Set getCourses() {
return Collections.unmodifiableSet(courses);
}
}
-
이제 컬렉션을 캡슐화 한다.
-
countAdvance의 메서드는 person에서 하는게 좋다. Person 클래스로 옮긴다.
class Person {
private Set<Course> courses = new HashSet<Course>();
public Set getCourses() {
return Collections.unmodifiableSet(courses);
}
public void initializeCourses(Set arg) {
assertTrue(courses.isEmpty());
Iterator iter = arg.iterator();
while(iter.hasNext()) {
addCourse((Course) iter.next());
}
courses.addAll(arg);
}
public void addCourse(Course arg) {
courses.add(arg);
}
public void remove(Course arg) {
courses.remove(arg);
}
int countAdvance() {
Iterator iter = getCourses().iterator();
int count = 0;
while(iter.hasNext()) {
Course each = (Course) iter.next();
if(each.isAdvance()) {
count++;
}
}
return count;
}
}
12. 레코드를 데이터 클래스로 전환
- 전통적인 프로그래밍 환경에서 레코드 구조를 이용한 인터페이스를 제공해야 할 땐 레코드 구조를 저장할 덤 데이터 객체를 작성하자.
12.1 동기
- 레코드 구조를 사용할 경우 외부 요소를 처리할 인터페이스 역할을 하는 클래스를 작성하는 것이 좋다.
- 다른 필드와 메서드는 나중에 옮기면 된다.
12.2 방법
-
레코드를 표현할 클래스를 작성하자.
-
그 클래스에 각 데이터 항목에 대한 읽기/쓰기 메서드를 작성하고 private 필드를 선언하자.
-
이 객체에는 아직 기능이 없지만 추후 소개할 기법에서 기능 추가에 대해 설명한다.
13. 분류 부호를 클래스로 전환
- 기능에 영향을 미치는 숫자형 분류 부호가 든 클래스가 있을 땐 그 숫자를 새 클래스로 바꾸자.
13.1 동기
- 숫자형 분류 부호, 즉, 열거 타입은 코드를 이해하기 쉬워지나 그것 뿐이다.
- 분류부호를 인자로 받는 모든 메서드는 숫자만을 인자로 받으며, 상징적 이름을 전달할 방법은 없다.
-
그래서 코드를 이해하기 힘들어질 수 있고 버그가 생길 수도 있다.
- 숫자형 분류 부호를 클래스로 빼내면 컴파일러는 그 클래스 안에서 종류 판단을 수행할 수 있다.
- 그 클래스 안에 팩토리 메서드를 작성하면 유효한 인스턴스만 생성되는지와 그런 인스턴스가 적절한 객체로 전달되는지를 정적으로 검사할 수 있다.
- 그렇다고 무작정하는 것이 아니라 그 전에 분류 부호를 다른 것으로 전환하는 방안을 고려한다.
- 분류 부호를 클래스로 만드는 건 분류 부호가 순수한 데이터일 때만 실시해야 한다.
- 다시 말해, 분류 부호가 switch 문안에 사용되어 다른 기능을 수행하거나 메서드를 호출할 땐 클래스로 전환하면 안 된다.
- switch문에는 임의의 클래스를 사용할 수 없으며 오직 정수 타입만 사용 가능하므로 클래스로의 전환은 실패를 맞게 된다.
- 더 중요한 건, 모든 switch문은 조건문을 재정의로 전환 기법을 적용해 전부 없애야 하는 대상이다.
- 분류 부호가 값에 따라 다른 기능을 호출하지 않더라도 분류 부호 클래스로 만들면 더 좋은 기능이 있을 수도 있으므로, 메서드 이동 기법을 한두 차례 실시할 것을 고려하자.
13.2 방법
- 분류 부호의 종류를 판단할 새 클래스를 작성하자.
- 그 클래스 안에 분류 부호와 대응하는 code 필드를 선언하고 그 값을 읽을 읽기 메서드를 작성하자.
- 그 클래스에서 생성이 허용되는 인스턴스를 저정할 static 변수와, 원래의 클래스에 따라 다른 인자를 받아서 적절한 인스턴스를 반환하는 static 메서드를 저장한 static 변수도 선언해야 한다.
- 새 클래스를 사용하게 원본 클래스의 내용을 수정하자.
- 기존 코드를 사용하는 인터페이스는 그대로 놔두되, static 필드는 새 클래스를 통해 코드를 생성하게 수정하자.
- 다른 코드를 사용하는 메서드도 새 클래스에서 부호 숫자를 읽게 수정하자.
- 원본 클래스 코드를 사용하는 원본 클래스 안의 각 메서드마다, 그에 대응하는 새 클래스를 사용하는 새 메서드를 작성하자.
- 코드를 인자로 받는 메서드는 새 클래스의 인스턴스를 인자로 받는 새 메서드로 바꿔야 한다.
- 코드를 반환하는 메서드는 새 클래스 인스턴스를 반환하는 새 메서드로 바꿔야 한다.
- 기존 코드를 사용하는 프로그램을 더욱 이해하기 쉽게 만들려면 새 읽기/쓰기 메서드를 작성하기 전에 기존의 읽기/쓰기 메서드에 메서드명 변경을 적용해야 한다.
- 원본 클래스 호출 부분을 한 번에 하나씩 새 인터페이스 호출로 수정하자.
- 전부 수정했으면 컴파일과 테스트를 실시하자.
- 컴파일과 테스트 결과가 일관되게 나오기까지 여러 메서드를 수정해야 할 가능성도 있다.
- 기존 코드를 사용하는 기존 인터페이스를 삭제하고, 기존 코드의 static 변수 선언도 삭제하자.
13.3 예제
- Person 클래스에는 혈액형 그룹이 있다.
class Person {
public static final int O = 0;
public static final int A = 1;
public static final int B = 2;
public static final int AB = 3;
private int bloodGroup;
public Person (int bloodGroup) {
this.bloodGroup = bloodGroup;
}
public void setBllodGroup(int arg) {
this.bloodGroup = arg;
}
public int getBloodGroup() {
return bloodGroup;
}
}
- 먼저 다음과 같이 혈액형 그룹을 판단할 BloodGroup 클래스를 작성하고, 분류 부호 숫자가든 인스턴스를 작성하자.
public class BloodGroup {
public static final BloodGroup O = new BloodGroup(0);
public static final BloodGroup A = new BloodGroup(1);
public static final BloodGroup B = new BloodGroup(2);
public static final BloodGroup AB = new BloodGroup(3);
private final int code;
private BloodGroup(int code) {
this.code = code;
}
public int getCode() {
return this.code;
}
public static BloodGroup code(int arg) {
return values(arg);
}
}
- Person 클래스의 코드를 수정해서 새 클래스를 사용하게 하자.
class Person {
public static final int O = BloodGroup.O.getCode();
public static final int A = BloodGroup.A.getCode();
public static final int B = BloodGroup.B.getCode();
public static final int AB = BloodGroup.AB.getCode();
private BloodGroup bloodGroup;
public Person (int bloodGroup) {
this.bloodGroup = BloodGroup.code(bloodGroup);
}
public void setBloodGroup(int arg) {
this.bloodGroup = BloodGroup.code(arg);
}
public int getBloodGroup() {
return bloodGroup.getCode();
}
}
- 이것으로써 BloodGroup 클래스 안에서 런타임 분류 부호 검사가 이뤄진다.
- 이 수정에서 진정한 효과를 얻으려면 Person 클래스 사용 부분이 int 타입 대신 bloodGroup을 사용하게 해야 한다.
-
우선 Person 클래스의 bloodGroup을 읽는 읽기 메서드명 변경을 적용해서 변경된 코드를 이해하기 쉽게 만들자.
- 새 클래스를 사용하는 읽기 메서드를 작성하자.
- 생성자 메서드와 새 클래스를 사용하는 쓰기 메서드도 작성하자.
class Person {
public static final int O = BloodGroup.O.getCode();
public static final int A = BloodGroup.A.getCode();
public static final int B = BloodGroup.B.getCode();
public static final int AB = BloodGroup.AB.getCode();
private BloodGroup bloodGroup;
public Person (int bloodGroup) {
this.bloodGroup = BloodGroup.code(bloodGroup);
}
public void setBloodGroup(int arg) {
this.bloodGroup = BloodGroup.code(arg);
}
// 수정함. getBloodGroupCode 함수가 생기므로
public BloodGroup getBloodGroup() {
return bloodGroup;
}
// 아래 새로 추가
public Person (BloodGroup bloodGroup) {
this.bloodGroup = bloodGroup;
}
public int getBloodGroupCode() {
return bloodGroup.getCode();
}
public void setBloodGroup(BloodGroup arg) {
this.bloodGroup = arg;
}
}
- 이제 Person 클래스 사용 부분을 수정한다.
- static 변수 참조 부분은 전부 다음과 같이 수정해야 한다.
Person thePerson = new Person(Person.A); // 수정 전
Person thePerson = new Person(BloodGroup.A); // 수정 후
- 읽기 메서드 호출 부분은 다음과 같이 수정한다.
thePerson.getBloodGroupCode(); //수정 전
thePerson.getBloodGroup().getCode();//수정 후
- 쓰기 메서드도 똑같다.
thePerson.setBloodGroup(Person.AB);
thePerson.setBloodGroup(BloodGroup.AB);
- Person 클래스 사용 부분을 전부 수정했으면 읽기, 생성자, static 변수, 정수 타입을 사용하는 쓰기 메서드를 삭제한다.
- 그리고 BloodGroup 클래스에 있는 숫자형 부호 사용 메서드를 private 타입으로 만든다.
class Person {
private BloodGroup bloodGroup;
public BloodGroup getBloodGroup() {
return bloodGroup;
}
public Person (BloodGroup bloodGroup) {
this.bloodGroup = bloodGroup;
}
public void setBloodGroup(BloodGroup arg) {
this.bloodGroup = arg;
}
}
public class BloodGroup {
public static final BloodGroup O = new BloodGroup(0);
public static final BloodGroup A = new BloodGroup(1);
public static final BloodGroup B = new BloodGroup(2);
public static final BloodGroup AB = new BloodGroup(3);
private final int code;
private BloodGroup(int code) {
this.code = code;
}
private int getCode() {
return this.code;
}
private static BloodGroup code(int arg) {
return values(arg);
}
}
14. 분류 부호를 하위클래스로 전환
- 클래스 기능에 영향을 주는 변경불가 분류 부호가 있을 땐 분류 부호를 하위클래스로 만들자.
14.1 동기
- 클래스 기능에 영향을 주지 않는 분류 부호가 있을 땐 분류 부호를 클래스로 전환기법을 실시하면 된다.
- 그러나 분류 부호가 클래스 기능에 영향을 준다면 재정의를 통해 조금씩 다른 기능을 처리하는 것이 최선이다.
- 분류 부호가 클래스 기능에 영향을 미치는 현상은 조건문이 있을 때 주로 나타난다.
- 어느 조건문이든 분류 부호의 값을 검사해서 그 값에 따라 다른 코드를 실행한다.
- 이런 조건문은 조건문을 재정의로 전환을 실시해서 재정의로 바꿔야 한다.
- 이 기법이 효과를 보려면 분류 부호를 다형화된 기능이 든 상속 구조로 고쳐야 한다.
- 그런 상속 구조는 하나의 클래스와 각 분류 부호에 대한 하위클래스로 구성된다.
- 제일 쉬운 방법은 분류 후호를 하위 클래스로 전환을 실시하는 것
- 분류 부호가 든 클래스 코드를 이용해 각 분류 부호별 하위클래스를 작성하자.
- 단, 기법을 적용할 수 없는 몇 가지 경우는 다음과 같다.
- 분류 부호의 값이 객체 생성 후 변할 때
- 다은 이유로 분류 부호를 이미 하위클래스로 만들었을 때
- 분류 부호를 상태/전략 패턴으로 전환을 실시해야 할 때
14.2 방법
- 분류 부호를 자체 캡슐화하자.
- 분류 부호가 생성자 메서드로 전달될 땐 생성자 메서드를 팩토리 메서드로 바꿔야 한다.
- 각 분류 부호 값마다 그에 해당하는 하위클래스를 작성하자. 그 하위클래스 안에 관련 값을 반환하는 분류 부호 읽기 메서드를 재정의하자.
- 반환 값은 return 같이 return 문으로 하드 코딩된다. 복잡해 보여도 모든 case 문을 수정할 때 까진 임시방편으로 취하는 불가피한 조치다.
- 상위 클래스의 분류 부호 필드를 삭제하자. 분류 부호 읽기 메서드와 쓰기 메서드를 abstract 타입으로 선언하자.
14.3 예제
public class Employee {
private int type;
static final int ENGINEER = 0;
static final int SALESMAN = 1;
static final int MANAGER = 2;
public Employee(int type) {
this.type = type;
}
}
- 우선 분류 부호에 필드 자체 캡슐화 기법을 적용하자.
public class Employee {
private int type;
static final int ENGINEER = 0;
static final int SALESMAN = 1;
static final int MANAGER = 2;
public Employee(int type) {
this.type = type;
}
int getType() {
return type;
}
}
- Employee 클래스의 생성자 메서드가 분류 부호를 매개변수로 받으니까, 그 생성자 메서드를 다음과 같이 팩토리 메서드로 바꿔야 한다.
public class Employee {
private int type;
static final int ENGINEER = 0;
static final int SALESMAN = 1;
static final int MANAGER = 2;
int getType() {
return type;
}
Employee create(int type) {
return new Employee(type);
}
private Employee (int type) {
this.type = type;
}
}
- 먼저 분류 부호 ENGINEER 변수를 Engineer 하위 클래스로 만들자. 다음과 같이 하위클래스를 작성하고 ENGINEER 분류 부호에 해당하는 재정의 메서드를 작성하자.
public class Engineer extends Employee{
int getType() {
return Employee.ENGINEER;
}
}
- 적절한 객체를 생성하게 팩토리 메서드도 다음과 같이 작성한다.
public class Employee {
private int type;
static final int ENGINEER = 0;
static final int SALESMAN = 1;
static final int MANAGER = 2;
int getType() {
return type;
}
static Employee create(int type) {
if(type == ENGINEER) {
return new Engineer(type);
}
return new Employee(type);
}
private Employee (int type) {
this.type = type;
}
}
- 이런 식으로 나머지 분류 부호도 바꾼다.
- (여기까지 작성했는데 Employee에서 생성자 오류가 나는데 일단 내맘대로 작성함)
- 그리고 Employee 클래스의 분류 부호 필드를 삭제하고 abstract 타입의 getType 메서드를 작성한다.
public abstract class Employee {
static final int ENGINEER = 0;
static final int SALESMAN = 1;
static final int MANAGER = 2;
abstract int getType();
static Employee create(int type) {
if(type == ENGINEER) {
return new Engineer(type);
} else if(type == SALESMAN) {
return new Salesman(type);
} else if(type == MANAGER) {
return new Manager(type);
} else {
throw new IllegalArgumentException("분류 부호 값이 잘못됨");
}
}
}
- (책에서는 switch를 했는데 나는 그냥 if로 함)
- 이거 이후에 메서드 하향과 필드 하향을 실시한다.
15. 분류 부호를 상태/전략 패턴으로 전환
- 분류 부호가 클래스의 기능에 영향을 주지만 하위클래스로 전환할 수 없을 땐 그 분류 부호를 상태 객체로 만들자.
15.1 동기
- 하위클래스로 전환과 비슷하지만, 분류 부호가 객체 수명주기 동안 변할 때나 다른 이유로 하위클래스로 만들 수 없을 때 사용한다.
- 이 기법은 상태 or 전략 패턴 중 하나를 사용한다.
- 조건문을 재정의로 전환으로 하나의 알고리즘을 단순화해야 할 때는 전략이 더 적절
- 상태별 데이터를 이동하고 객체를 변화하는 상태로 생각할 때는 상태 패턴이 더 적절
15.2 방법
- 분류 부호를 자체 캡슐화하자.
- 분류 부호의 목적을 나타내는 이름으로 새 클래스를 작성하자. 이것이 상태 객체이다.
- 그 상태 객체에 각 분류 부호에 해당하는 하위클래스를 추가하자
- 하위클래스 추가는 한 번에 하나씩보단 한꺼번에 추가하는 것이 간편하다.
- 상태 객체 안에 분류 코드를 반환하는 abstract 메서드 호출을 작성하자. 올바른 분류 부호를 반환하는 상태 객체 하위클래스 각각에 대한 재정의 메서드 호출을 작성하자.
- 원본 클래스 안에 새 상태 객체를 나타내는 필드를 선언하자.
- 원본 클래스에 있는 분류 부호 판단 코드를 상태 객체에 위임하게 수정하자.
- 원본 클래스의 분류 부호 쓰기 메서드를 적절한 상태 객체 하위클래스의 인스턴스를 할당하게 수정하자.
15.3 예제
- 아래 코드가 있다고 가정
public class Employee {
private int type;
private int monthlySalary;
private int commission;
private int bonus;
static final int ENGINEER = 0;
static final int SALESMAN = 1;
static final int MANAGER = 2;
public Employee(int type) {
this.type = type;
}
int payAmout() {
switch (type) {
case ENGINEER:
return monthlySalary;
case SALESMAN:
return monthlySalary + commission;
case MANAGER:
return monthlySalary + bonus;
default:
throw new RuntimeException("사원이 잘못됨");
}
}
}
- 분류 부호가 수시로 변하므로 하위 클래스로 만들 수 없다.
- 우선 분류 부호를 자체 캡슐화 한다.
public class Employee {
private int type;
private int monthlySalary;
private int commission;
private int bonus;
static final int ENGINEER = 0;
static final int SALESMAN = 1;
static final int MANAGER = 2;
public Employee(int type) {
setType(type);
}
int getType() {
return type;
}
void setType(int arg) {
type = arg;
}
int payAmout() {
switch (getType()) {
case ENGINEER:
return monthlySalary;
case SALESMAN:
return monthlySalary + commission;
case MANAGER:
return monthlySalary + bonus;
default:
throw new RuntimeException("사원이 잘못됨");
}
}
}
- 이제 상태 클래스를 선언하자.
- 상태 클래스를 abstract 타입으로 선언하고 그 안에 분류 부호를 반환하는 abstract 메서드를 다음과 같이 작성한다.
public abstract class EmployeeType {
abstract int getTypeCode();
}
- 분류 부호별 하위 클래스 작성
public class Engineer extends EmployeeType{
@Override
int getTypeCode() {
return Employee.ENGINEER;
}
}
- 분류 부호 읽기/쓰기 메서드를 수정해서 하위클래스를 Employee 클래스에 실제로 연결한다.
public class Employee {
private EmployeeType type;
private int monthlySalary;
private int commission;
private int bonus;
static final int ENGINEER = 0;
static final int SALESMAN = 1;
static final int MANAGER = 2;
public Employee(int type) {
setType(type);
}
int getType() {
return type.getTypeCode();
}
void setType(int arg) {
switch(arg) {
case ENGINEER:
type = new Engineer();
case SALESMAN:
type = new Salesman();
case MANAGER:
type = new Manager();
default:
throw new RuntimeException("사원 부호가 잘못됨");
}
}
int payAmout() {
switch (getType()) {
case ENGINEER:
return monthlySalary;
case SALESMAN:
return monthlySalary + commission;
case MANAGER:
return monthlySalary + bonus;
default:
throw new RuntimeException("사원이 잘못됨");
}
}
}
- 생성자를 팩토리 메서드로 전환 기법을 사용할 수 있다.
- 조건문을 재정의로 전환기법을 재빨리 사용할 수도 있다.
- 이제 우선 분류 부호 정의를 EmployeeType으로 복사하고, EmployeeType에 대한 팩토리 메서드를 작성한 후, Employee 클래스의 쓰기 메서드를 수정하자.
- 그리고 Employee 클래스에서 분류 부호 정의를 삭제하고 EmployeeType 클래스 참조를 넣는다.
public class Employee {
private EmployeeType type;
private int monthlySalary;
private int commission;
private int bonus;
static final int ENGINEER = 0;
static final int SALESMAN = 1;
static final int MANAGER = 2;
public Employee(int type) {
setType(type);
}
int getType() {
return type.getTypeCode();
}
void setType(int arg) {
type = EmployeeType.newType(arg);
}
int payAmout() {
switch (getType()) {
case ENGINEER:
return monthlySalary;
case SALESMAN:
return monthlySalary + commission;
case MANAGER:
return monthlySalary + bonus;
default:
throw new RuntimeException("사원이 잘못됨");
}
}
}
public abstract class EmployeeType {
static final int ENGINEER = 0;
static final int SALESMAN = 1;
static final int MANAGER = 2;
abstract int getTypeCode();
public static EmployeeType newType(int code) {
switch(code) {
case ENGINEER:
return new Engineer();
case SALESMAN:
return new Salesman();
case MANAGER:
return new Manager();
default:
throw new RuntimeException("사원 부호가 잘못됨");
}
}
}
- 이것으로 payAmount 메서드에 조건문을 재정의로 전환 기법을 적용할 수 있게 됐다.
16. 하위클래스를 필드로 전환
- 여러 하위클래스가 상수 데이터를 반환하는 메서드만 다를 땐 각 하위클래스의 메서드를 상위클래스 필드로 전환하고 하위클래스는 전부 삭제하자.
16.1 동기
- 기능을 추가하거나 기능을 조금씩 달리할 하위클래스를 작성하자.
- 다형적인 기능의 한 형태는 상수 메서드다.
- 상수 메서드는 하드코딩된 값을 반환하는 메서드다.
- 상수 메서드는 읽기 메서드에 각기 다른 값을 반환하는 하위클래스에 넣으면 유용하다.
- 상위클래스 안에 읽기 메서드를 정의하고 그 읽기 메서드를 하위클래스에서 다양한 값으로 구현하자.
- 상수 메서드가 유용하기 해도, 하위클래스를 상수 메서드로만 구성한다고 해서 그만큼 효용성이 커지는 것은 아니다.
- 상위클래스 안에 필드를 넣고 그런 하위클래스는 완전히 삭제하면 된다.
- 그렇게 하면 하위클래스의 불필요한 복잡함도 사라진다.
16.2 방법
- 하위클래스에 생성자를 팩토리 메서드로 전환을 적용하자.
- 하위클래스 참조 부분을 전부 상위클래스 참조로 수정하자.
- 상위클래스에 각 상수 메서드에 대한 final 타입의 필드를 선언하자.
- protected 타입의 상위클래스 생성자를 선언해서 필드를 초기화하자.
- 하위클래스 생성자를 추가하거나 수정해서 새 상위클래스 생성자를 호출하자.
- 상위클래스 안에 필드를 반환하는 각 상수 메서드를 구현하고 하위클래스의 메서드는 삭제하자.
- 하위클래스 메서드를 전부 삭제했으면 메서드 내용 직접 삽입 기법을 실시해서 생성자 메서드 내용을 상위클래스의 팩토리 메서드에 직접 넣자.
- 하위 클래스를 삭제하자.
- 생성자 메서드 내용을 다시 팩토리 메서드에 직접 넣고, 작업이 완료되면 각 하위클래스를 삭제하자.
16.3 예제
abstract class Person {
abstract boolean isMale();
abstract char getCode();
}
public class Male extends Person{
boolean isMale() {
return true;
}
@Override
char getCode() {
return 'M';
}
}
public class Female extends Person{
boolean isMale() {
return false;
}
@Override
char getCode() {
return 'F';
}
}
- Male 하위클래스와 Female 하위클래스는 하드코딩된 상수 메서드 반환만 다르다.
- 이렇게 기능이 충실하지 못한 하위클래스는 삭제하자.
- 우선 생성자를 팩토리 메서드로 전환을 실시해야 한다.
- 이 예제는 각 하위클래스마다 팩토리 메서드가 필요하다.
abstract class Person {
abstract boolean isMale();
abstract char getCode();
static Person createMale() {
return new Male();
}
static Person createFemale() {
return new Female();
}
}
- 다음과 같은 형태의 호출 코드를 찾아서 바꾸자.
Person kent = new Male(); // 바꾸기 전
Person Kent = Person.createFemale(); // 바꾼 후
- 호출 부분을 수정했으면 하위클래스 참조 코드를 전부 삭제해야 한다.
- 텍스트 검색 기능으로 하위클래스 참조 부분을 찾은 후 하위클래스들을 private 타입으로 바꿔서 적어도 패키지 외부에서 이 하위클래스들을 참조하는 부분이 없는지 확인하면 된다.
- 상위클래스에서 다음과 같이 각 상수 메서드별 필드를 선언하자.
- 상위클래스에 protected 타입의 생성자 메서드를 추가한다.
- 새로 작성한 생성자를 호출하는 생성자 메서드를 추가한다.
- 상위클래스에 읽기 메서드를 넣어 필드를 사용하게 만들고 하위클래스 메서드는 삭제하자.
class Person {
private final boolean isMale;
private final char code;
protected Person(boolean isMale, char code) {
this.isMale = isMale;
this.code = code;
}
boolean isMale() {
return isMale;
}
char getCode() {
return code;
}
static Person createMale() {
return new Male();
}
static Person createFemale() {
return new Female();
}
}
public class Male extends Person{
Male() {
super(true, 'M');
}
}
public class Female extends Person{
Female() {
super(false, 'F');
}
}
- 하위 클래스 내용을 모두 삭제했으면 메서드 내용 직접 삽입을 실시해서 하위클래스 생성자 내용을 상위 클래스 안에 넣는다.
class Person {
private final boolean isMale;
private final char code;
protected Person(boolean isMale, char code) {
this.isMale = isMale;
this.code = code;
}
boolean isMale() {
return isMale;
}
char getCode() {
return code;
}
static Person createMale() {
return new Person(true, 'M');
}
static Person createFemale() {
return new Person(false, 'F');
}
}
- Male, Female 클래스는 삭제한다.