클린코드 chap 10. 클래스 잘 설계하기

최준영·2021년 10월 8일
0

클린한 코드

목록 보기
11/16

1. 캡슐화 되어야 한다.


  • 클래스를 개발할 때 기본적으로 구현을 감추고, 외부 객체와 상호작용 하는 부분만 노출한다.
  • 외부의 잘못된 사용을 방지한다.
  • 경계에서 배웠던 부분(Map)

2. 단일 책임 원칙(SRP)


클래스는 작아야 한다.

  • 함수와 마찬가지로 클래스도 작아야 한다.
  • 함수는 라인 수로 크기를 측정했는데, 클래스는 맡은 책임의 수로 크기를 측정한다.
  • 클래스 설명은 만일(if), 그리고(and), 하며(or), 하지만(but)을 사용하지 않고 25단어 내외로 가능해야 한다. -> 책임이 한가지 이어야 한다.
  • 작은 클래스가 많은 시스템이든 큰 클래스가 몇 개 뿐이 시스템이든 돌아가는 푸품은 그 수가 비슷하다.
  • 큼직한 다목적 클래스 몇 개로 이뤄진 시스템은 (변경을 가할 때) 당장 알 필요가 없는 사실까지 들이밀어 독자를 방해한다.
  • 작은 클래스는 각자 맡은 책임이 하나며, 변경할 이유가 하나며, 다른 작은 클래스와 협력해 시스템에 필요한 동작을 수행한다.

예제 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() 
}
  • focuse, version 두가지 책임이 보인다.

예제 2. 좋은 예

public Version {
    public int getMajorVersionNumber()
    public int getMinorVersionNumber()
    public int getBuildNumber() 
}
  • 다른 곳에서도 재활용 할 수 있다.

3. 낮은 결합도, 높은 응집도


  • 결합도란 다른 모듈간의 의존도를 말한다.
  • 응집도란 모듈 내부의 기능 집중도를 말한다.
  • 결합도는 낮을 수록, 응집도는 높을 수록 유지보수성이 좋다.

1) 문제점

  • 결합도가 높은 클래스의 문제점
    • 연관된 클래스가 변경되면 수정이 필요하다.
    • 결합도가 높으면 연관된 클래스들을 모두 이해해야 한다.
  • 응집도가 낮은 클래스의 문제점
    • 여러 기능이 있으므로 이해하기 어렵다
    • 재사용하기 어렵다.

2) 낮은 결합도

  • 시스템의 결합도를 낮추면 유연성과 재사용성도 더욱 높아진다.
  • DIP : 클래스가 상세한 구현이 아니라 추상화에 의존해야한다.
  • 추상화를 이용하면 테스트코드 짜기에 용이하다.

예제 3. 나쁜 예

public class TokyoStockExchange {
  public Money currentPrice(String symbol);
}

public Protfolio {
  private TokyoStockExchange tokyoStockExchange;
  public Portfolio(TokyoStockExchange exchange) {
    this.tokyoStockExchange = tokyoStockExchange;
  }
}
  • TokyoStockExchange 함수의 API가 5분마다 값이 달라지는 상황이다.
    • Portfolio 클래스 테스트 코드를 짜기 어렵다.

예제 4. 좋은 예

public interface StockExchange {
  Money currentPrice(String symbol);
}

public class TokyoStockExchange implements StockExchange {
  public Money currentPrice(String symbol) {
    // call API ...
  }
}

public Portfolio {
  pricate stockExchange exchange;
  public Portfolio(StockExchange exchange) {
    this.exchange = exchange;
  }
}
  • StockExchange 인터페이스를 통해 Portfolio와 tokyoStockExchange의 결합도를 끊어준다.

예제 5. 예제 4의 테스트코드

public class PortfolioTest {
  private FixedStockExchangeStub exchange;
  private Portfolio portfolio;
  
  @Before
  protected void setUp() throws Exception {
    exchange = new FixedStockExchangeStub();
    exchange.fix("MSFT", 100);
    portfolio = new Portfolio(exchange);
  }
  
  @Test
  public void GivenFiveMSFTTotalShouldBe500() throws Exception {
    portfolio.add(5, "MSFT");
    AssertEquals(500, portfolio.value());
  }
}
  • 객체를 Mockking하면 변경되는 클래스도 테스트할 수 있다.
  • 확장될 가능성이 적다면 일단 결합하고, 나중에 추상화해도 좋다.

3) 높은 응집도

  • 클래스는 인스턴스 변수 수가 적어야 한다.
  • 메서드는 인스턴스 변수를 하나 이상 사용해야 한다.
  • 메서드가 인스턴스 변수를 많이 사용할수록 응집도가 높다
  • 응집도가 높다 = 클래스에 속한 메서드와 변수가 서로 의존하며 논리적인 단위로 묶인다 = 서로 관계있는 애들만 모여있다.
  • 클래스가 응집도를 잃어간다면 함수를 쪼개야한다.

예제 6. 좋은 예

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

4. 변경하기 쉬워야 한다.


예제 7. 나쁜 예

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)
}
  • 새로운 SQL을 추가할 때도 수정이 발생하고, 기존 SQl문을 수정할 때도 수정이 발생하므로 OCP가 위반된다.

예제 8. 좋은 예

    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()
    }
  • 공개 인터페이스를 전부 SQL 클래스에서 파생하는 클래스로 만들고, 비공개 메서드는 해당 클래스로 옮기고, 공통된 인터페이스는 따로 클래스로 뺐다.
  • 기존의 클래스를 건드리지 않아도 된다.
profile
do for me

0개의 댓글