필드 자체 캡슐화

  • 필드에 직접 접근하고 있는데 필드에 대한 결합이 이상해지면 get/set 메소드를 만들어 필드에 접근.
동기
  • 직접 접근 방식 : 절차적인 개발진행중 데이타 변경이 일어날 수 있는 소지가 있음.
  • 간접 접근 방식 : 객체지향 필드를 직접 접근하지 않으므로 정보 은닉 및 의도하지 않은 데이터 변경이 안된다.

필드 자체 캡슐화를 실시해야 할 가장 절실할 시점은 상위클래스 안의 필드에 접근하되 이 변수 접근을 하위클래스에서 계산된 값으로 재정의해야 할때다. 먼저 필드를 자체 캡슐화한 후 필요할때 읽기 메서드와 쓰기 메서드를 재정의하면 된다.

방법
  1. 필드 읽기메서드와 쓰기메서드를 작성하자.
  2. 그 필드 참조 부분을 전부 찾아서 읽기 메서드와 쓰기 메서드로 고치자
  3. 필드를 private로 만들자.
예제

package com.kws.eight;

public class IntRange {
	private int _low,_high;
	
	boolean includes(int arg){
		return arg >= _low && arg <= _high;
	}
	
	void grow(int factor){
		_high = _high * factor;
	}
	
	IntRange(int low, int high){
		_low = low;
		_high = high;
	}
}

package com.kws.eight;

public class IntRange {
	private int _low,_high;
	
	boolean includes(int arg){
		return arg >= get_low() && arg <= get_high();
	}
	
	void grow(int factor){
		this._high = get_high() * factor;
	}

	public int get_low() {
		return _low;
	}

	public void set_low(int _low) {
		this._low = _low;
	}

	public int get_high() {
		return _high;
	}

	public void set_high(int _high) {
		this._high = _high;
	}
	
}


자체 캡슐화를 실시할 때는 생성자 안에 쓰기 메서드를 사용할 때 주의해야 한다.대체로 객체가 생성된 후엔 속성을 변경하려고 쓰기 메서드를 사용하므로,쓰기 메서드에 초기화 시점과 다른 기능이 추가 됐을 수 있다고 전체할 때가 많다. 이럴땐 생성자나 별도의 초기화 메서드에서 직접 접근하게 하는 것이 좋다.

package com.kws.eight;

public class IntRange {
	private int _low,_high;
	
	IntRange(int low, int high){
		initialize(low, high);
	}
	
	private void initialize(int low, int high){
		this._low = low;
		this._high = high;
	}

	boolean includes(int arg){
		return arg >= get_low() && arg <= get_high();
	}
	
	void grow(int factor){
		this._high = get_high() * factor;
	}

	public int get_low() {
		return _low;
	}

	public void set_low(int _low) {
		this._low = _low;
	}

	public int get_high() {
		return _high;
	}

	public void set_high(int _high) {
		this._high = _high;
	}
	
}

생성자 초기화 메서드를 해두면 IntRange의 기능을 전부 재정의하면 기능을 하나도 수정하지 않고 cap을 계산에 넣을 수 있다.

package com.kws.eight;

public class CappedRange extends IntRange{
	private int _cap;
	
	CappedRange(int low, int high,int cap) {
		super(low, high);
		this._cap = cap;
	}

	public int get_cap() {
		return _cap;
	}

	public void set_cap(int _cap) {
		this._cap = _cap;
	}
	
}



데이터 값을 객체로 전환


데이터 항목에 데이터나 기능을 더 추가해야 할때는 데이터 항목을 객체로 전환하자.

동기

추가적인 데이터나 동작을 필요로 하는 데이터 아이템이 있을 때는 데이터 아이템을 객체로 바꾸어라.

방법
  1. 데이터값을 넣을 클래스를 작성하자.그 클래스 원본 클래스 안의 값과 같은 타입의 final필드를 추가하자 그 필드를 인자로 받는 생성자와 읽기 메서드를 추가하자.
  2. 원본 클래스에 든 필드의 타입을 새 클래스로 바꾸자.
  3. 원본 클래스 안의 읽기 메서드를 새 클래스의 읽기 메서드를 호출하게 수정
  4. 그 필드가 원본 클래스 생성자 안에 사용된다면 새 클래스의 생성자를 이용해서 필드를 대입하자.
  5. 새 클래스의 새 인스턴스를 생성하게끔 읽기 메서드를 수정하자.
public class OrderBefore {
	private String customer;
	
	
	public String getCustomer() {
		return customer;
	}

	public void setCustomer(String customer) {
		this.customer = customer;
	}

	private static int numberOrderFor(Collection orders, String customer){
		int result = 0;
		Iterator iter = orders.iterator();
		while(iter.hasNext()){
			OrderBefore each = (OrderBefore) iter.next();
			if(each.getCustomer().equals(customer)){
				result++;
			}
		}
		return result;
	}
	
}

public class Customer {
	private final String name;

	public Customer(String customer) {
		this.name = customer;
	}

	public String getCustomers() {
		return this.name;
	}
}
public class OrderAfter {
	
	private Customer _customer;
	
	public OrderAfter(String arg) {
		this._customer = new Customer(arg);
	}
	
	public String getCustomers(){
		return _customer.getCustomers();
	}
	
	public void setCustomer(String customer){
		_customer = new Customer(customer);
	}
	
	@SuppressWarnings("rawtypes")
	private static int numberOrderFor(Collection orders, String customer){
		
		int result = 0;
		
		Iterator iter = orders.iterator();

		while(iter.hasNext()){
			OrderAfter each = (OrderAfter) iter.next();
			if(each.getCustomers().equals(customer)){
				result++;
			}
		}
		
		return result;
	}
	
}



