이 장은 깨끗한 클래스를 작성하는 법에 대해서 다룬다.
객체지향의 특징인 S.O.L.I.D 원칙을 어떻게 적용하는 지 매우 구체적으로 코드를 통해서 설명해주신다.
이 글부터는 길고 구체적인 예시가 많이 나오므로, 정말 요약만 전달하겠다.
클래스는 작아야한다.
함수와 동일하게 클래스도 작을 수록 좋다.
다만, "작다"의 기준은 함수와 다르게 책임이 적어야한다가 기준이 된다.
단일책임원칙(SRP)
책임이 작아야하지만, 사실 원칙상 클래스의 책임은 하나여야한다.
즉, 클래스를 변경해야할 이유는 단 하나여야한다.
만약, 클래스를 변경해야할 이유가 두 가지라면 클래스를 쪼개야한다는 의미이며 큰 클래스 하나보다 책임이 하나인 작은 클래스 여러 개가 더 바람직하다 표현한다.
저자는 많은 개발자가 SRP를 알지만, 본능적으로 "깨끗하고 체계적인 소프트웨어"보다 "돌아가는 소프트웨어"에 초점이 맞춰져 이런 실수가 자주 일어난다 말한다.
하지만, 시스템이 커지고 복잡할수록 체계적인 정리가 매우 중요하기 때문에 클래스를 나눠서 체계적으로 관리하는 것을 권장한다.
응집도
응집도 : 클래스에 속한 메서드와 변수가 서로 의존하며 논리적인 단위로 묶이는 것. 쉽게 말하면, 메서드들이 클래스 내 변수를 더 많이 사용하는 것을 의미한다.
이 때, 이 응집도가 낮다면 그것은 클래스를 분리해야한다는 신호이다. 큰 함수를 여러 작은 함수로 쪼개 듯, 큰 클래스도 여러 작은 클래스로 나누어라. 그러면서 프로그램은 점점 더 체계가 잡히고 구조가 투명해진다.
* 단일 책임 원칙과 동일하게 느껴진다. 큰 클래스 내에서 하나인 줄 알았던 책임이 다시 여러 개로 나눌 수 있음을 알게된다. 그리고 나누면 이 클래스는 하나의 책임을 지니는 매우 명확한 클래스가 된다.
* 예시가 굉장히 인상 깊었다. 커누스 교수의 Literate Programming에 나오는 예제라 한다. 맨 끝에 첨부해 두겠다.
변경하기 쉬운 클래스
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문을 수정하려는 코드를 새로 추가할 때, Sql 클래스를 손대어 고쳐야한다.
이는 다른 코드를 망가뜨릴 수 있는 잠재적인 위험이 존재한다.
이를 아래처럼 파생클래스를 이용하여서 수정하면 Sql 클래스는 수정하지 않고 추가/ 수정 시 관련 클래스를 만들거나 관련 파생 클래스만 수정하면 되어서 매우 수정이 간단해진다.
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()
}
이런 설계는 OCP(개방-폐쇄 원칙)를 지키며 수정을 매우 쉽게 만들어준다.
* 이 부분에서 굉장히 감탄했다. 각 원칙은 정말 서로서로를 잘 이어준다는 느낌을 받았고, 어떻게 활용되는 지 체감되니 이렇게 쓰고 싶었다.
시스템 결합도를 낮춰라
결합도 : 시스템 요소가 다른 요소와 다른 요소의 변경으로부터 잘 격리되어 있다는 것
TokyoStockExchange라는 외부 API가 있고 이를 통해서 Portfolio 클래스에서 값을 계산한다 가정하자.
이 TokyoStockExchange는 5분마다 다른 값을 주므로 이를 통해서 테스트하기는 쉽지 않다.
근데 이 Portfolio 클래스에서 TokyoStockExchange 대신 현재 값을 가져오는 StockExchange 인터페이스를 사용하자. (구체적인 값이 아니라, "현재 값"으로 추상화되었다.)
public insterface StockExchange {
Money currentPrice(String symbol);
}
public Portfolio {
private StockExchange exchange;
public Portfolio(StockExchange exchange) {
this.exchange = exchange;
}
// ...
}
이 경우 현재 값으로 추상화 되었으므로, 테스트를 하기 쉬워진다. 이렇게 결합도를 낮춘다면, 유연성과 재사용성이 높아진다.
그리고 이렇게 결합도를 최소로 줄이면, DIP(의존 역전 원칙)을 따르는 클래스가 만들어진다. 상세 구현이 아니라 추상화에 의존함으로써 시스템이 더 유연해졌다.
결론
이번 장이 진짜 가치 있고 뜻깊은 예제들이 많았다.
특히, 나는 객체지향을 외웠구나 하는 느낌을 많이 받았고 이제야 개념을 이해하는구나 느낌이 왔다.
사실상 나는 JS만 다루고 있어서 이 예시를 활용한 일이 생길지는 모르겠다. (이러고 풀스텍이 될 수도 있고...) 하지만, S.O.L.I.D 원칙을 최대한 지키면서 코드를 짜볼 거 같다. 추상화를 잘하고 코드를 깔끔하게 짜는 부분에서 이 원칙이 객체지향과 클래스에서만 작용되기는 너무 아까운 거 같다. (L은 사실 잘 모르겠다. 더 공부해보면 알겠지)
추가로, 요즘IT에서 객체지향과 JS에 대한 글을 적었는데 인상깊어서 달아둔다. 이 글과 같이 JS와 Java는 달라서, 완전히 적용할 수는 없을 거 같다.
https://yozm.wishket.com/magazine/detail/1396/
자바스크립트에서 객체지향을 하는 게 맞나요? | 요즘IT
이번 글에서는 객체지향 프로그래밍에 대해 이야기를 해보려고 합니다. 그리고 자바스크립트의 객체지향은 일반적인 객체지향 프로그래밍과는 어떻게 다른지 그리고 Javascript에서는 객체지향
yozm.wishket.com
추가 첨부 : 커누스 교수의 Literate Programming
// 리팩터링 전
package literatePrimes;
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;
public class PrimePrinter {
public static void main(String[] args) {
final int NUMBER_OF_PRIMES = 1000;
int[] primes = PrimeGenerator.generate(NUMBER_OF_PRIMES);
final int ROWS_PER_PAGE = 50;
final int COLUMNS_PER_PAGE = 4;
RowColumnPagePrinter tablePrinter =
new RowColumnPagePrinter(ROWS_PER_PAGE,
COLUMNS_PER_PAGE,
"The First " + NUMBER_OF_PRIMES + " Prime Numbers");
tablePrinter.print(primes);
}
}
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;
}
}
'책과 강연 > 클린 코드' 카테고리의 다른 글
클린 코드 11장: 시스템 (0) | 2024.01.01 |
---|---|
클린코드 9장 : 단위 테스트 (1) | 2023.12.29 |
클린 코드 8장 : 경계 (1) | 2023.12.29 |
클린코드 7장 : 오류 처리 (0) | 2023.12.28 |
클린코드 6장 : 객체와 자료구조 (0) | 2023.12.24 |