| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- 프로그래머스
- 코딩테스트
- 기초
- 월부
- 세 개의 구분자
- 개발자북클럽
- 문자열이 몇 번 등장하는지 세기
- 첫번째로 나오는 음수
- 노마드코더
- 배열 만들기6
- 열반스쿨기초반
- 배열 만들기3
- 0 떼기
- 파이썬
- 풀스택gpt
- 프로그래머스 코딩테스트 기초
- 무작위로 k개의 수 뽑기
- 월급쟁이부자들
- Python
- 열기반
- 특별한 이차원 배열 1
- l로 만들기
- 주사위 게임1
- 부동산강의
- 노개북
- 프로그래머스 입문
- 월부닷컴
- 클린코드
- 특정 문자열로 끝나는 가장 긴 문자열 찾기
- 프로그래머스 기초
- Today
- Total
해쟈스토리
클린 코드 책읽기 day3 - 3장 함수 본문
요약
- 함수는 작게
- 한가지 일만 처리해라(플래그 인수 쓰지마라!)
- 내려가기 규칙(위에서 아래로 읽으면서 추상화가 한단계씩 낮아진다)으로!
- switch는 한가지만 할 수 없다.
- 서술적인 이름을 사용해라
- 함수 인수는 적은게 좋다(인수의 순서를 기억하는 것도 일이다)
- 부수효과 일으키지마라라(함수명을 보고 떠오르지 않는 일은 안하는게 맞다)
기억하고 싶은 내용
함수는 작게 만들어라
작게 만들어서 잘 읽히도록 해라. 중첩 구조가 생실만큼 함수가 커져서는 안된다. 함수에서 들여쓰기 수준은 1단이나 2단을 넘어서면 안된다. 읽고 이해하기 쉬워야한다.
한가지만 하는 함수로 만들어라
함수가 확실히 한가지 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야한다.
함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다. 특정 표현이 근본 개념인지 아니면 세부사항인지지 구분하기 어려운 탓이다. 하지만 문제는 이 정도로 그치지않는다. 근본 개념과 세부사항을 뒤섞기 시작하면, 깨어진 창문처럼 사람들이 함수에 세부사항을 점점 더 추가한다.
위에서 아래로 내려가기
위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한번에 한 단계씩 낮아진다.
switch는 한가지만 할 수 없다.
case가 두개인 switch문도 너무 길다. 본질적으로 switch 문은 N가지를 처리하기 때문에 한가지 작업을 하기 어렵다.
switch문을 저차원 클래스에 숨기고 절대로 반복하지 않는 방법은 있다.
switch문을 추상 팩토리에 꽁꽁 숨긴다. 팩토리는 switch 문을 사용해 적절한 Employee 파생 클래스의 인스턴스를 생성한다.
calculatePay, isPayday, deliverPay 등과 같은 함수는 Employee 인터페이스를 저쳐 호출된다. 그러면 다형성으로 인해 실제 파생 클래스의 함수가 실행된다.
서술적인 이름을 사용해라
includesetupAndTeardownPages 등과 같이 서술적인 이름을 지었다. 함수가 작고 단순할수록 서술적인 이름을 고르기도 쉬워진다. 이름이 길어도 괜찮다. 겁먹을 필요없다. 길고 서술적인 이름이 짧고 어려운 이름보다 좋다. 길고 서술적인 이름이 길고 서술적인 주석보다 좋다.
이름을 붙일때는 일관성이 있어야한다. 모듈내에에서 함수 이름은 같은 문구, 명사, 동사를 사용한다. incluseSetupPages, includeSuiteSetupPage 등... 문젳가 비슷하면 이야기를 순차적으로 풀어가기도 쉬워진다.
함수 인수
함수의 인수는 많지 않도록해라. 이상적인것은 0개. 인수가 3개를 넘어가면 인수마다 유효한 값으로 모든 조합을 구성해 테스트하기가 상당히 부담스러워 진다.
함수의 매개변수로 받은 데이터 자체를 변경하려고 하지말고 변경된 것을 return해서 그 값을 받아 변경하는 형식으로 해야된다.(불변성 유지)
플래그 인수
함수한테 부울값을 넘기는 것은 안된다. 함수가 한꺼번에 여러가지 처리한다고 대놓고 공표하는 셈이니까!
플래그가 참이면 이걸하고 거짓이면 저걸하겠다는 말이니까.
def process_order(order, is_priority):
if is_priority:
print("우선 처리")
else:
print("일반 처리")
flag 인수를 통해 함수 내부에서 분기처리를 하면 호출하는 쪽에서는 내부 로직을 보기 전까지는 함수의 의도파악이 힘들다.
조건에 따라서 다른 분기처리가 필요하다면 함수 외부에서 진행하고 그에 따라 로직함수를 실행할 수 있도록하자
def process_priority_order(order):
print("우선 처리")
def process_reqular_order(order):
print("일반 처리")
if is_priority:
process_priority_order(order)
else:
process_reqular_order(order)
이항 함수
인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어렵다. 따라서 인수를 줄이도록 노력하자.
물론, 이항 함수가 적절한 경우도 있다. point(0,0) 가 좋은 예이다. 좌표로서 x, y를 받는 것은 당연하다. 두 요소에는 자연적인 순서가 있다. 그러나 대부분의 함수의 인수는 자연적인 순서가 있지 않다.
assertEquals(expexted, actual)에도 문제가 있다. expected인수에 actual 값을 집어넣는 실수가 얼마나 많던가?
우리는 인수의 순서를 인위적으로 기억해야한다.
이항 함수가 무조건 나쁘다는 것은 아니다. 다만 그만큼 위험이 따른다는 사실을 이해해야한다.
인수 객체
인수가 2-3개 필요하다면 일부를 독자적인 클래스 변수로 선언할 가능성을 짚어본다.
객체를 생성해 인수를 줄이는 방법이 눈속임이라 여겨질지 모르지만 그렇지않다.
함수 이름에 인수를 추가하는 형식으로로 assertEquals보다 assertExpectedEqualsActual(expected, actual)이 더 좋다. 이렇게 짓게 되면 인수 순서를 기억할 필요가 없어진다.
부수효과를 일으키지마라
checkPassword 라는 함수가 있다. 이 함수명만 보면 패스워드를 확인하는 구나 알 수 있다. 그런데 이 함수안에 세션이 초기화되는 기능이 들어 있다면? 함수 호출자는 생각지도 못했는데 기존 세션 정보를 지워버리게 된다.
그래서 checkPasswordAndInitionlizeSession이라는 이름이 더 좋다.
물론 함수가 한가지만 한다는 규칙을 어기게 된다만.
출력인수
일반적으로 출력 인수는 피해야한다. 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택한다.
appendFooter(s)
이 항수는 무언가에 s를 바닥글로 첨부하는 것인지, s에 바닥글을 첨부하는 것인지 모호할 수 있다.
차라리 아래와 같이 되도록 하자
report.appendFooter(s)
이전에 결합도 관련한 글에서 order.total_price 뭐 이런 매개변수가 있었는데 그냥 order 객체를 넘겨주는게 낫다.
명령과 조회를 분리하라
함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다. 둘 다 하면 안된다. 객체 상태를 변경하거나 아니면 객체 정보를 반환하거나 줄 중 하나다. 둘 다 하면 혼란을 초래한다.
if set("username", "unclebob"):
pass
if문에 이렇게 있으니 반환값이 true인가? 근데 무슨뜻이지 싶다.
if attributeExists("username"):
setAttribute("username", "unclebob")
위와 같이 명령과 조회를 분리해 혼란을 뿌리뽑자
오류 코드보다 예외를 사용하라
if deletePage(page) == E_OK:
if registry.deleteReference(page.name) == E_OK:
if configKeys.deleteKey(page.name.makeKey()) == E_OK:
logger.log("page deleted")
else:
logger.log("configKey not deleted")
else:
logger.log("deleteRefence from registry failed")
else:
logger.log("delete failed")
return E_ERROR
위와 같이 오류코드를 반환하는 방식은 여러 단계로 중첩되는 코드를 야기한다. 오류 코드를 반환하면 호출자는 오류코드를 곧바로 처리해야 한다는 문제에 부딪힌다.
try:
deletePage(page)
registry.deleteRefernce(page.name)
configKeys.deleteKey(page.name.makeKey())
except Exception as e:
logger.log(e.getMessage())
반면 오류 코드 대신 예외를 사용하면 오류처리 코드가 원해 코드에서 분리되므로 코드가 깔끔해진다.
python으로 소스코드를 변환해서 보자
class Page:
def __init__(self, name):
self.name = name
def make_key(self):
return f"key_{self.name}"
class Registry:
def delete_reference(self, name):
print(f"Deleting reference for {name}")
class ConfigKeys:
def delete_key(self, key):
print(f"Deleting key {key}")
class Logger:
def log(self, message):
print(f"Logging: {message}")
registry = Registry()
config_keys = ConfigKeys()
logger = Logger()
def delete(page):
try:
delete_page_and_all_references(page)
except Exception as e:
log_error(e)
def delete_page_and_all_references(page):
delete_page(page)
registry.delete_reference(page.name)
config_keys.delete_key(page.name.make_key())
def delete_page(page):
print(f"Deleting page {page.name}")
def log_error(e):
logger.log(str(e))
# 사용 예
page = Page("ExamplePage")
delete(page)
이 구조의 장점은 아래와 같다.
1. 관심사의 분리:
- `delete` 함수는 오직 오류 처리만 담당합니다.
- `delete_page_and_all_references` 함수는 실제 삭제 로직을 담당합니다.
2. 가독성 향상:
- `delete` 함수를 보면 전체적인 흐름을 쉽게 파악할 수 있습니다. "페이지를 삭제하려고 시도하고, 실패하면 오류를 기록한다"라는 의도가 명확합니다.
- `delete_page_and_all_references` 함수는 오류 처리 없이 순수하게 삭제 로직만 포함하고 있어, 실제 동작을 이해하기 쉽습니다.
3. 유지보수성 향상:
- 오류 처리 로직을 변경하고 싶다면 `delete` 함수만 수정하면 됩니다.
- 삭제 로직을 변경하고 싶다면 `delete_page_and_all_references` 함수만 수정하면 됩니다.
4. 단일 책임 원칙:
- 각 함수가 하나의 책임만 가집니다. `delete`는 오류 처리, `delete_page_and_all_references`는 삭제 로직, `log_error`는 오류 기록을 담당합니다.
5. 오류 전파:
- `delete_page_and_all_references` 함수는 오류를 처리하지 않고 상위로 전파합니다. 이로 인해 `delete` 함수에서 모든 종류의 오류를 일관되게 처리할 수 있습니다.
이렇게 구조화하면, 코드의 각 부분이 무엇을 하는지 명확해지고, 수정이 필요할 때 어디를 수정해야 할지 쉽게 알 수 있습니다. 또한, 오류 처리 로직과 실제 비즈니스 로직이 분리되어 있어 각각을 독립적으로 테스트하고 수정할 수 있습니다.
오류처리도 한가지 작업이다. 위예제에서 보았듯이 오류를 처리하는 함수는 모류만 처리해야 마땅하다.
오류처리를 반환이 아닌 예외로 할때
from enum import Enum
class Error(Enum):
OK = 0
INVALID = 1
NO_SUCH = 2
LOCKED = 3
OUT_OF_RESOURCES = 4
WAITING_FOR_EVENT = 5
def some_function():
# 어떤 작업 수행
return Error.OK
def another_function():
result = some_function()
if result == Error.OK:
print("작업 성공")
else:
print(f"오류 발생: {result}")
이 방식의 문제점:
1. `Error` 열거형에 새로운 오류를 추가하면, 이 열거형을 사용하는 모든 코드를 수정해야 할 수 있습니다.
2. 개발자들이 새로운 오류 코드를 추가하는 것을 꺼리게 되어, 기존의 오류 코드를 부적절하게 재사용할 수 있습니다.
이제 예외를 사용하는 방식을 보겠습니다:
class CustomError(Exception):
pass
class InvalidError(CustomError):
pass
class NoSuchError(CustomError):
pass
class LockedError(CustomError):
pass
def some_function():
# 어떤 작업 수행
raise NoSuchError("요청한 항목이 존재하지 않습니다.")
def another_function():
try:
some_function()
print("작업 성공")
except CustomError as e:
print(f"오류 발생: {str(e)}")
# 나중에 새로운 예외 추가
class NewSpecificError(CustomError):
pass
이 방식의 장점:
1. 새로운 예외 클래스(예: `NewSpecificError`)를 추가해도 기존 코드를 변경할 필요가 없습니다. `CustomError`를 상속받는 모든 예외는 `except CustomError`에서 처리됩니다.
2. 예외는 더 자세한 정보를 제공할 수 있습니다. 오류 메시지, 스택 트레이스 등을 포함할 수 있습니다.
3. 예외는 계층 구조를 가질 수 있어, 더 세밀한 오류 처리가 가능합니다.
예외를 사용하면:
1. 코드의 흐름을 방해하지 않고 오류를 처리할 수 있습니다.
2. 오류 처리 로직과 일반 로직을 분리할 수 있습니다.
3. 새로운 오류 유형을 쉽게 추가할 수 있습니다.
소감
여태까지 정말 의식의 흐름대로 함수를 짠거 같다. 맨날 어느정도로 나눠야하ㄷ나 이건 너무 짧은데 함수로 해야되나 싶은것들이 있었는데 왜 그래야하는지 알겠다.
'파이썬 > 클린코드' 카테고리의 다른 글
| 데코레이터를 사용한 코드 개선 (0) | 2024.09.06 |
|---|---|
| 클린 코드 책읽기 day4 - 4장 주석 (1) | 2024.08.30 |
| 클린 코드 책읽기 day3 - 2장 의미있는 이름 (1) | 2024.08.27 |
| 클린 코드 책읽기 day2 - 1장 깨끗한 코드 (0) | 2024.08.27 |
| 클린코드 책 읽기 day1 (0) | 2024.08.24 |