값을 참조로 전환


클래스에 같은 인스턴스가 많이 들어 있어서 이것들을 하나의 객체로 바꿔야 할땐 그 객체를 참조 객체로 전환하자.

동기

  • 참조객체는 고객이나 계좌 같은 것
  • 값 객체는 날짜나 돈 같은 것(값 객체는 전적으로 데이터 값을 통해서만 정의된다.

방법

  1. 생성자를 팩토리 메서도로 전환을 실시하자.
  2. 참조 객체로 접근을 담당할 객체를 정하자.
  3. 객체를 미리 생성할지 사용하기 직전에 생성할지를 정하자.
  4. 참조 객체를 반환하게 팩토리 메서드를 수정하자.

예제

public class Customer {
	
	private final String name;
	
	public Customer(String name){
		this.name = name;
	}

	public String getName() {
		return name;
	}
	
}

public class Order {
	
	private Customer customer;
	
	public Order(String customerName){
		this.customer = new Customer(customerName);
	}

	public String getCustomer() {
		return customer.getName();
	}

	public void setCustomer(String customerName) {
		this.customer = new Customer(customerName);
	}
	
	@SuppressWarnings({ "unused", "rawtypes" })
	private static int numberOrderFor(Collection orders, String customer){	
		int result = 0;		
		Iterator iter = orders.iterator();
		while(iter.hasNext()){
			Order each = (Order) iter.next();
			if(each.getCustomer().equals(customer)){
				result++;
			}
		}
		return result;
	}
	
}
  • Customer는 값 객체다. 각 Order인스턴스에는 고유한 Customer객체가 들어 있다. 비록 모든 Customer객체는 개념상 동일한 고객을 나타내는 객체긴 하지만 말이다. 개념상 동일한 고객에 주문이 여러개 있을 경우 하나의 Customer객체만 사용하게끔 이것을 수정해야 한다. 즉 고객 이름 하나당 한개의 Customer객체만 있어야 한다.
  • 생성자를 팩토리 메서드로 전환을 적용하자.
  • Customer객체에 다음과 같이 팩토리 메서드를 적용하자.
	public static Customer create(String name){
		return new Customer(name);
	}

그 다음, 생성자 호출을 팩토리 메서드 호출로 수정하자.

	public Order(String customerName){
		this.customer = Customer.create(customerName);
	}

그 생성자 메서드를 private로 만들자

	private Customer(String name){
		this.name = name;
	}

Customer에 한 개의 필드를 사용해서 라인 항목을 저장해 Customer클래스를 접근거점으로 만들겠다.

private static Dictionary _instances = new Hashtable();

그 다음 Customer인스턴스들을 요청이 있을 때 즉석에서 생성할지 아니면 미리 생성해 둘지 결졍해야 한다. 미리 생성해 두는 방식으로 하며, 시작 코드 안에 사용되는 Customer인스턴스들을 로딩하는 코드를 넣겠다.

	static void loadCustomers(){
		new Customer("우리 렌터카").store();
		new Customer("커피 자판기").store();
		new Customer("삼천리 가스").store();
	}
	
	@SuppressWarnings("unchecked")
	private void store(){
		_instances.put(this.getName(), this);
	}

이제 팩토리 메서드를 수정해서 미리 생성해둔 Customer인스턴스를 반환하게 하자.

	public static Customer create(String name){
//		return new Customer(name);
//		미리 load된 객체반환
		return ((Customer) _instances.get(name));
	}

create 메서드는 반드시 미리 생성되어 있던 Customer인스턴스를 반환하므로 메서드명 변경을 실시해서 확인해야 한다.

	public static Customer getNamed(String name){
		return (Customer) _instances.get(name);
	}

참조를 값으로 전환


참조 객체가 작고 수정할 수 없고 관리하기 힘들 땐 그 참조 객체를 값 객체로 만들자.

동기
  • 작고,불변성이고 관리하기가 어려운 참조 객체가 있는 경우, 그것을 값 객체로 바꿔라.
  • Reference의 필요 없이 변하지 않는 데이터의 값을 가지는 object를 필요로 하는 경우 Reference의 Object를 Value object로 대체함.
  • 참조 객체로 작업하는 것이 복잡해지면 참조에서 값으로 바꿀 이유가 될 수 있다.
방법
  1. 전환할 객체가 변경불가인지 변경 가능인지 검사하자.
  2. equals 메서드와 hash메서드를 작성하자.
  3. 컴파일과 테스트를 실시하자.
  4. 팩토리 메서드를 삭제하고 생성자를 public으로 만들어야 좋은지 생각해보자.
예제
public class Currency {
	private String code;

	public String getCode() {
		return code;
	}

	private Currency(String code){
		this.code = code;
	}
	
	public static void main(String[] args) {
		System.out.println(new Currency("USD").equals(new Currency("USD"))); // false;
	}
}

Currency 클래스에는 여러 인스턴스가 들어 있다. 생성자만 사용하는 것은 불가능하다.그래서 private인 것이다. 이것을 값 객체로 변환하려면 그 객체가 변경불가인지 확인해야 한다. 변경불가가 아니면 값이 변경 가능할 경우 끝없는 별칭 문제가 발생하므로 이렇게 수정하지 말어야 한다. 여기서 참조 객체는 변경 불가이므로 이제 다음과 같이 equals메서드를 정의해야 한다.

	public boolean equals(Object arg){
		if(!(arg instanceof Currency)) return false;
		Currency other = (Currency)arg;
		return (code.equals(other.code));
	}

equals 메서드를 정의할 때 다음과 같이 hashCode 메서드도 정의해야 한다.

	public int hashCode(){
		return this.code.hashCode();
	}

배열을 객체로 전환


배열을 구성하는 특정 원소가 별의별 의미를 지닐땐 그 배열을 각 원소마다 필드가 하나씩 든 객체로 전환하자.

동기
  • Array는 유사한 데이터의 집합을 담고 있어야 한다.
  • 데이터의 관리가 용이해야 한다.
방법
  1. 배열 안의 정보를 표현할 새 클래스를 작성하자. 그 클래스 안에 배열을 저장할 public 필드를 하나 작성하자.
  2. 배열 참조 부분을 전부 새 클래스 참조로 수정하자.
  3. 배열의 각 원소마다 참조 코드에 사용할 읽기 메서드를 하나씩 넣자. 배열 원소의 용도를 따서 읽기 메서드 이름을 정하자. 참조 부분을 읽기 메서드 호출로 전부 수정하자. 하나의 수정을 마칠때마다 테스트를 실시하자.
  4. 배열 참조 부분을 전부 메서드로 교채했으면 배열을 private로 만들자.
  5. 컴파일하자.
  6. 클래스 안에 배열의 각 원소에 대응되는 하나의 필드를 생성한 후, 그 필드를 사용하게끔 읽기/쓰기 메서드를 수정하자.
  7. 각 원소에 대한 수정을 마칠 때마다 컴파일과 테스트를 실시하자.
  8. 모든 원소를 필드로 교체했으면 배열을 삭제하자.
예제
String[] row = new String[3];
row[0] = "Liverpool";
row[1] = "15";

String name = row[0];
int wins = Integer.parseInt(row[1]);

Performance row = new Performance();
row.setName("Liverpool");
row.setWins("15");

Performance Class

public class Performance {
	
	private String name;
	private String wings;

	public String getName() {
		return name;
	}

	public void setName(String arg) {
		this.name = arg;
	}
	
	public int getWins(){
		return Integer.parseInt(wings);
	}
	
	public void setWins(String arg){
		this.wings = arg;
	}
}

관측 데이터 복제


도메인 데이터는 GUI컨트롤 안에서만 사용 가능한데,도메인 메서드가 그 데이터에 접근해야 할 땐 그 데이터를 도메인 객체로 복사하고,양측의 데이터를 동기화하는 관측 인터페이스 observer를 작성하자.

동기

계층구조가 체계적인 시스템은 비즈니스 로직 처리 코드와 사용자 인터페이스 처리 코드가 분리되어 있다.아래와 같은 몇가지 이유로…

  • 비슷한 비즈니스 로직을 여러 인터페이스가 처리해야 하는 경우라서
  • 비즈니스 로직까지 처리하려면 사용자 인터페이스가 너무 복잡해지니까
  • GUI와 분리된 도메인 객체가 더욱 유지보수하기 쉬우니까
  • 두 부분을 서로 다른 개발자가 다루게 할 수 있으니까
방법
  1. 표현 클래스를 도메인 클래스의 관측 인터페이스로 만들자.
  2. GUI클래스 안의 도메인 데이터를 대상으로 필드 자체 캡슐화를 실시하자.
  3. 이벤트 핸들러 메서드 안에 쓰기 메서드 호출 코드를 추가하자. 이 쓰기 메서드는 직접 접근 방식으로 컴포넌트를 현재 값으로 수정한다.
  4. 도메인 클래스 안에 데이터와 읽기/쓰기 메서드를 정의하자.
  5. 쓰기 메서드가 도메인 필드에 쓰도록 참조를 수정하자.
  6. 관측 인터페이스의 update메서드를 도메인 필드에서 GUI컨트롤로 데이터를 복사하게 수정하자.
예제
public class IntervalWindow extends Frame{
	TextField _startField;
	TextField _endField;
	TextField _lengthField;
	
	class SymFocus extends FocusAdapter{
		public void focusLost(FocusEvent event){
			Object object = event.getSource();
			
			if(object == _startField){
				StartField_FocusLost(event);
			}else if(object == _endField){
				EndField_FocusLost(event);
			}else if(object == _lengthField){
				LengthField_FocusLost(event);
			}
		}
	}

	private boolean isNotInteger(String text) {
		boolean result = false;
		try{
			Integer.parseInt(text);
			result = true;
		}catch(Exception e){
			
		}
		return result;
	}
	
	void StartField_FocusLost(FocusEvent event){
		if(isNotInteger(_startField.getText())){
			_startField.setText("0");
		}
		calculateLength();
	}
	
	void EndField_FocusLost(FocusEvent event){
		if(isNotInteger(_endField.getText())){
			_endField.setText("0");
		}
		calculateLength();
	}
	
	void LengthField_FocusLost(FocusEvent event){
		if(isNotInteger(_lengthField.getText())){
			_lengthField.setText("0");
		}
		calculateEnd();
	}

	void calculateLength(){
		try{
			int start = Integer.parseInt(_startField.getText());
			int end = Integer.parseInt(_endField.getText());
			int length = end - start;
			_lengthField.setText(String.valueOf(length));
		}catch(NumberFormatException e){
			throw new RuntimeException("잘못된 숫자 형식 에러");
		}
	}
	
	void calculateEnd(){
		try{
			int start = Integer.parseInt(_startField.getText());
			int length = Integer.parseInt(_endField.getText());
			int end = start + length;
			_lengthField.setText(String.valueOf(end));
		}catch(NumberFormatException e){
			throw new RuntimeException("잘못된 숫자 형식 에러");
		}
	}

}

여기서 할 일은 GUI코드와 로직코드를 분리하는 것이다. calculateLength 메서드와 calculateEnd 메서드를 별도의 도메인 클래스로 옮겨야 한다. 그러려면 start,end,length 변수를 IntervalWindow 클래스를 거치지 않고 참조해야 한다.그러기 위한 유일한 방법은 start,end,length 변수 데이터를 도메인 클래스로 복사하고 그 데이터를 GUI클래스의 데이터와 동기화하는 것이다.이 작업을 소위 관측 데이터 복제라고 한다.

  • 아직 도메인 클래스가 없으니 빈 도메인 클래스를 다음과 같이 작성하자.
public class Interval extends Observable{}
  • 이렇게 작성한 도메인 클래스를 참조하는 필드를 IntervalWindow클래스에 다음과 같이 넣어야 한다.
class IntervalWindow....{
    private Interval _subject;
}
  • 그런 다음 이 필드를 적절히 초기화하고 IntervalWindow클래스를 Interval 도메인 클래스의 관측 인터페이스로 만들어야 한다. 그럴려면 IntervalWindow클래스의 생성자 메서드 안에 다음 코드를 넣어면 된다.
	public IntervalWindowChange(){
		_subject = new Interval();
		_subject.addObserver(this);
		update(_subject, null);
	}
  • 그 다음 IntervalWindow클래스가 Observer클래스를 상속구현해야 한다.
public class IntervalWindowChange extends Frame implements Observer{}
  • Observer클래스를 상속구현하려면 update메서드를 오버라이드 해야한다.
	@Override
	public void update(Observable o, Object arg) {

	}
  • 이제 필드를 옮길 차례다. 필드 자체캡슐화를 실시해서 모든 텍스트 필드는 getText메서드와 setText메서드를 통해 업데이트 한다. 읽기메서드와 쓰기 메서드를 다음과 같이 작성하자.
	public String getEnd(){
		return _endField.getText();
	}
	
	public void setEnd(String arg){
		_endField.setText(arg);
	}
  • 그런 다음 _endField필드 참조 부분을 전부 찾아서 읽기메서드나 쓰기메서드로 교체하자.
	void calculateLength(){
		try{
			int start = Integer.parseInt(_startField.getText());
			int end = Integer.parseInt(getEnd());
			int length = end - start;
			_lengthField.setText(String.valueOf(length));
		}catch(NumberFormatException e){
			throw new RuntimeException("잘못된 숫자 형식 에러");
		}
	}
	
	void calculateEnd(){
		try{
			int start = Integer.parseInt(_startField.getText());
			int length = Integer.parseInt(_lengthField.getText());
			int end = start + length;
			setEnd(String.valueOf(end));
		}catch(NumberFormatException e){
			throw new RuntimeException("잘못된 숫자 형식 에러");
		}
	}

	void EndField_FocusLost(FocusEvent event){
                setEnd(_endField.getText());
		if(isNotInteger(getEnd())){
			setEnd("0");
		}
		calculateLength();
	}
  • setEnd메서드를 호출할 때 매개변수로 getEnd메서드를 사용하지 않고 end필드를 직접 사용했다.그 이유는 리팩토링 후반부에 가서 getEnd메서드는 값을 end필드에서 가져오는게 아니라 도메인 객체를 통해 가져오기 때문이다.그때 도메인 객체로부터 값을 가져오면 사용자가 필드 값을 변경할 때마다 이 코드는 endField값을 변경 전의 값으로 되돌리게 되므로,end필드를 직접 읽어야 하는것이다.
  • 다음 Interval 도메인 클래스에 end필드와 읽기메서드와 쓰기메서드를 작성하자.
private String end = "0";
	void setEnd(String arg){
		this.end = arg;
		setChanged();
		notifyObservers();
	}
	
	String getStart(){
		return start;
	}
  • Interval클래스에 위와 같이 작성했으면 IntervalWindow클래스의 읽기/쓰기 메서드를 수정해서 Interval을 거치게 만들자.
	public String getEnd(){
		return _subject.getEnd();
	}
	
	public void setEnd(String arg){
		_subject.setEnd(arg);
	}
  • GUI클래스가 그 변경사항 통지에 확실히 반응하게 하려면 다음과 같이 update메서드를 수정해야 한다.
	@Override
	public void update(Observable o, Object arg) {
		_endField.setText(_subject.getEnd());
	}
  • 앞의 코드에서 역시 endField로의 직접 접근 방식을 사용했다. 이렇게 하지 않고 만약 쓰기 메서드를 호출하면 무한루프에 빠진다.
  • 지금껏 end필드에 수행한 과정을 start,length필드에도 그대로 적용하자. 메서드 이동 기법으로 calculaterEnd메서드와 calculaterLength메서드를 Interval클래스로 옮기자.그러면 모든 도메인 기능과 데이터는 도메인 클래스에 들어 있게 되어 GUI코드로 분리된다.

클래스의 단방향 연결을 양방향으로 전환


두 클래스가 서로의 기능을 사용해야 하는데 한 방향으로만 연결되어 있을땐 역 포인터를 추가하고 두 클래스를 모두 업데이트할 수 있게 접근 한정자를 수정하자.

동기
  • 각각 서로의 기능을 필요로 하는 클래스가 있는데 링크가 한쪽 방향으로만 되어 있는 경우, 반대 방향으로 포인터 추가하고, 수정자가 양쪽 세트를 모두 업데이트 하게 변경하라
방법
  1. 역 포인터 참조용 필드를 추가하자.
  2. 연결 제어 기능을 어느 클래스에 넣을지 정하자.
  3. 연결 제어 기능이 없는 클래스 안에 헬퍼 메서드를 작성하고, 그 메서드에 제한된 용도가 분명히 드러나는 이름을 붙이자.
  4. 기존 변경 메서드가 연결 제어 클래스에 들어 있으면 역 포인터를 업데이트하게 변경 메서드를 수정하자.
  5. 기존 변경 메서드가 연결 제어 클래스에 들어 있으면 제어 클래스 안에 제어 메서드를 작성하고 기존 변경 메서드가 그 메서드를 호출하게 하자.
예제
class Order...
    Customer getCustomer(){
        return customer;
    }    

    void setCustomer(Customer arg){
        customer = arg;
    }

class Customer{
   private Set orders = new HashSet(); 
...
}

클래스의 양방향 연결을 단방향으로 전환


두 클래스가 양방향으로 연결되어 있는 한 클래스가 다른 클래스의 기능을 더 이상 사용하지 않게 됐을 땐 불필요한 방향의 연결을 끊자.

동기

양방향 연결로 인해 두 클래스는 서로 종속된다, 한 클래스를 수정하면 다른 클래스도 변경된다.종속성이 많으면 시스템의 결합력이 사소한 수정에도 예기치 못한 각종 문제가 발생한다.

방법
  1. 삭제하려는 포인터가 저장된 필드를 읽는 모든 부분을 검사해서 삭제해도 되는지 파악
  2. 참조 코드가 속성 읽기 메서드를 사용해여 한다면 속성 읽기 메서드에 필드 자체 캡슐화를 적용하고 알고리즘 전환
  3. 참조 코드에 읽기 메서드 호출을 넣을 필요가 없다면 각 필드 사용 부분을 찾아서 필드 안의 객체를 다른 방법으로 가져오게끔 수정
  4. 필드 안의 속성 읽기 메서드를 모두 삭제했으면 필드 업데이트 코드 전부와 필드를 삭제하자.
예제
public class Order {
	private Customer customers;

	public Customer getCustomer() {
		return customers;
	}

	public void setCustomer(Customer arg) {
		// this.customer = customer;
		if (customers != null)
			customers.friendOrders().remove(this);
		customers = arg;
		if (customers != null)
			customers.friendOrders().add(this);
	}
}

public class Customer {
	private static Set orders = new HashSet();
	
	void addOrder(Order arg){
		arg.setCustomer(this);
	}
	
	Set friendOrders(){
		return orders;
	}
}
  • 예제 프로그램을 보니 customer가 먼저 있어야만 order가 있음을 알수 있다. 따라서 Order클래스에서 Customer클래스로 가는 연결을 끊어야 한다.
  • 우선 모든 필드 읽기 메서드와 그 읽기 메서드를 사용하는 메서드들을 파악해야 한다.Customer객체를 제공할 다른 방법이 있을까? 한 명령을 Customer객체에 하나의 매개변수로 전달하는 방법을 사용할 때가 많다.
Order...
	double getDiscountedPrice(Customer customer) {
		return getGrossPrice() * (1 - customer.getDiscount());
	}
Customer...
	double getPriceFor(Order order){
		return order.getDiscountedPrice(this);
	}

마법숫자를 기호 상수로 전환


특수 의미를 지닌 리터럴 숫자가 있을 땐 의미를 살린 이름의 상수를 작성한 후 리터럴 숫자를 그 상수로 교체하자.

방법
  1. 상수를 선언하고 그상수에 마법 숫자의 값을 할당.
  2. 마법 숫자 사용되는 부분 모두 찾아내고 마법 숫자가 상수의 용도와 맞는지 확인하고 그렇다면 마법 숫자를 상수로 교체하자
예제
	double potentiaEnergy(double mass, double height){
		return mass * 9.81 * height;
	}
	
	static final double GRAVITATION_CONSTANT = 9.81;
	
	double potentiaEnergys(double mass, double height){
		return mass * GRAVITATION_CONSTANT * height;
	}

필드 캡슐화


동기
  • 객체지향의 주요 원칙 중 하나는 캡슐화다. 캡슐화는 데이터 은닉이라고 부르기도 한다. 그래서 데이터는 절대로 public타입으로 선언하면 안된다. 데이터를 public타입으로 만들면 데이터가 있는 객체가 모르는 사이에 다른 객체가 데이터 값을 읽고 변경할 수 있다. 이로 인해 데이터와 기능을 분리한다.
방법
  1. 캡슐화할 필드를 읽고 쓰기 위한 메서드와 쓰기 메서드를 작성하자.
  2. 클래스 외부에서 그 필드를 참조하는 모든 부분을 찾자.찾아낸 참조 부분이 값을 읽는 코드라면 그 참조 부분을 읽기 메서드 호출로 수정하고,값을 변경하는 코드라면 쓰기 메서드 호출로 수정하자.
  3. 필드를 참조하는 부분을 전부 수정했으면 그 필드의 선언 타입을 private로 수정하자.
예제
public String name;

--------------------------------------

private String name;

public String getName(){return name;}
public void setName(String arg){name = arg;}


컬렉션 캡슐화


메서드가 컬렉션을 반환할때 그 메서드가 읽기전용 뷰를 반환하게 수정하고 추가메서드와 삭제메서드를 작성하자.

동기

컬렉션은 다른 종류의 데이터와는 약간 다른 읽기/쓰기 방식을 사용해야 한다. 읽기 메서드는 컬렉션 객체 자체를 반환해선 안된다. 왜냐하면 컬렉션 참조 부분이 컬렉션의 내용을 조작해도 그 컬렉션이 든 클래스는 무슨 일이 일어나는지 모르기 때문이다.이로 인해 컬렉션 참조 코드에서 그 객체의 데이터 구조가 지나치게 노출된다.값이 여러 개인 속성을 읽는 읽기 메서드는 컬렉션 조작이 불가능한 형식을 반환하고 불필요하게 자세한 컬렉션 구조 정보는 감춰야 한다. 컬렉션 쓰기 메서드는 절대 있으면 안되므로, 원소를 추가하는 메서드와 삭제하는 메서드를 대신 사용해야 한다.

방법
  1. 컬렉션 원소를 추가하는 add메서드와 remove메서드를 추가하자.
  2. 필드를 빈 컬렉션으로 초기화하자.
  3. 쓰기 메서드 호출 부분을 찾아서 add메서드와 remove메서드 호출로 바꾸던지, 그 위치에 직접 컬렉션에 원소를 추가하고 삭제하는 코드를 작성하자.
  4. set이라는 이름을 initalize와 replace로 수정하자.
  5. 읽기 메서드를 호출하여 컬렉션을 변경하는 부분을 전부 찾아서,add메서드 호출과 remove메서드호출로 바꾸자. 6.컬렉션을 변경하고자 읽기 메서드를 호출하는 부분을 전부 추가/삭제 메서드 호출로 고쳤으면,컬렉션의 읽기 전용 뷰를 반환하게 읽기 메서드를 수정하자.
  6. 읽기 메서드 호출 부분을 찾고 컬렉션이 든 객체로 옮겨야 할 코드를 찾아서 메서드추출과 메서드이동을 실시해서 컬렉션이 든 객체로 옮기자.
  7. 기존 읽기 메서드에 메서드명 변경을 적용하고,Enumeration타입을 반환하는 새 메서드를 작성하고,기존 메서드 호출 부분을 새 메서드 호출로 바꾸지.
예제
package collection;

public class Course {
	
	private String name;
	private boolean isA;
	
	public Course(String name, boolean isAdvanced){
		this.name = name;
		this.isA = isAdvanced;
	}
	
	public boolean isAdvanced(){
		return true;
	}
}

package collection;

import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import junit.framework.Assert;

public class Person {
	
//	private Set course;
	@SuppressWarnings("rawtypes")
	private Set course = new HashSet();

	public Set getCourse() {
		return course;
	}
	
//	@SuppressWarnings("unchecked")
//	public Set getCourse() {
//		return Collections.unmodifiableSet(course);
//		unmodifiableSet 메소드는 지정된 세트의 변경 불가능한 뷰를 돌려줍니다.
//	}

//	초기화하려고 원소를 추가할때 추가 기능이 확실히 필요 없다면 다음과 같이 루프를 삭제하고 addAll메서드를 사용.
//	public void setCourse(Set course) {
//		this.course = course;
//	}
	
	@SuppressWarnings("rawtypes")
	public void initalizeCourse(Set arg) {
//		Assert.isTrue(course.isEmpty());
		Iterator iter = arg.iterator();
		while(iter.hasNext()){
			addCourse((Course) iter.next());
		}
	}
	
//	public void initalizeCourse(Set arg) {
//		Assert.isTrue(course.isEmpty());
//		course.addAll(arg);
//	}
	
	@SuppressWarnings("unchecked")
	public void addCourse(Course arg){
		course.add(arg);
	}
	
	public void removeCourse(Course arg){
		course.remove(arg);
	}
	
	
	@SuppressWarnings("rawtypes")
	int numberOfAdvancedCourse(Person person){
		Iterator iter = person.getCourse().iterator();
		
		int count = 0;
		
		while(iter.hasNext()){
			Course each = (Course) iter.next();
			if(each.isAdvanced()){
				count++;
			}
		}
		
		return count;
	}
	
	int numberOfCourse(){
		return course.size();
	}
	
}

package collection;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import org.junit.Assert;


public class Client {
	
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public static void main(String[] args) {
		
		Person kent = new Person();
		
		Set s = new HashSet();
		
//		before
//		s.add(new Course("스몰토크 프로그래밍", false));
//		s.add(new Course("싱글몰드 위스키 음미하기", true));
		
//		after
		kent.addCourse(new Course("스몰토크 프로그래밍", false));
		kent.addCourse(new Course("싱글몰드 위스키 음미하기", true));
		
//		kent.setCourse(s);
		kent.initalizeCourse(s);
		
//		Assert.assertEquals(2, kent.getCourse().size());
		Course refact = new Course("리팩토링", true);
		
		kent.getCourse().add(refact);
		
//		before
//		kent.getCourse().add(new Course("지독한 빈정거림", false));
//		after
		kent.addCourse(new Course("지독한 빈정거림", false));
		
//		Assert.assertEquals(4, kent.getCourse().size());
		
		kent.getCourse().remove(refact);
		
//		Assert.assertEquals(3, kent.getCourse().size());
		
//		System.out.println(kent.getCourse().size());
		System.out.println(kent.getCourse());
		//고급과정
		Iterator iter = kent.getCourse().iterator();
		
		int count = 0;
		
		while(iter.hasNext()){
			Course each = (Course) iter.next();
			if(each.isAdvanced()){
				count++;
			}
		}
		
		System.out.println(count);
	}
	
}

package collection;

import java.util.Enumeration;
import java.util.Vector;

public class PersonVecter {
	
	private Vector course;

	public Vector getCourse() {
		return course;
	}

	public void setCourse(Vector course) {
		this.course = course;
	}
	
	@SuppressWarnings("unchecked")
	public void addCourse(Course arg){
		course.addElement(arg);
	}
	
	public void removeCourse(Course arg){
		course.removeElement(arg);
	}
	
	@SuppressWarnings("rawtypes")
	public void initalizeCourse(Vector arg) {
//		Assert.isTrue(course.isEmpty());
		Enumeration iter = arg.elements();
		while(iter.hasMoreElements()){
			addCourse((Course) iter.nextElement());
		}
	}
	
}

package collection;

public class ArrayTest {
	
	private String[] skills;

//	public String[] getSkills() {
//		return skills;
//	}
	public String[] getSkills() {
		String[] result = new String[skills.length];
		System.arraycopy(skills, 0, result, 0, skills.length);
		return result;
	}
	
//	public void setSkills(String[] arg) {
//		this.skills = skills;
//	}
	
	public void setSkill(int index, String skill) {
		skills = new String[5];
		skills[index] = skill;
	}
	
	public void setSkills(String[] arg) {
		skills = new String[skills.length];
		for(int i = 0; i < skills.length; i++){
			setSkill(i,arg[i]);
		}
	}
	
}


레코드를 데이터 클래스로 전환


전통적인 프로그래밍 환경에서의 레코드 구조에 대한 인터페이스가 필요한 경우, 그 레코드를 위한 데이터 객체를 만들어라.

방법
  1. 레코드를 표현할 클래스를 작성하자.
  2. 그 클래스에 각 데이터 항목에 대한 읽기 메서드와 쓰기 메서드를 작성하고 private필드를 선언하자.

분류 부호를 클래스로 전환


기능에 영향을 숫자형 분류 부호가 든 클래스가 있을 땐 그 숫자를 새 클래스로 바꾸자.

동기
  • 숫자형 분류 부호를 클래스로 빼내면 컴파일러는 그 클래스 안에서 종류 판단을 수핼할 수 있다. 그 클래스 안에 팩토리 메서드를 작성하면 유효한 인스턴스만 생성되는지와 그런 인스턴스가 적절한 객체로 전달되는지를 정적으로 검사할 수 있다.
  • 분류 부호를 클래스로 만드는 건 분류 부호가 순수한 데이터일 때만 실시해야 한다.
방법
  1. 분류 부호의 종류를 판단랑 새 클래스를 작성하자.
  2. 새 클래스를 사용하게 원본 클래스의 내용을 수정하자.
  3. 원본 클래스 코드를 사용하는 원본 클래스 안의 각 메서드마다, 그에 대응하는 새 캘르스를 사용하는 새 메서드를 사용하자.
  4. 원본 클래스 호출 부분을 한 번에 하나씩 새 인터페이스 호출로 수정하자.
예제

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 setBloodGroup(int arg){
        this.bloodGroup = arg;
    }

    public int getBloodGroup(){
        return bloodGroup
    }
}

