추상화 수준이 비슷한 메소드 호출로 하나의 메소드를 구성하는 것이 좋다. 다음 코드를 보자.
void compute() {
input();
flags |= 0x0080; // ?
output();
}
위의 메소드에서 2번째 라인은 무엇을 의미하는 걸까? 코드를 읽다가 갑자기 추상화 수준이 다르면 읽기 흐름이 깨진다. 이 코드를 다음과 같이 변경하면 코드를 읽을 때 좀 더 이해하기가 쉬울 것이다.
void setTranslate() {
flags |= 0x0080;
}
void compute() {
input();
setTranslate();
output();
}
위의 코드와 같이 비록 짧은 메소드를 하나 더 만들었지만 compute() 메소드를 분석할 때, 더 이해하기가 쉬울 것이다.
짧은 메소드를 많이 사용하는 것에 대한 반대 의견은 잦은 호출로 인해 성능 저하가 발생한다는 것이다. 그렇지만 CPU는 점점 빨라지고, 성능상 병목 현상을 일으키는 코드는 밀집되어 있는 경향이 있으므로 성능 문제는 실제 수행을 통해 측정 후 개선하는 것이 좋다.
메소드를 사용하는 쪽에서는 메소드 이름을 통해 메소드가 하고자 하는 의도를 쉽게 파악할 수 있어야 한다.
Customer.linearCustomerSearch(String id);
...
Customer.find(String id);
두 번째 메소드 정의처럼 이름을 통해서는 메소드의 의도를 전달하고, 그 외 정보는 다른 방식으로 전달하는 것이 좋다.
Customer.findwithLinearSearch(String id);
Customer.findwithHashSearch(String id);
위의 코드와 같이 메소드가 어떤 id를 가지고 선형 탐색할 지, 해쉬 탐색을 할 지는 알 수 있겠지만 사용자 입장에서는 그리 중요한 것이 아닐 수가 있다. 이런 긴 메소드 이름은 이 메소드를 쓰는 코드의 가독성이 떨어진다.
메소드의 이름을 지을 때는 그 메소드를 호출하는 입장에서 생각해보자. 왜 이 메소드를 사용하는 것인가? 이름은 이 질문에 답을 얻을 수 있어야 한다.
자바에서 메소드 가시성, 즉 public, default (package), protected, private 역시 프로그래머의 의도를 전달한다.
먼저 가장 제한적인 가시성을 가지는 private로 시작하여 필요에 따라 조금씩 가시성을 높여라.
복잡하게 짜여진 메소드를 읽기 쉽게, 세부 구현 전달이 쉽도록 바꿔준다.
이 리팩토링된 새로은 클래스는 다시 한 번 리팩토링하기가 쉬울 수 있다. 모든 데이터가 필드로 변경되었으므로 파라미터를 사용할 필요도 없고 리팩토링할 수 있는 여지가 많기 때문이다.
이 오버라이드를 통해 유사한 연산 사의의 차이점을 상위 클래스와 하위 클래스로 나누어 표현할 수 있다.
상위 클래스 메소드를 잘 구성하여 가급적 작은 코드로 구성하였을 경우 오버라이드하기가 쉽다.
오버로드의 의미는 이 메소드를 사용함에 있어서 다양한 포맷이 존재한다라는 의미이다.
같은 메소드 이름에 다른 수의 파라미터를 사용하는 것은 좋지 않다. 메소드 이름 뿐만 아니라 파라미터도 유심히 살펴봐야 되기 때문이다. 너무 복잡해지면 어떤 인자를 사용했을 때 어떤 메소드가 호출될지 알아야 한다.
메소드 오버로드는 파라미터 타입만 다를 뿐, “같은 연산”을 수행해야 한다. 메소드의 의도가 다르다면 다른 이름을 사용해야 한다.
Class Polar {
Cartesian asCartesian() {
...
}
}
위와 같이 특정 정보에서 다른 형태의 객체를 얻기 위해 변환 메소드를 사용할 수 있지만, 너무 자주 사용하면 클래스 코드를 일기 어려울 뿐만 아니라, 서로 다른 두 클래스간에 의존성이 생긴다.
다음과 같이 다른 객체를 파라미터로 취하여 새로운 객체를 생성할 때 사용한다.
public Meter {
public Meter(User user) {
...
}
}
이 생성자는 하나의 특정 타입 객체를 여러 타입으로 변환할 때 유용하다. 변환 메소드를 사용할 때보다 해당 특정 타입의 클래스에 메소드를 일일이 추가할 필요없기 때문이다.
객체는 연산을 하기위해 정보가 필요한데, 객체 생성시 생성자를 통해 객체를 사용하기 위한 값들을 전달할 수 있도록 해야한다. 다음과 같은 코드는 좋지 않다.
Rectangle box = new Rectangle();
box.setLeft(0);
box.setWidth(0);
box.setHeight(200);
box.setTop(0);
위와 같은 코드는 사용자 입장에서 이 객체를 생성하기 위해 어떤 파라미터들이 필요한지 정확하게 알기 어렵다.
객체 생성할 때 다른 방법은 그 클래스의 static 메소드를 통해 객체를 생성하는 것이다. 이 팩토리 메소드는 이 메소드를 통해 특정 작업이 필요한 경우에만 사용해야 한다.
팩토리 메소드 사용하면, 코드 읽는 사람의 입장에서는 이 객체를 생성하기 위해 어떤 작업이 일어나는지 궁금해할 수 있다. 다른 사람이 내가 짠 코드를 읽는 시간을 아껴주고 싶으면 평범한 객체 생성에 대해서는 그냥 일반 생성자를 사용하라.
다음과 같은 클래스가 있다고 해보자.
class BookStore {
List<Book> books;
...
List<Book> getBooks() {
return books;
}
}
위와 같이 정의했을 때, 사용자가 직접 컬렉션을 조작하게 되므로 컬렉션 필드에 의존하는 다른 객체의 필드가 유효하지 않을 수 있다.
어떤 특정 클래스 컬렉션 필드에 대해서는 제한적이지만 의미있는 접근을 제공하는 메소드를 다음과 같이 제공해야 한다.
class BookStore {
List<Book> books;
...
public addBook(Book book) {
books.add(book);
}
public int getBookCount() {
return books.size();
}
}
만약 사용자가 컬렉션에 대해서 각각의 원소마다 접근할 필요가 생긴다면 iterator를 리턴하는 것이 좋다.
public Iterator getBooks() {
return books.iterator();
}
또한 만약 내부 컬렉션에 대해서 수정되는 것을 금지하고 싶다면 iterator를 오버라이드하여 특정 메소드에 대해 예외를 던지게하여 금지시킬 수 있다.
public Iterator<Book> getBooks() {
final Iterator<Book> reader = books.iterator();
return new Iterator<Book>() {
public boolean hasNext() {
return reader.hasNext();
}
public Book next() {
return reader.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
만약 이런 컬렉션에 대한 작업을 대신하는 메소드를 많이 구현하고 있다면 설계 상에 문제가 있을 확률이 높다. 만약 클래스가 적절한 작업을 제공하고 있다면 이런 메소드를 많이 구현할 필요가 없다.
어떤 객체의 boolean 상태를 변경하는데에 있어서 다음 코드와 같이 구현할 수 있을 것이다.
void setValid(boolean newState) {
...
}
그렇지만 상태를 변경함에 있어서 넘어오는 파라미터가 간단한 값이라면, 다음과 같이 프로시저 형태로 제공하는 것이 인터페이스를 더욱 명확히 할 수 있다.
public void valid();
public void invalid();
하지만 다음 코드와 같이 상태 변경에 있어 파라미터에 따라 분기문과 같이 로직이 달라질 경우, setValid(boolean newState) 형태로 제공하는 것이 낫다.
...
if (...boolean expression...) {
...
} else {
....
}
어느 특정 객체에 대해 동일성이 아닌, 동등성을 비교해야 한다면 equals() 와 hashCode() 메소드를 구현해야 한다. 동등한 객체는 같은 해시 값을 가져야 하므로, hashCode() 메소드를 구현할 때, equals() 메소드를 구현하는데 사용한 데이터만을 사용해야 한다.
객체의 어떤 필드에 대해 접근을 허용하기 위한 한 가지 방법은 getXXX()와 같은 getter를 제공하는 것이다. 그런데 이런 getter를 사용한다는 것은 이 데이터를 사용하는 코드가 다른 곳에 있다는 것을 의미한다.
Setter는 getter와는 다르게, 메소드 이름은 클라이언트 입장에서 짓는 것이 좋다. 다음과 같이 setter를 제공할 경우, 클래스 내부 구현이 노출된다.
object.setJustification(Paragraph.CENTERED);
위와 같이 정의하는 것보다는 다음과 같이 인터페이스 이름을 정해주면 코드 읽기가 쉬워질 것이다.
object.centered();