Clean Code 3판을 읽고 정리한 글입니다
Ⅲ. 함수
함수
- 프로그래밍 초창기 : 시스템을 루틴과 하위 루틴으로 나눔
- 포트란, PL/1 시절 : 프로그램, 하위 프로그램, 함수로 나눔
- 현재는 함수만 살아남음 : 가장 기본적 단위
함수 잘만드는 규칙
작게 만들어라
- 첫째도 작게. 둘째도 더 작게!
- 블록과 들여쓰기
- if/while등에 들어가는 블록은 단 한줄이어야 한다
- 대게 여기서 함수 호출
- wraping하는 enclosing func가 작아짐
- 블록안의 함수명이 적절하면 이해도 상승
- 함수의 들여쓰기는 1단이나 2단을 넘어서면 안된다
한가지만 해라!
함수에 대한 선배들의 충고
- 함수는 한 가지를 해야 한다
- 그 한가지를 잘해야 한다
- 그 한가지만을 해야한다
한가지의 범위 : TO
- ex) To RenderPageWithSetupsAndTeardowns
- 페이지가 테스트 페이지인지 확인
- 테스트 페이지면 설정,해제 페이지를 넣는다
- 테스트 페이지임에 상관없이 HTML 렌더링
- 지정된 함수 이름 아래 추상화 수준이 한단계 : 한 가지 작업만 하는 함수
- 섹션이 나누어지는 함수(선언,초기화,로직등)은 여러 작업을 한다는 증거
함수당 추상화 수준은 하나
- 함수 내 모든 문장의 추상화 수준이 동일해야 한다
- 다른 추상화 수준이 섞여 있으며 읽기에 헷갈림
- 내려가기 규칙
- 위에서 아래로 이야기처럼 읽혀야 좋다
- 위에서 아래로 읽으면 함수 추상화가 한번에 한 단계씩 낮아짐
Switch문 처리
siwtch문은 작게 만들기 어려움
- 본질적으로 N가지 처리
- 블행히도 switch문을 완전히 피할 방법도 없음
- 예제 : 직원 유형에 따라 다른 값을 계산해 반환하는 함수
Payroll.java 1
2
3
4
5
6
7
8
9
10
11
12
13
14public Money calculatePay(Employee e)
throws InvalidEmployeeType {
switch (e.type) {
case COMMISSIONED:
return calculateCommissionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
return new InvalidEmployeeType(e.type);
}
}- 문제점
- 함수가 길다. 새 회원 유형이 추가되면 더 길어짐
- ‘한 가지’작업만 수행하지 않음
- SRP(Single Responsibility Principal) 위반 : 코드 변경 이유가 여럿
- OCP(Open Closed Principal) 위반 : 새회원 유형 추가할 떄마다 코드 변경
- 위함수와 구조가 동일한 함수가 무한정 존재
isPayday(Employee e, Date date);
deliverPay(Employee e, Money pay);
- 해결 방법 : 다형성 객체를 생성하는 코드로 상속관계로 숨긴다
- switch문을 추상 팩토리에 숨기고 아무에게 보여주지 않음
- 팩토리는 switch문을 이용해 적절한 Employee 파생 클래스의 인스턴스 생성
- 함수들(calculatePay, isPayday, deliverPay)는 Employee 인터페이스를 거쳐 호출 → 다형성(polymorphism)으로 실제 파생 클래스의 함수가 실행됨
- 문제점
1 | public abstract class Employee { |
서술적인 이름을 사용하라
- 좋은 이름이 주는 가치는 매우 중요
- 길고 서술적인 이름이 짧고 어려운 이름보다 좋음
- 길고 서술적인 이름 > 길고 서술적인 주석
- 좋은 이름을 고르고 코드를 더 좋게 재구성한 사례도 존재함
- 일관성 있는 네이밍
- 모듈내의 함수 이름은 같은 문구/명사/동사 사용
함수 인수
이상적 인수 : 0개
- 3개: 가능한 피하라, 4개: 특별한 이유가 필요, 5개 특별한 이유가 있어도 안됨
인수는 어렵다
- 개념을 이해하기 어렵게 만듬
- 함수 이름과 인수 사이에 추상화 수준이 다름
- 코드 읽는 사람이 인수까지 파악해야함
테스트 관점 더 어려움
- 인수가 3개 이상이면 유효한 값으로 모든 조합 구성해 테스트하기 부담스럽다
출력인수는 더 어렵다
- 개발자는 함수에 인수로 입력을 넘기고 반환으로 출력을 받는것에 익숙
최선은 0개의 인수, 차선은 1개의 인수
많이 쓰는 단항 형식
- 가장 흔한 경우
- 인수에 질문을 던지는 경우
- 인수를 근거해서 먼가를 변환해 결과를 반환
- 흔하지 않지만 유용한 경우 : 이벤트
- 이벤트는 입력 인수만 있고 출력 인수가 없음
- 이벤트는 이름과 문맥 주의해서 선택 : 이벤트라는 사실이 코드에 명확하게
- 이외에는 가급적 단항 함수는 피할 것
- 입력 인수를 변환하는 함수라면 변환 결과는 반환값으로 돌려줄 것
플래그 인수
- 추하다
- 함수로 부울값을 넘기는 관례는 끔찍
- 부울 값에 따라 하는 일이 다름 → 함수가 여러가지를 처리한다고 공표
2항 함수
- 2항 함수는 1항 함수보다 이해가 어려움
- 2항 함수가 적절한 경우 : 직교 좌표계 점
Point p = new Point(0,0)
assertEquals(expected, actual)
- 문제가 있음
- 바꿔쓰는 경우가 허다
- 자연적 순서가 없어서 인위적으로 기억해야 함
- 2항이 무조건 나쁘다는 소리가 아님
- 불가피하면 어쩔 수 없음
- 하지만 그만큼 위험이 따르는 부분이라는 것을 인지 필요
- 가능한 단항으로 바꾸도록 애써야 한다
3항 함수
- 2항보다 더 이해 어려움 - 신중히 고려 필요
assertEquals(expected, actual, message)
- 메시지 무시 하는 오버로딩도 있음
- expected 위치 자꾸 헷갈림
인수 객체
- 인수가 많다면 독자적인 클래스 변수로 선언?
- VO나 DTO → 결국 개념을 표현
인수 목록
- 3항이상의 가변 인수 의 경우 문제가 있음
- 동사와 키워드
- 함수의 의도, 인수의 순서, 의도 표현 - 좋은 함수 이름 필 수
- 단항 함수 : 동사/명사 쌍
- 함수 이름에 키워드 추가 하는 형식도 있음
assertExpectedEqualsActual(expected,actual)
: 인수 순서 기억 필요 없음
부수 효과를 일으키지마라
- Side effect는 거짓말 : 함수에서 1가지만 하겠다고 해놓고 다른 것도 함
- 많은 경우 시간적 결합(temporal coupling), 순서 종속성(order dependency) 초래
출력 인수
- 인수를 출력으로 쓰지말아라
- 기본적으로 우리는 인수를 함수 입력으로 해석
- 선언부를 보고 확인? 벌써 주춤 및 생산성 저하,
- 객체 지향 언어에서는 출력 인수를 사용할 필요가 거의 없다
- 이전 언어에는 출력인수가 불가피한 경우가 존재
- 객체 지향에서는 this가 존재
- ex)
public void appendFooter(StringBuffer report)
appendFooter(s);
: 출력을 인수에 넣었음- 다음과 같이 쓸 수 있도록 고치자 :
report.appendFooter()
- 함수에서(굳이) 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하자.
명령과 조회 분리 = CQS
- 함수는 커맨드(무언가를 수행)하거나 쿼리(무언가에 답)하거나 둘중 하나만 할 것
- 코드 자체가 일단 괴상해짐(책의 내용처럼)
- 함수는 하나만 해야 하는 SRP를 지킬 수 있겠고
- 사이드 이펙트도 최소화할 수 있음
오류 코드 보다는 예외 사용
명령 함수에서 오류코드 반환 → CQS 약간 위반
- 오류 코드 반환시 호출자는 오류 코드를 곧바로 처리해야 함 → 코드 복잡
- 예외를 사용하면 오류 처리가 원래 코드와 분리 → 코드가 깔끔해짐
try-catch 뽑아내기
- 추함 : 블록 자체가 코드 구조에 혼란을 주고 정상/오류 동작을 뒤섞는다
- 해당 블록을 별도 함수로 뽑아내자
- catch 블록도 별도로 뽑아야 한다
- 함수는 한가지 작업만 해야 한다
- 오류 처리도 ‘한 가지’ 작업에 속함
- 오류 처리 함수는 오류만 처리해야 함
- 함수에 try가 있다면 함수는 try로 시작해서 catch/finally로 끝나야 한다
Error.java 의존성 자석
- 오류코드를 반환한다 → (클래스,열거,변수 어떤 거든) 어디선가 오류 코드 정의한다
- Error.java : 에러 코드를 모아놓은 enum class 라고 가정 → 의존성 자석
- 다른 클래스에서 Error enum을 import
- Error enum이 변하면 Error를 사용하는 모든 클래스 재컴파일 필요
- 재컴파일/재배치 번거로움 → 새오류 코드 정의 싫어함 → 기존 코드 재 사용;
- 예외는
Exception클래스
에서 파생 → 재컴파일/재배치 없이 새 예외 클래스 추가 가능
반복하지 마라 : DRY(Don’t Repeat Yourself) 원칙
중복 = 모든 악의 근원
- 길이가 늘어남, 변경시 중복 된만큼 케어, 바쁘릴시 오류가 발생할 확율도 높음
- 많은 법칙/기법이 오직 중복을 없애거나 제어할 목적으로 존재
- RDMBS의 정규형 : 중복 제거
- OOP : 코드를 부모 클래스로 몰아넣음 → 중복 제거
- AOP, COP : 어떤 관점으로 보면 이들 모두 중복 제거 전략
- 하위 루틴 발명 이래로 모든 sw개발의 혁신은 중복을 제거하려는 지속적인 노력의 결과
구조적 프로그래밍
모든 함수와 함수 내 모든 블록에 입구와 출구는 하나만 존재해야 한다
- 함수는 return이 하나이어야 할 것
- 루프 안에서 break/continue 사용하면 안된다
- goto는 절대로 사용하지 말 것
위 규칙은 함수가 큰 경우에 한해서 상당한 이득
- 함수를 아주 작게 만든다면 간혹 return/break/continue 여러번 사용 무방
- 때로는 오히려 단일 입/출구 규칙보다 의도 표현이 용이
- goto는 큰함수에서만 의미 → 작은 함수에서 피해야 함
함수를 어떻게 짜는가?
sw작성 = 글짓기
- 글짓기
- 먼저 기록을 기록후 읽기 좋게 다듬는다
- 초안은 대게 서투르고 어수선
- 말을 다듬고 문장을 고치고 문단을 정리
- 제대로 작성될 때까지
- 함수 작성도 마찬가지
- 처음엔 길고 복잡, 들여쓰기 단계도 많고 중복된 루프와 긴 인수 목록
- 네이밍도 즉흥적, 중복 코드도 많음
- 서투른 위의 코드를 빠짐없이 테스트 하는 단위 테스트 케이스 작성
- 이후 코드 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거
- 이 와중에 코드는 항상 단위 테스트를 통과
- 최종적으로 이 챕터의 규칙을 따르는 함수를 결과물로.
- 처음부터 짜내지 않는다. 그게 가능한 사람도 없다
결론
DSL
- 특정 응용분야 시스템을 기술할 목적으로 설계된 도메인 특화언어
프로그래머 대가
- 시스템은 구현할 프로그램이 아니라 풀어갈 이야기로 여긴다
- 프로그램 언어를 수단으로 좀더 풍부하고 표현력이 강한 언어를 만들어 이야기를 풀어간다
- 재귀라는 기교로 시스템에 발생하는 동작은 그 도메인에 특화된 언어를 사용해 자신만의 이야기
3.7 규칙적용 코드 예시
1 | public class SetupTeardownIncluder { |
추가
SOLID - 너무 유명한 법칙인데 계속 드문드문한 법칙
SRP(Single Responsibility Principle)
- 단일 책임의 원칙
- THERE SHOULD NEVER BE MORE THAN ONE REASON FOR A CLASS TO CHANGE.
- 한 클래스는 하나의 기능을 가진다
- 클래스가 제공하는 모든 서비스는 그 하나의 책임을 수행하는데 집중되어야 한다
- 어떤 변화에 의해 클래스를 변경해야 하는 이유도 오직 하나뿐이어야 한다
- 적용시 책임영역이 확실 → 책임 변경에서 다른 책임의 변경의 연쇄를 막음
- 책임의 적절한 분배 → 코드 가독성 향상, 유지보수 용ㅇ이
- 다른 원리들을 적용하는 기초
- 리팩토링이 필요한 위험사항 해법은 직/간접적으로 SRP원리와 연관
- 리팩토리 근본정신(항상 코드를 최상으로 유지)도 항상 객체 책임을 최상의 상태로 분배한다는 것에서 비롯되므로
- 다른 것에 비해 단순하지만 실무에서 직접 적용해서 설계가 그리 쉽지는 않음
- 의도적인 많은 연습과 경험이 필요
- 무조건적인 책임 분리가 SRP가 적용되는 것이 아니다
- 객체간의 응집력(cohesion)이 있다면 병합이 순작용의 수단 - 강 응집력 지향
- 결합력(coupling)이 있다면 분리가 순 작용의 수단 - 약 결합력 지향
OCP(Open Close Principal)
- 개방폐쇄의 원칙
- YOU SHOULD BE ABLE TO EXTEND A CLASSES BEHAVIOR, WITHOUT MODIFYING IT.
- sw 요소는 확장에 열려있고 변경에 닫혀있어야 한다
- 객체지향 소프트웨어 설계라는 책에서 정의됨 - 1998, 버틀란트 메이어(Bertrand Meyer)
- sw요소 : 컴포넌트, 클래스, 모듈, 함수
- 변경을 위한 비용은 최소화, 확장을 위한 비용은 극대화
- 객체지향의 장점을 극대화하는 아주 중요한 원리
- 요구사항 변경이나 추가 사항시
- 기존 요소는 수정이 일어나지 말아야한다
- 기존 구성요소를 쉽게 확장해서 재사용할 수 있어야 한다
- 밥아저씨
- OCP는 관리 가능하고 재사용 가능한 코드를 만드는 기반
- OCP를 가능하게 하는 중요 매커니믐 : 추상화, 다형성
LSP (The Liskov Substitution principle)
- 리스코프 치환원칙
- FUNCTIONS THAT USE POINTERS OR REFERENCES TO BASE CLASSES MUST BE ABLE TO USE OBJECTS OF DERIVED CLASSES WITHOUT KNOWING IT.
- 서브 타입은 언제나 기반 타입으로 교체 가능해야 함
- 서브 타입은 기반 타입이 약속한 규약을 포함하고 지켜야함
- 클래스의 상속, 인터페이스 상속 이용해 확장성 획득
- 다형성/확장성을 극대화하기 위해 인터페이스를 사용하는 것이 좋다
- 상속을 통한 재사용은 기반/서브 사이에 IS-A 관계일 경우로만 제한 권장
- 부모클래스를 작성한 개발자가 세웠던 가정, 추론 과정, 원리를 정확히 이해 필요
이는 자식 클래스가 부모클래스에게 강하게 결합된 의미 - 상속은 부모와 자식 클래스의 결합도가 매우 높음
- 상속은 추상화를 이용을 해야 위의 문제가 해결됨
- 자식의 클래스가 부모 클래스의 구현이 아닌 추상화에 의존하도록
- 합성(컴포지션) 사용 가능 : 이펙티브 자바 18번 아이템
- 구글에서 “이펙티브 자바 18” 검색
- 데코레이터 패턴 처럼
ISP (Interface Segregation Principle)
- 인터페이스 분리 원칙
- CLIENTS SHOULD NOT BE FORCED TO DEPEND UPON INTERFACES THAT THEY DO NOT USE.
- 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다
- 가능한 최소한의 인터페이스만 구현
- 특정 클래스 이용하는 클라이언트가 여러 개이며 각각 클래스의 특정 부분만 이용?
→ 여러 인터페이스로 분류해서 클라이언트가 필요한 기능만 전달한다 - SRP가 클래스의 단일 책임이라면? ISP는 인터페이스의 단일 책임
DIP (Dependency Inversion Principle)
- 의존성 역전의 법칙
- A. HIGH LEVEL MODULES SHOULD NOT DEPEND UPON LOW LEVEL MODULES. BOTH SHOULD DEPEND UPON ABSTRACTIONS.
- B. ABSTRACTIONS SHOULD NOT DEPEND UPON DETAILS. DETAILS SHOULD DEPEND UPON ABSTRACTIONS.
- 상위 모델은 하위 모델의 의존하면 안되며 둘다 추상화에 의존해야 한다
- 추상화는 세부사항에 의존해선 안되며 세부사항은 추상화에 따라 달라진다
- 하위 모델의 변경이 상위 모듈의 변경을 요구하는 위계 관계를 끊는다
- 실제 사용 관계는 그대로이나 추상화를 매개로 메시지를 주고 받으며 관계를 느슨하게
- 의존성 역전의 법칙
추가 참조
간결한 함수
- 분기는 가능한 추상 클래스 or Factory
안전한 함수 - Side Effect
함수 리팩터링
- 기능을 구현하는 서투른 함수를 작성
- 테스트 코드 작성
- 리팩터링
Related POST
- [Clean Code] Ⅰ. 깨끗한 코드
- [Clean Code] Ⅱ.의미 있는 이름
- [Clean Code] Ⅲ. 함수
- [Clean Code] Ⅳ. 주석
- [Clean Code] Ⅴ. 형식 맞추기
- [Clean Code] Ⅵ. 객체와 자료구조
- [Clean Code] Ⅶ. 오류 처리
- [Clean Code] Ⅷ. 경계
- [Clean Code] Ⅸ. 단위 테스트
- [Clean Code] Ⅹ. 클래스
- [Clean Code] Ⅺ. 시스템
- [Clean Code] Ⅻ. 창발성(創發性)
- [Clean Code] XIII. 동시성
- [Clean Code] XIV. 점진적 개선(SUCCESSIVE REFINEMENT)
- [Clean Code] XV. JUnit 들여다보기
- [Clean Code] XVI. SerialDate 리팩터링
- [Clean Code] XVII. 냄새와 휴리스틱
- [Clean Code] 다 읽었다~