package replace;

public class Person {
	
	private BloodGroup bloodgroup;
	
	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 int bloodGroup;
	
	public Person(BloodGroup arg){
//		this.bloodGroup = arg;
		this.bloodgroup = arg;
	}

	public BloodGroup getBloodGroup() {
//		return bloodGroup;
		return bloodgroup;
	}

	public void setBloodGroup(BloodGroup arg) {
		this.bloodgroup = arg;
	}
	
	public static void main(String[] args) {
		
//		Person thePerson = new Person(Person.A);
		Person thePerson = new Person(BloodGroup.A);
		
		thePerson.getBloodGroup().getCode();
		
//		thePerson.setBloodGroup(Person.AB);
		thePerson.setBloodGroup(BloodGroup.AB);
	}
}

package replace;

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 static final BloodGroup[] values = {O ,A ,B ,AB};
	
	private final int code;
	
	private BloodGroup(int arg){
		this.code = arg;
	}

	public int getCode() {
		return code;
	}

	public static BloodGroup code(int arg){
		return values[arg];
	}
	
}


분류 부호를 하위클래스로 전환


클래스 기능에 영향을 주는 변경불가 분류 부호가 있을 땐 분류 부호를 하위클래스로 만들자

동기
  • 분류 부호의 값이 객체 생성 후 변할때
  • 다른 이유로 분류 부호를 이미 하위클래스로 만들었을때
  • 분류 부호를 상태/전략 패턴으로 전환을 실시해야 할때

