클래스를 만들 때 첫 번째 규칙은 크기다.
함수는 물리적인 행 수로 크기를 측정했다
클래스는 맡은 책임을 센다
아래 클래스를 말로 풀어서 설명해보자면,
'마지막으로 포커스를 얻었던 컴포넌트에 접근하는 방법을 제공하며, 버전과 빌드 번호를 추적하는 ~~..'
책임이 너무 많다.
public class SuperDashboard extends JFrame implements MetaDataUser {
public Component getLastFocusedComponent()
public void setLastFocused(Component lastFocused)
public int getMajorVersionNumber()
public int getMinorVersionNumber()
public int getBuildNumber()
}
메서드 수가 작음에도 책임이 너무 많다.
클래스 이름은 클래스 책임을 기술해야 한다.
작명은 클래스 크기를 줄이는 첫 번째 관문이다.
아래와 같이 버전 정보를 다루는 메서드 세 개를 따로 빼내 Version 이라는 독자적인 클래스를 만들 수 있다.
Version 클래스는 다른 애플리케이션에서 재사용하기 아주 쉬운 구조다.
// 단일 책임 클래스
public class Version {
public int getMajorVersionNumber()
public int getMinorVersionNumber()
public int getBuildNumber()
}
SRP는 이상하게도 설계자가 가장 무시하는 규칙 중 하나다.
많은 책임을 떠맡은 클래스는 당장 알 필요가 없는 사실까지 들이밀며 독자를 방해한다.
큰 클래스 몇 개보다는 작은 클래스 여럿으로 이뤄진 시스템이 더 바람직하다.
클래스는 인스턴스 변수 수가 작아야 한다.
일반적으로 메서드가 인스턴스 변수를 더 많이 사용할수록 메서드와 클래스는 응집도가 더 높다.
아래 클래스는 size()
를 제외한 모든 메서드가 두 변수를 모두 사용하는 응집도가 높은 클래스이다.
// 응집도가 높은 클래스
public class Stack {
private int topOfStack = 0;
List<Integer> elements = new LinkedList<Integer>();
public int size() {
return topOfStack;
}
public void push(int element) {
topOfStack++;
elements.add(element);
}
public int pop() throws PoppedWhenEmpty {
if (topOfStack == 0)
throw new PoppedWhenEmpty();
int element = elements.get(--topOfStack);
elements.remove(topOfStack);
return element;
}
}
함수를 작게 만들다 보면 때때로 몇몇 메서드만이 사용하는 인스턴스 변수가 많아진다. (== 응집도가 낮아진다)
이는 십중팔구 새로운 클래스로 쪼개야 한다는 신호다.
응집도가 높아지도록 변수와 메서드를 적절히 분리해 새로운 클래스 두세 개로 쪼개줄 수 있다.
public class Sql {
public Sql(String table, Column[] columns)
public String create()
public String insert(Object[] fields)
public String selectAll()
public String findByKey(String keyColumn, String keyValue)
public String select(Column column, String pattern)
public String select(Criteria criteria)
public String preparedInsert()
private String columnList(Column[] columns)
private String valuesList(Object[] fields, final Column[] columns) private String selectWithCriteria(String criteria)
private String placeholderList(Column[] columns)
}
가까운 장래에 update 문이 필요하지 않고 논리적으로 완성으로 여긴다면
책임을 분리하려 시도할 필요가 없다.
하지만 클래스에 손대는 순간 설계를 개선하려는 고민과 시도가 필요하다.
abstract public class Sql {
public Sql(String table, Column[] columns)
abstract public String generate();
}
public class CreateSql extends Sql {
public CreateSql(String table, Column[] columns)
@Override public String generate()
}
public class SelectSql extends Sql {
public SelectSql(String table, Column[] columns)
@Override public String generate()
}
public class InsertSql extends Sql {
public InsertSql(String table, Column[] columns, Object[] fields)
@Override public String generate()
private String valuesList(Object[] fields, final Column[] columns)
}
public class SelectWithCriteriaSql extends Sql {
public SelectWithCriteriaSql(
String table, Column[] columns, Criteria criteria)
@Override public String generate()
}
public class SelectWithMatchSql extends Sql {
public SelectWithMatchSql(String table, Column[] columns, Column column, String pattern)
@Override public String generate()
}
public class FindByKeySql extends Sql public FindByKeySql(
String table, Column[] columns, String keyColumn, String keyValue)
@Override public String generate()
}
public class PreparedInsertSql extends Sql {
public PreparedInsertSql(String table, Column[] columns)
@Override public String generate() {
private String placeholderList(Column[] columns)
}
public class Where {
public Where(String criteria) public String generate()
}
public class ColumnList {
public ColumnList(Column[] columns) public String generate()
}
인터페이스를 사용해 구현이 미치는 영향을 격리한다.
Portfolio 클래스에서 TokyoStockExchange 클래스에 직접 의존하기 보다는,
StockExchange 라는 인터페이스를 생성한 후 TokyoStockExchange 가 해당 인터페이스를 구현하게 만들 수 있다.
이와 같이 테스트가 가능할 정도로 시스템의 결합도를 낮추면(== 변경으로부터 잘 격리하면)
유연성과 재사용성도 더욱 높아진다.
또한 자연스럽게 구체 타입이 아닌 추상 타입에 의존하게 되어 DIP를 따르게 된다.