[CleanCode] -10. 클래스

23205 단어 cleancodecleancode

1. 클래스는 작아야 한다

클래스를 만들 때 첫 번째 규칙은 크기다.

함수는 물리적인 행 수로 크기를 측정했다
클래스는 맡은 책임을 센다

아래 클래스를 말로 풀어서 설명해보자면,
'마지막으로 포커스를 얻었던 컴포넌트에 접근하는 방법을 제공하며, 버전과 빌드 번호를 추적하는 ~~..'
책임이 너무 많다.

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;
    }
}

함수를 작게 만들다 보면 때때로 몇몇 메서드만이 사용하는 인스턴스 변수가 많아진다. (== 응집도가 낮아진다)
이는 십중팔구 새로운 클래스로 쪼개야 한다는 신호다.
응집도가 높아지도록 변수와 메서드를 적절히 분리해 새로운 클래스 두세 개로 쪼개줄 수 있다.


2. 변경하기 쉬운 클래스

  • 적절한 SQL 문자열을 만들어주는 Sql 클래스
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()
}
  • 장점
    • SRP 지원: 하나의 클래스에서 하나의 책임만을 맡음
    • OCP 지원: 기능 추가 시 기존 구조를 변경할 필요 없이 새 객체를 추가하면 됨

변경으로부터 격리

인터페이스를 사용해 구현이 미치는 영향을 격리한다.

Portfolio 클래스에서 TokyoStockExchange 클래스에 직접 의존하기 보다는,

StockExchange 라는 인터페이스를 생성한 후 TokyoStockExchange 가 해당 인터페이스를 구현하게 만들 수 있다.

이와 같이 테스트가 가능할 정도로 시스템의 결합도를 낮추면(== 변경으로부터 잘 격리하면)
유연성과 재사용성도 더욱 높아진다.
또한 자연스럽게 구체 타입이 아닌 추상 타입에 의존하게 되어 DIP를 따르게 된다.

좋은 웹페이지 즐겨찾기