분류 부호를 하위클래스로 전환 기법은 주로 조건문을 재정의로 전환을 가능하게 하는 사전 작업으로 시행할 때가 많다. 분류 부호를 하위클래스로 전환기법은 조건문이 있으면 실시한다.

특정 분류 부호의 객체에만 관련된 기능이 있을 때도 분류 부호를 하위클래스로 전환 기법을 적용해야 한다.

방법
  1. 분류 부호를 캡슐화 하자.
  2. 각 분류 부호 값마다 그에 해당하는 하위클래스를 작성하자. 그 하위클래스 안에 관련 값을 반환하는 분류 부호 읽기 메서드를 정의하자.
  3. 상위클래스의 분류 부호 필드를 삭제하자. 분류 부호 읽기 메서드와 쓰기 메서드를 abstract타입으로 선언하자.
예제
package subclass;

abstract class Employee {
	private int type;

	static final int ENGINEER = 0;
	static final int SALESMAN = 1;
	static final int MANAGER = 2;

	static Employee create(int arg) {
		switch (arg) {
		case ENGINEER:
			return new Engineer(0);
		case SALESMAN:
//			return new Salesman();
		case MANAGER:
//			return new Manage();
		default:
			throw new IllegalArgumentException("Incorrect");
		}
	}

	public Employee(int arg) {
		this.type = arg;
	}

