더 높은 단계의 클린코드
클래스 체계
클래스를 정의하는 표준 자바관례
- 변수 목록
- 정적 공개 상수 (public static)
- 비공개 변수 (private static)
- 비공개 인스턴스 변수 (private)
- 공개함수
- 비공개 함수 (자신을 호출하는 공개함수 직후)
캡슐화
- 변수와 유틸리티 함수는 가능한 공개하지 X
- 비공개 상태를 유지해야 하고 같은 패키지 안에서
테스트 코드가 함수를 호출하거나 변수를 사용해야 할 경우 protected로 선언하거나 패키지 전체 공개
클래스는 작아야 한다.
- 함수와 마찬가지로 "작게" 가 기본 규칙이다.
- 함수는 "작게" 의 개념을 행 수로 크기를 측정했다.
- 클래스는 맡은 책임을 기준으로 세운다.
- 예제의 superDashboard와 같은 70개의 공개 메서드 -> 딱봐도 책임이 너무 많다.
protocol SuperDashboard : JFrame, MetaDataUser {
func getCustomerizerLanguagePath() -> String
func setSystemConfigPath(systemConfigDocument: String)
func getSystemConfigDocument() -> String
func setSystemConfigDocument(systemConfigDocument: String)
func getGuruState() -> Bool
func getNoviceState() -> Bool
func getOpenSourceState() -> Bool
func showObject()
///... etc
}
- 메서드 몇개만 포함한다고 하면 괜찮은가? 정답은 아니다. 왜 아닌가 생각해보자
protocol SuperDashboard : JFrame, MetaDataUser {
func getLastFocusedComponent() -> Component
func setLastFocused(lastFocused: Component)
func getMajorVersionNumber() -> Int
func getMinorVersionNumber() -> Int
func getBuilderNumber() -> Int
}
- 메서드 수는 많지만 책임이 너무 많다.
- 책임이 많은건 어떻게 아느냐?!
- 간결한 이름이 떠오르지 않으면, 클래스의 책임이 너무 많아서다.
- Processor, Manager, Super등과 같은 단어가 있다면 그 클래스는 여러 책임을 떠안았다는 증거다.
- 이걸 네이밍으로 판단 조져버리네
여튼 SuperDashboard의 책임은 두개로 나눌 수 있다.
- 마지막으로 포커스를 얻었던 컴포넌트에 접근하는 방법을 제공하며
- 버전과 빌드 번호를 추적하는 메커니즘을 제공한다.
단일 책임 원칙
- 클래스나 모듈을 변경할 이유가 단 하나 뿐이여야 한다는 원칙
- 책임이라는 개념을 정의하며 적절한 클래스 크기를 제시해 준다.
그래서 겉보기에 작아 보이는 SuperDashboard 도 변경할 이유가 두가지 있다.
- SuperDashBoard는 소프트웨어 버전 정보를 추적한다. 그런데 버전 정보는 소프트웨어 출시 때마다 달라진다.
- SuperDashBoard는 자바 스윙 컴포넌트를 관리한다. 그런데 스윙 코드를 변경할 때 마다 버전 번호가 달라진다.
그러니까 스윙 관련해서 코드 작업을 할때도 SuperDashboard가 변경될 수 있고 버전 정보가 바뀔 때마다도 SuperDashboard가 변경될 수 있으니 변경될 이유가 두가지라는 거고 이걸 따로 분리하라는 말인가?
책임 즉 변경할 이유를 파악하려 애쓰다 보면 코드를 추상화 하기도 쉬워진다
protocol Version {
func getMajorVersionNumber() -> Int
func getMinorVersionNumber() -> Int
func getBuilderNumber() -> Int
}
- SRP는 클래스 설계자가 가장 무시하는 규칙 중 하나이다.
- 깨끗하고 체계적인 소프트웨어보다 "돌아가는 소프트웨어"에 초점을 맞추기 때문
- 프로그램이 돌아가면 다시 되돌아가 만능 클래스를 단일 책임 클래스로 여럿 분리하는 대신 다음 피쳐로 넘어감
- 자잘한 단일 책임 클래스가 많아지면 큰 그림을 이해하기 어려워진다고 우려함 (타고 또 타고 들어가야되서)
- 결론은 작은 클래스 여럿으로 이뤄진 시스템이 바람직하다!
응집도
- 클래스는 인스턴스 변수가 작아야 한다.
- 각 클래스 메서드는 클래스 인스턴스 변수를 하나이상 사용해야 한다.
- 메서드가 변수를 더 많이 사용할 수록 응집도가 높다.
- 모든 인스턴스 변수를 메서드마다 사용하는 클래스도 응집이 가장 높다.
클래스에 속한 메서드와 변수가 서로 의존하며 논리적인 단위로 묶인다는 것!
// 응집도 높은 예시
public class Stack {
private var topOfStack = 0
var elements = [Int]()
public func size() -> Int {
return topOfStack
}
public func push(element: Int) {
topOfStack += 1
elements.append(element)
}
public func pop() throws -> Int {
if topOfStack == 0 {
throw StackErrors.whatTheFuck
}
let element = elements.popLast()
topOfStack -= 1
return element!
}
}
enum StackErrors: Error {
case whatTheFuck
}
- 함수를 작게, 매개변수 목록을 짧게 하다보면 몇몇 메서드만이 사용하는 인스턴스 변수가 많아진다 -> 쪼개라
- 응집도가 높아지도록 변수와 메서드를 적절히 분리해 새로운 클래스 두세 개로 쪼개준다.
큰 함수의 일부를 작은 함수 하나로 빼내고 싶다.
빼내려는 코드가 함수에 정의된 변수 네개를 사용한다.
- 파라미터로 넘길 것인가 -> 애바다
- 인스턴스 변수로 뺄것인가 -> 응집력을 잃는다
정답: 독자적인 클래스로 분리해라
실전연습
package clean.code.chapter10.original;
public class PrintPrimes {
public static void main(String[] args) {
final int M = 1000;
final int RR = 50;
final int CC = 4;
final int WW = 10;
final int ORDMAX = 30;
int P[] = new int[M + 1];
int PAGENUMBER;
int PAGEOFFSET;
int ROWOFFSET;
int C;
int J;
int K;
boolean JPRIME;
int ORD;
int SQUARE;
int N;
int MULT[] = new int[ORDMAX + 1];
J = 1;
K = 1;
P[1] = 2;
ORD = 2;
SQUARE = 9;
while (K < M) {
do {
J = J + 2;
if (J == SQUARE) {
ORD = ORD + 1;
SQUARE = P[ORD] * P[ORD];
MULT[ORD - 1] = J;
}
N = 2;
JPRIME = true;
while (N < ORD && JPRIME) {
while (MULT[N] < J)
MULT[N] = MULT[N] + P[N] + P[N];
if (MULT[N] == J)
JPRIME = false;
N = N + 1;
}
} while (!JPRIME);
K = K + 1;
P[K] = J;
}
{
PAGENUMBER = 1;
PAGEOFFSET = 1;
while (PAGEOFFSET <= M) {
System.out.println("The First " + M +
" Prime Numbers --- Page " + PAGENUMBER);
System.out.println("");
for (ROWOFFSET = PAGEOFFSET; ROWOFFSET < PAGEOFFSET + RR; ROWOFFSET++){
for (C = 0; C < CC;C++)
if (ROWOFFSET + C * RR <= M)
System.out.format("%10d", P[ROWOFFSET + C * RR]);
System.out.println("");
}
System.out.println("\f");
PAGENUMBER = PAGENUMBER + 1;
PAGEOFFSET = PAGEOFFSET + RR * CC;
}
}
}
}
- 소수 목록을 주어진 행과 열에 맞춰 페이지에 출력하는 클래스
- 두개의 책임을 나누는 리팩터링
- 소수 목록
-
더보기
package literatePrimes; import java.util.ArrayList; public class PrimeGenerator { private static int[] primes; private static ArrayList<Integer> multiplesOfPrimeFactors; protected static int[] generate(int n) { primes = new int[n]; multiplesOfPrimeFactors = new ArrayList<Integer>(); set2AsFirstPrime(); checkOddNumbersForSubsequentPrimes(); return primes; } private static void set2AsFirstPrime() { primes[0] = 2; multiplesOfPrimeFactors.add(2); } private static void checkOddNumbersForSubsequentPrimes() { int primeIndex = 1; for (int candidate = 3 ; primeIndex < primes.length ; candidate += 2) { if (isPrime(candidate)) primes[primeIndex++] = candidate; } } private static boolean isPrime(int candidate) { if (isLeastRelevantMultipleOfNextLargerPrimeFactor(candidate)) { multiplesOfPrimeFactors.add(candidate); return false; } return isNotMultipleOfAnyPreviousPrimeFactor(candidate); } private static boolean isLeastRelevantMultipleOfNextLargerPrimeFactor(int candidate) { int nextLargerPrimeFactor = primes[multiplesOfPrimeFactors.size()]; int leastRelevantMultiple = nextLargerPrimeFactor * nextLargerPrimeFactor; return candidate == leastRelevantMultiple; } private static boolean isNotMultipleOfAnyPreviousPrimeFactor(int candidate) { for (int n = 1; n < multiplesOfPrimeFactors.size(); n++) { if (isMultipleOfNthPrimeFactor(candidate, n)) return false; } return true; } private static boolean isMultipleOfNthPrimeFactor(int candidate, int n) { return candidate == smallestOddNthMultipleNotLessThanCandidate(candidate, n); } private static int smallestOddNthMultipleNotLessThanCandidate(int candidate, int n) { int multiple = multiplesOfPrimeFactors.get(n); while (multiple < candidate) multiple += 2 * primes[n]; multiplesOfPrimeFactors.set(n, multiple); return multiple; } }
- 행과 열에 맞춰 출력
-
더보기
package literatePrimes; import java.io.PrintStream; public class RowColumnPagePrinter { private int rowsPerPage; private int columnsPerPage; private int numbersPerPage; private String pageHeader; private PrintStream printStream; public RowColumnPagePrinter(int rowsPerPage, int columnsPerPage, String pageHeader) { this.rowsPerPage = rowsPerPage; this.columnsPerPage = columnsPerPage; this.pageHeader = pageHeader; numbersPerPage = rowsPerPage * columnsPerPage; printStream = System.out; } public void print(int data[]) { int pageNumber = 1; for (int firstIndexOnPage = 0 ; firstIndexOnPage < data.length ; firstIndexOnPage += numbersPerPage) { int lastIndexOnPage = Math.min(firstIndexOnPage + numbersPerPage - 1, data.length - 1); printPageHeader(pageHeader, pageNumber); printPage(firstIndexOnPage, lastIndexOnPage, data); printStream.println("\f"); pageNumber++; } } private void printPage(int firstIndexOnPage, int lastIndexOnPage, int[] data) { int firstIndexOfLastRowOnPage = firstIndexOnPage + rowsPerPage - 1; for (int firstIndexInRow = firstIndexOnPage ; firstIndexInRow <= firstIndexOfLastRowOnPage ; firstIndexInRow++) { printRow(firstIndexInRow, lastIndexOnPage, data); printStream.println(""); } } private void printRow(int firstIndexInRow, int lastIndexOnPage, int[] data) { for (int column = 0; column < columnsPerPage; column++) { int index = firstIndexInRow + column * rowsPerPage; if (index <= lastIndexOnPage) printStream.format("%10d", data[index]); } } private void printPageHeader(String pageHeader, int pageNumber) { printStream.println(pageHeader + " --- Page " + pageNumber); printStream.println(""); } public void setOutput(PrintStream printStream) { this.printStream = printStream; } }
- 좀 더 서술적인 변수 이름을 사용
- 주석을 대신할 수 있을만한 함수 선언과 클래스 선언
- 공백 추가와 형식 맞춤
변경하기 쉬운 클래스
시스템은 언젠간 기필코 변경되기 때문에 변경이 있을 때 마다 버그가 생길 가능성이 있다면 스트레스 만땅
예를들어
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 코드는 5법칙 중 무엇을 위반하는가??
- 새로운 SQL문을 지원할 때 손대야 하고,
- 기존 SQL문을 수정할 때도 손대야 하므로
SRP 위반한다.
클래스 일부에서만 사용되는 비공개 메서드를 보면 코드를 개선의 가능성을 알 수 있다.
(클래스 일부에서만 사용하니까 쪼개라는 건가?)
- 공개 인터페이스를 전부 SQL 클래스에서 파생되는(SQL 클래스 상속하는) 클래스로 만듬
- 비공개 메서드는 사용되는 해당 클래스로 옮김
- 공통된 인터페이스도 따로 클래스 분리 (Where, CoulumList)
위 작업을 통해 새로운 sql문 (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()
}
템플릿 메소드 패턴?
결론은 위와 같이 잘 짜여진 시스템은 추가와 수정이 있을떄 수정해야할 코드가 최소이다.
- concrete 클래스에 의존하는 클래스는 구현이 바뀌면 버그가 발생할 수 있다.
- 그래서 abstract 클래스를 사용해 수정에 영향에서 격리시켜야 한다.
- 추상화를 통해 시스템의 결합도를 낮춘다고 표현할 수 있다.
- 유연성과 재사용성도 높아진다.
예제
상세한 구현에 의존하는 코드
[문제상황]
Portfolio 클래스를 테스트 하려고 하는데
Portfolio 클래스는 외부 ToykoStockExchangeAPI를 사용해서 값을 계산한다.
api는 5분마다 값이 달라진다.
[해결방법]
Portfolio에서 직접 api를 호출하지 않고 StockExchange라는 인터페이스를 생성한 후 메서드를 선언한다.
public interface StockExchange {
Money currentPrice(String symbol);
}
인터페이스를 구현하는 TokyoStockExchange 클래스를 구현하고난 뒤
Portfolio 생성자에 StockExchange 레퍼런스를 파라미터로 받는다.
public Portfolio {
private StockExchange exchange;
public Portfolio(StockExchange exchange) {
this.exchange = exchange;
}
// ...
}
TokyoStockExchange Mock class를 만들 수 있다.
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");
Assert.assertEquals(500, portfolio.value());
}
}
인터페이스를 활용하여 구현 클래스와의 격리를 통해 실제 구현이 달라져도 영향받지 않게 만들 수 있다.
댓글