	// public int getType() {
	// return type;
	// }

	abstract int getType();

	public void setType(int type) {
		this.type = type;
	}

}

package subclass;

public class Engineer extends Employee{

	public Engineer(int arg) {
		super(arg);
	}

	public int getType(){
		return Employee.ENGINEER;
	}
}

분류 부호를 상태/전략 패턴으로 전환


분류 부호가 클래스의 기능에 영향을 주지만 하위클래스로 전환할 수 없을 땐 그 분류 부호를 상태 객체로 만들자.

동기
  • 클래스의 동작에 영향을 미치는 타입 코드가 있지만 서브클래싱을 할 수 없을 때는 타입 코드를 스테이트(State) 객체로 바꿔라.
방법
  1. 분류 부호를 캡슐화하자.
  2. 분류 부호의 목적을 나타내는 이름으로 새 클래스를 작성하자. 이것이 상태객체다.
  3. 그 상태 객체에 각 분류 부호에 해당하는 하위클래스로 추가하자.
  4. 상태 객체 안에 분류 코드를 반환하는 abstract 메서드 호출로 작성하자. 올바른 분류 부호를 반환하는 상태 객체 하위클래스 각각에 대한 재정의 메서드 호출을 작성하자.
  5. 원본 클래스 안에 새 상태 객체를 나타내는 필드에 선언하자.
  6. 원본 클래스에 있는 분류 부호 판단 코드를 상태 객체에 위임하게 수정하자.
  7. 원 본 클래스의 분류 부호 쓰기 메서드를 적절한 상태 객체 하위클래스의 인스턴스를 할당하게 수정하자.
예제
public class Employee {
    private EmployeeType type;

    static final int ENGINEER = 0;
    static final int SALESMAN = 1;
    static final int MANAGER = 2;
    
    private int monthlySalary = 0;
    private int commission = 0;
    private int bonus = 0;

    public Employee(EmployeeType arg) {
        this.type = arg;
    }

    public void setType(int arg) {
          this.type = type;
    }
}
  • 이제 상태클래스를 선언하자.상태 클래스는 abstract타입으로 선언하고 그 안에 분류부호를 반환하는 abstract메서드를 선언하자.
abstract class EmployeeType {
    abstract int getTypeCode();
}
  • 그리고 다음과 같이 분류 부호별 하위클래스를 작성하자.
public class Engineer extends EmployeeType{
	int getTypeCode() {
		return Employee.ENGINEER;
	}
}

public class Manager extends EmployeeType{
	int getTypeCode() {
		return Employee.MANAGER;
	}
}

	int getTypeCode() {
		return Employee.SALESMAN;
	}
}
  • 이제 분류 부호 읽기/쓰기 메서드를 수정해서 하위클래스 Employee클래스를 연결하자.
    private EmployeeType type;
    
    int getType(){
    	return type.getTypeCode();
    }

    public void setType(int arg) {
//        this.type = type;

    	switch (arg) {
    	
		case ENGINEER:
			type = new Engineer();
			break;
		case SALESMAN:
			type = new Salesman();
			break;
		case MANAGER:
			type = new Manager();
			break;
		default:
			throw new IllegalArgumentException("사원 부호가 잘못됨.");
		}
    	
    	type = EmployeeType.newType(arg);
    }
  • 분류 부호를 상태/전략 패턴으로 전환
Employee...  
    public void setType(int arg) {
    	type = EmployeeType.newType(arg);
    }

EmployeeType...
	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 IllegalArgumentException("사원 부호가 잘못됨.");
		}
	}

Employee...
    int payAmount(int arg) {
        switch (arg) {
        case EmployeeType.ENGINEER:
            return monthlySalary;
        case EmployeeType.SALESMAN:
        	return  monthlySalary + commission;
        case EmployeeType.MANAGER:
        	return  monthlySalary + bonus;
        default:
            throw new IllegalArgumentException("Incorrect");
        }
    }

하위클래스를 필드로 전환


여러 하위클래스가 상수 데이터를 반환하는 메서드만 다른땐 각 하위클래스의 메서드를 상위클래스 필드로 전환하고 하위클래스는 전부 삭제하라.

동기

기능을 추가하거나, 동작의 변화를 허용하기 위해서 서브클래스를 만든다. 동작 변화의 한 형태는 상수메소드(하드 코딩된 값을 리턴하는 메소드)이다. 상수메소드는 유용하기는 하지만, 상수 메소드를 포함하고 있는 서브클래스는 존재할 가치가 있을 만큼 충분한 일을 하는 것이 아니다. 서브클래스를 사용하면서 생기는 여러 복잡성을 제거할 수 있다.

방법
  1. 하위클래스에 생성자를 팩토리 메서드로 전환
  2. 하위클래스 참조 부분을 전부 상위클래스 참조로 수정
  3. 상위클래스에 각 상수 메서드에 대한 final타입의 필드를 선언
  4. protected타입의 상위클래스 생성자를 선언해서 필드를 초기화
  5. 하위클래스 생성자를 추가하거나 수정해서 새 상위클래스 생성자를 호출하자
  6. 상위클래스 안에 필드를 반환하는 각 상수 메서드를 구현하고 하위클래스의 메서드는 삭제하자.
  7. 하위클래스 메서드를 전부 삭제했으면 메서드 내용 직접삽입 기법을 실시해서 생성자 메서드 내용을 상위클래스의 팩토리 메서드에 넣자.
예제
abstract class Person {
	abstract boolean isMale();
	abstract char getCode();
}

public class Maie extends Person{

	@Override
	boolean isMale() {
		return true;
	}

	@Override
	char getCode() {
		return 'M';
	}

}

public class Female extends Person{

	@Override
	boolean isMale() {
		return false;
	}

	@Override
	char getCode() {
		return 'F';
	}

}
  • Male하위클래스와 Female하위클래스는 하위클래스는 하드 코딩된 상수 메서드반환만 다르다.
  • 생성사를 팩토리 메서드로 전환
  • 상위클래스에 다음과 같이 각 상수 메서드별 필드를 선언하자.
	static Person createMale(){
		return new Person(true,'M');
	}
	
	static Person createFemale(){
		return new Person(false,'F');
	}

	private final boolean _isMale;
	private final char _code;