1.1 자, 시작해보자!
우선, 연극을 외주로 받아서 공연하는 극단 예시가 처음 나온다.
극단에서 필요한 충성도 시스템을 다음과 같이 구현해 보았다. 원래는 자바스크립트인데, 파이썬으로 앞으로 하고 싶어서 파이썬으로 구현해보았다.
{
"hamlet": {"name": "Hamlet", "type": "tragedy"},
"as-like": {"name": "As You Like It", "type": "comedy"},
"othello": {"name": "Othello", "type": "tragedy"}
}
[
{
"customer": "BigCo",
"performances": [
{
"playID": "hamlet",
"audience": 55
},
{
"playID": "as-like",
"audience": 35
},
{
"playID": "othello",
"audience": 40
}
]
}
]
import json
with open("plays.json", "r") as f:
plays = json.load(f)
with open("invoices.json", "r") as f:
invoices = json.load(f)
def statement(invoice, plays):
total_amount = 0
volume_credits = 0
result = f"청구 내역 (고객명: {invoice['customer']})\n"
cur_format = "${:,.2f}"
for perf in invoice['performances']:
play = plays[perf['playID']]
this_amount = 0
if play['type'] == "tragedy":
this_amount = 40000
if perf['audience'] > 30:
this_amount += 1000 * (perf['audience'] - 30)
elif play['type'] == "comedy":
this_amount = 30000
if perf['audience'] > 20:
this_amount += 10000 + 500 * (perf['audience'] - 20)
this_amount += 300 * perf['audience']
else:
raise Exception(f"알 수 없는 장르: ${play['type']}")
volume_credits += max(perf['audience'] - 30, 0) # 포인트를 적립한다
if play['type'] == "comedy":
volume_credits += perf['audience'] // 5 # 포인트를 적립한다
result += f" {play['name']}, {cur_format.format(this_amount/100)}, ({perf['audience']}석)\n"
total_amount += this_amount
result += f"총액: {cur_format.format(total_amount/100)}\n"
result += f"적립 포인트: {volume_credits}점\n"
return result
def main():
result = statement(invoices[0], plays)
print(result)
if __name__ == '__main__':
main()
1.2 예시 프로그램을 본 소감
프로그램이 새로운 기능을 추가하기에 편한 구조가 아니라면, 먼저 기능을 추가하기 쉬운 형태로 리팩터링 하고 나서 원하는 기능을 추가해야 한다.
위 코드의 경우 다음 두 가지 변경사항이 있을 수 있다.
- 청구 내역을 HTML로 출력하는 기능이 필요하다.
- 새로운 요구 사항(다양한 장르...)을 추가하기 쉬워야 한다.
1.3 리팩터링의 첫 단계
리팩터링하기 전에 제대로 된 테스트부터 마련해야한다. 테스트는 반드시 자가진단하도록 만든다.
1.4 statement() 함수 쪼개기
- 위 코드에서 switch문에 마음에 안드신다고 한다. 그래서 이를 별도의 함수로 추출하는 방식으로 바꿀 것이다. 이러한 과정은 이후에 함수 추출하기에서 더 절차적으로 할 수 있게 기록했다고 한다.
- 함수를 별도로 빼내었을 때, 범위에 문제가 생기는 변수는 perf, play, this_amount가 있다.
- 여기서 perf, play는 값을 변경하지 않기 때문에 매개변수로 이용할 수 있다.
- this_amount의 경우에는 값이 변경되기 때문에 조심히 다뤄야하는데, 여기서는 return하도록 작성했다.
def amount_for(perf, play):
this_amount = 0
if play['type'] == "tragedy":
this_amount = 40000
if perf['audience'] > 30:
this_amount += 1000 * (perf['audience'] - 30)
elif play['type'] == "comedy":
this_amount = 30000
if perf['audience'] > 20:
this_amount += 10000 + 500 * (perf['audience'] - 20)
this_amount += 300 * perf['audience']
else:
raise Exception(f"알 수 없는 장르: ${play['type']}")
return this_amount
def statement(invoice, plays):
...
for perf in invoice['performances']:
play = plays[perf['playID']]
this_amount = amount_for(perf, play)
...
- 이렇게 변경하고 실행을 반드시 해봐야 한다.
리팩터링은 프로그램 수정을 작은 단계로 나눠서 진행한다. 그래서 중간에 실수하더라도 버그를 쉽게 찾을 수 있다.
- 위 예시의 경우 중첩함수를 만들면 매개변수를 전달할 필요가 없어서 일반적으로 더 편하다고 한다.
- amount_for()에서 변수명 this_amount를 result로 바꾸는 것도 좋다고 한다. 결과는 항상 result로 한다는 저자의 의견.
- perf의 경우 aPerformance로 바꾸는 것이 더 좋다고 한다.
- 동적 타입 언어를 사용할 때에는 변수명에 타입이 드러나게 작성하면 좋고, 역할이 뚜렷하지 않을 때에는 부정관사(a/an)을 붙이는 것도 좋다고 한다.
컴퓨터가 이해하는 코드는 바보도 작성할 수 있다. 사람이 이해하도록 작성하는 프로그래머가 진정한 실력자다.
play 변수 제거하기
- perf(aPerformance)와 달리 play 변수는 매개변수로 전달할 필요가 없다. 그냥 함수 안에서 다시 계산해도 된다.
- 이를 해결해주는 리팩터링으로는 '임시 변수를 질의 함수로 바꾸기'가 있다고 한다.
def play_for(aPerformance):
return plays[aPerformance['playID']]
def statement(invoice, plays):
...
for perf in invoice['performances']:
play = play_for(perf)
this_amount = amount_for(perf, play)
volume_credits += max(perf['audience'] - 30, 0) # 포인트를 적립한다
if play['type'] == "comedy":
volume_credits += perf['audience'] // 5 # 포인트를 적립한다
result += f" {play['name']}, {cur_format.format(this_amount/100)}, ({perf['audience']}석)\n"
total_amount += this_amount
...
이제 play 변수를 다음과 같이 제거할 수 있다. (그런데 계산은 늘어난 거 아닌가..?)
def statement(invoice, plays):
...
for perf in invoice['performances']:
this_amount = amount_for(perf, play_for(perf))
volume_credits += max(perf['audience'] - 30, 0) # 포인트를 적립한다
if play_for(perf)['type'] == "comedy":
volume_credits += perf['audience'] // 5 # 포인트를 적립한다
result += f" {play_for(perf)['name']}, {cur_format.format(this_amount/100)}, ({perf['audience']}석)\n"
total_amount += this_amount
...
그리고 amount_for함수도 변경이 가능하다. 변경하고 나서는 호출하는 곳에서도 매개변수를 없애줘야 한다.
def amount_for(aPerformance):
result = 0
if plays(aPerformance)['type'] == "tragedy":
result = 40000
if aPerformance['audience'] > 30:
result += 1000 * (aPerformance['audience'] - 30)
elif plays(aPerformance)['type'] == "comedy":
result = 30000
if aPerformance['audience'] > 20:
result += 10000 + 500 * (aPerformance['audience'] - 20)
result += 300 * aPerformance['audience']
else:
raise Exception(f"알 수 없는 장르: ${plays(aPerformance)['type']}")
return result
-
이렇게 해도 성능의 큰 차이는 없다고 한다. 그리고 이렇게 한 것이 나중에 개선하기가 더 쉽다고 한다.
-
지역 변수를 제거해서 얻는 가장 큰 이점은 추출 작업이 훨씬 쉬워진다는 것이다.
-
이제는 "변수 인라인하기"라는 작업을 할 것이다. this_amount라는 변수는 한번 설정되고 변경이 되지 않아서 하면 좋다고 한다.
- this_amount를 전부 play_for(perf)로 바꾸었다.
def statement(invoice, plays):
total_amount = 0
volume_credits = 0
result = f"청구 내역 (고객명: {invoice['customer']})\n"
cur_format = "${:,.2f}"
for perf in invoice['performances']:
volume_credits += max(perf['audience'] - 30, 0) # 포인트를 적립한다
if play_for(perf)['type'] == "comedy":
volume_credits += perf['audience'] // 5 # 포인트를 적립한다
result += f" {play_for(perf)['name']}, {cur_format.format(amount_for(perf)/100)}, ({perf['audience']}석)\n"
total_amount += amount_for(perf)
result += f"총액: {cur_format.format(total_amount/100)}\n"
result += f"적립 포인트: {volume_credits}점\n"
return result
적립 포인트 계산 코드 추출하기
- perf와 volume_credits를 아직 처리해줘야하는데, volume_credits은 값이 계속 누적되어서 더 까다롭다고 한다.
- 이 상황에서 최선의 방법은 volume_credits의 복제본을 초기화한 뒤 계산 결과를 반환하게 하는 것이다.
def volume_credits_for(aPerformance):
volume_credits = 0
volume_credits += max(aPerformance['audience'] - 30, 0)
if play_for(aPerformance)['type'] == "comedy":
volume_credits += aPerformance['audience'] // 5
return volume_credits
def statement(invoice, plays):
...
for perf in invoice['performances']:
volume_credits += volume_credits_for(perf) # 포인트를 적립한다
result += f" {play_for(perf)['name']}, {cur_format.format(amount_for(perf)/100)}, ({perf['audience']}석)\n"
...
format 변수 제거하기
- 앞서 설명했듯이, 임시 변수는 나중에 문제를 일으킬 수 있다. 임시 변수는 자신이 속한 루틴에서만 의미가 있어서 루틴이 길고 복잡해지기 쉽다. format은 이중에서 가장 만만해 보인다.
- 이처럼 함수 변수를 일반 함수로 변경하는 것도 리팩터링이다. 이런 것은 리팩토링 목록에 넣지 않았는데, 목록에 없는 리팩터링 기법도 많다.
def cur_format(aNumber):
return "${:,.2f}".format(aNumber)
def statement(invoice, plays):
total_amount = 0
volume_credits = 0
result = f"청구 내역 (고객명: {invoice['customer']})\n"
for perf in invoice['performances']:
volume_credits += volume_credits_for(perf) # 포인트를 적립한다
result += f" {play_for(perf)['name']}, {cur_format(amount_for(perf)/100)}, ({perf['audience']}석)\n"
total_amount += amount_for(perf)
result += f"총액: {cur_format(total_amount/100)}\n"
result += f"적립 포인트: {volume_credits}점\n"
return result
- 그런데, cur_format는 뭔가 함수의 역할을 정확히 말하지 않는다. 그렇다고 format_as_usd라기에는 너무 장황하므로 그냥 usd라고만 하자.
- 단위 변환 로직도 안에 넣었다.
def usd(aNumber):
return "${:,.2f}".format(aNumber / 100)
volume_credits 변수 제거하기
- "반복문 쪼개기"로 빼낼 수 있다고 한다.
def statement(invoice, plays):
total_amount = 0
volume_credits = 0
result = f"청구 내역 (고객명: {invoice['customer']})\n"
for perf in invoice['performances']:
result += f" {play_for(perf)['name']}, {usd(amount_for(perf))}, ({perf['audience']}석)\n"
total_amount += amount_for(perf)
for perf in invoice['performances']: # 값 누적 로직을 별도 for문으로 분리
volume_credits += volume_credits_for(perf)
...
- 이어서 "문장 슬라이드하기"를 적용해서 volume_credits 변수를 선언하는 문장 을 반복문 바로 앞으로 옮긴다.
volume_credits = 0 # 변수 선언(초기화)을 반복문 앞으로 이동
for perf in invoice['performances']:
volume_credits += volume_credits_for(perf)
- 이렇게 하고 나서는 앞에서 play 변수를 제거했던 것 처럼 "임시 변수를 질의 함수로 바꾸기"가 가능해진다. 이번에도 역시 "함수로 추출"해준다.
def total_volume_credits(invoice):
result = 0 # 변수 선언(초기화)을 반복문 앞으로 이동
for perf in invoice['performances']:
result += volume_credits_for(perf)
return result
def statement(invoice, plays):
total_amount = 0
result = f"청구 내역 (고객명: {invoice['customer']})\n"
for perf in invoice['performances']:
result += f" {play_for(perf)['name']}, {usd(amount_for(perf))}, ({perf['audience']}석)\n"
total_amount += amount_for(perf)
volume_credits = total_volume_credits(invoice)
result += f"총액: {usd(total_amount)}\n"
result += f"적립 포인트: {volume_credits}점\n"
return result
- 바로 "변수를 인라인"해보자
def statement(invoice, plays):
total_amount = 0
result = f"청구 내역 (고객명: {invoice['customer']})\n"
for perf in invoice['performances']:
result += f" {play_for(perf)['name']}, {usd(amount_for(perf))}, ({perf['audience']}석)\n"
total_amount += amount_for(perf)
result += f"총액: {usd(total_amount)}\n"
result += f"적립 포인트: {total_volume_credits(invoice)}점\n"
return result
- 여기서, 반복문을 쪼갠 것이 성능의 저하를 일으키지 않을까 걱정할 수 있지만, 성능의 저하는 대체로 미미하다고 한다. 똑똑한 컴파일러들은 최신 캐싱 기법 등으로 무장하고 있어서 직관을 초월하는 결과를 내어 준다고 한다.
- 그런데 대체로 미미한 것이지, 항상 그렇다고 생각하면 안된다. 어떨 때는 성능에 큰 영향을 준다고 한다. 하지만 저자는 그런 거에 신경 쓰지 않는다고 한다.
요약하자면, 다음과 같은 네 단계를 수행하면 voluime_credits를 제거할 수 있었다.
- "반복문 쪼개기"로 변수 값을 누적시키는 부분을 분리한다.
- "문장 슬라이드 하기" 로 변수 초기화 문장을 변수 값 누적 코드 바로 앞에 옮긴다.
- "함수 추출하기"로 적립 포인트 계산 부분을 별도 함수로 추출한다.
- "변수 인라인하기"로 voluime_credits변수를 제거한다.
이렇게 과정을 잘게 나누고, 테스트를 하고 커밋을 하면서, 테스트에 실패하면 가장 최근 커밋으로 돌아가는 방식으로 하면 된다고 한다.
total_amount도 이제 없어질 차례다.
- 갑자기 무슨 apple_sauce라는 함수를 만드는데... 나는 그냥 total_amount로 하려고 한다.
def total_amount(invoice):
result = 0
for perf in invoice['performances']:
result += amount_for(perf)
return result
def statement(invoice, plays):
result = f"청구 내역 (고객명: {invoice['customer']})\n"
for perf in invoice['performances']:
result += f" {play_for(perf)['name']}, {usd(amount_for(perf))}, ({perf['audience']}석)\n"
result += f"총액: {usd(total_amount(invoice))}\n"
result += f"적립 포인트: {total_volume_credits(invoice)}점\n"
return result
1.6 계산단계와 포매팅 단계 분리하기
HTML 버전을 만들고 싶은데, 텍스트 버전과 동일한 방식으로 만들고 싶다고 한다.
- 이럴 때 단계 쪼개기라는 방식으로 리팩터링을 한다.
- 단계를 쪼개려면 먼저 두 번째 단계가 될 코드들을 함수 추출하기로 뽑아내야 한다. 텍스트 버전의 코드를 추출하고, statement에 중간 데이터 구조를 추가하면 다음과 같다.
def render_plane_text(data, invoice, plays):
result = f"청구 내역 (고객명: {invoice['customer']})\n"
for perf in invoice['performances']:
result += f" {play_for(perf)['name']}, {usd(amount_for(perf))}, ({perf['audience']}석)\n"
result += f"총액: {usd(total_amount(invoice))}\n"
result += f"적립 포인트: {total_volume_credits(invoice)}점\n"
return result
def statement(invoice, plays):
statement_data = {}
return render_plane_text(statement_data, invoice, plays)
사실 render_plane_text에는 수많은 중첩함수가 있는 형태이다. 이것도 다 빼내야 한다. 이제, render_plane_text 함수에서 invoice를 없애보자.
def render_plane_text(data, plays):
...
def total_volume_credits(data):
result = 0 # 변수 선언(초기화)을 반복문 앞으로 이동
for perf in data['performances']:
result += volume_credits_for(perf)
return result
def total_amount(data):
result = 0
for perf in data['performances']:
result += amount_for(perf)
return result
result = f"청구 내역 (고객명: {data['customer']})\n"
for perf in data['performances']:
result += f" {play_for(perf)['name']}, {usd(amount_for(perf))}, ({perf['audience']}석)\n"
result += f"총액: {usd(total_amount(data))}\n"
result += f"적립 포인트: {total_volume_credits(data)}점\n"
return result
def statement(invoice, plays):
statement_data = {}
statement_data['customer'] = invoice['customer']
statement_data['performances'] = invoice['performances']
return render_plane_text(statement_data, plays)
이와 같이 변경해서 invoice 매개변수를 삭제할 수 있다.
그리고, 다음과 같이 해서 연극 정보를 data에 추가한다. 여기서, play_for 함수가 statement 함수로 옮겨졌다.
def statement(invoice, plays):
def play_for(aPerformance):
return plays[aPerformance['playID']]
def enrich_performances(aPerformance):
result = aPerformance
result['play'] = play_for(result)
return result
statement_data = {}
statement_data['customer'] = invoice['customer']
statement_data['performances'] = [enrich_performances(perf) for perf in invoice['performances']]
return render_plane_text(statement_data, plays)
그런 다음 render_plane_text의 모든 play_for 함수를 aPerformance['play']와 같이 변경한다.
amount_for, volume_credits_for 함수도 비슷한 방식으로 옮기고, total_amount와 total_volume_credits을 옮긴다.
중간 점검을 하면 다음과 같이 변하였다.
def render_plane_text(data, plays):
def usd(aNumber):
return "${:,.2f}".format(aNumber / 100)
result = f"청구 내역 (고객명: {data['customer']})\n"
for perf in data['performances']:
result += f" {perf['play']['name']}, {usd(perf['amount'])}, ({perf['audience']}석)\n"
result += f"총액: {usd(data['total_amount'])}\n"
result += f"적립 포인트: {data['total_volume_credits']}점\n"
return result
def statement(invoice, plays):
def play_for(aPerformance):
return plays[aPerformance['playID']]
def amount_for(aPerformance):
result = 0
if aPerformance['play']['type'] == "tragedy":
result = 40000
if aPerformance['audience'] > 30:
result += 1000 * (aPerformance['audience'] - 30)
elif aPerformance['play']['type'] == "comedy":
result = 30000
if aPerformance['audience'] > 20:
result += 10000 + 500 * (aPerformance['audience'] - 20)
result += 300 * aPerformance['audience']
else:
raise Exception(f"알 수 없는 장르: ${aPerformance['play']['type']}")
return result
def volume_credits_for(aPerformance):
result = 0
result += max(aPerformance['audience'] - 30, 0)
if aPerformance['play']['type'] == "comedy":
result += aPerformance['audience'] // 5
return result
def total_volume_credits(data):
result = 0 # 변수 선언(초기화)을 반복문 앞으로 이동
for perf in data['performances']:
result += perf['volume_credits']
return result
def total_amount(data):
result = 0
for perf in data['performances']:
result += perf['amount']
return result
def enrich_performances(aPerformance):
result = aPerformance
result['play'] = play_for(result)
result['amount'] = amount_for(result)
result['volume_credits'] = volume_credits_for(result)
return result
statement_data = {}
statement_data['customer'] = invoice['customer']
statement_data['performances'] = [enrich_performances(perf) for perf in invoice['performances']]
statement_data['total_amount'] = total_amount(statement_data)
statement_data['total_volume_credits'] = total_volume_credits(statement_data)
return render_plane_text(statement_data, plays)
이렇게 하니 가볍게 반복문을 파이프라인으로 바꾸기가 하고싶어졌다고 한다. 일단 파이프라인이 뭔지 모르겠다. reduce를 쓰는 모습을 보인다.
def total_volume_credits(data):
return reduce(lambda acc, cur: acc + cur['volume_credits'], data['performances'], 0)
def total_amount(data):
return reduce(lambda acc, cur: acc + cur['amount'], data['performances'], 0)
대충 이런식으로 바꾸는 것 같은데... 파이썬의 경우 reduce를 쓰는 것 보다 sum이 가독성 및 성능의 측면에서 더 우수하다. 람다를 사용하는 것도 그냥 함수 객체를 만드는 것과 동일하다고 하니 다음과 같은 방법을 사용하자.(전문가를 위한 파이썬에서 봄)
def total_volume_credits(data):
return sum(perf['volume_credits'] for perf in data['performances'])
def total_amount(data):
return sum(perf['amount'] for perf in data['performances'])
여기서, 제너레이터를 쓴 것은 너무나도 당연한 이치.
def create_statement_data(invoice, plays):
def play_for(aPerformance):
return plays[aPerformance['playID']]
def amount_for(aPerformance):
result = 0
if aPerformance['play']['type'] == "tragedy":
result = 40000
if aPerformance['audience'] > 30:
result += 1000 * (aPerformance['audience'] - 30)
elif aPerformance['play']['type'] == "comedy":
result = 30000
if aPerformance['audience'] > 20:
result += 10000 + 500 * (aPerformance['audience'] - 20)
result += 300 * aPerformance['audience']
else:
raise Exception(f"알 수 없는 장르: ${aPerformance['play']['type']}")
return result
def volume_credits_for(aPerformance):
result = 0
result += max(aPerformance['audience'] - 30, 0)
if aPerformance['play']['type'] == "comedy":
result += aPerformance['audience'] // 5
return result
def total_volume_credits(data):
return sum(perf['volume_credits'] for perf in data['performances'])
def total_amount(data):
return sum(perf['amount'] for perf in data['performances'])
def enrich_performances(aPerformance):
result = aPerformance
result['play'] = play_for(result)
result['amount'] = amount_for(result)
result['volume_credits'] = volume_credits_for(result)
return result
statement_data = {}
statement_data['customer'] = invoice['customer']
statement_data['performances'] = [enrich_performances(perf) for perf in invoice['performances']]
statement_data['total_amount'] = total_amount(statement_data)
statement_data['total_volume_credits'] = total_volume_credits(statement_data)
return statement_data
def statement(invoice, plays):
return render_plane_text(create_statement_data(invoice, plays))
이제 이와 같이 별도로 다 빼버린다. 그리고 create_statement_data와 나머지 함수를 별도의 파일에 저장한다.
코드 길이가 늘어난 것은 너무 부정적으로 보지 않아도 괜찮다.
1.7 다형성을 활용해 계산 코드 재구성하기
amount_for 함수에서 장르별로 다른 방식으로 계산하는데, 이와 같은 코드는 코드의 길이가 늘어나는 주범이다. 그래서 다형성을 이용해서 이를 해결하는데, 이러한 과정은 조건부 로직을 다형성으로 바꾸기 기법이다.
이를 리팩터링하려면 상속 계층부터 정의해야 한다.
volume_credits_for 함수와 amount_for에서 type에 따라서 다르게 계산하는 모습을 보이는데, 이렇게 두함수를 전용 클래스로 옮기는 작업을 해야한다.
class performance_calculator:
def __init__(self, aPerformance):
self.aPerformance = aPerformance
...
def enrich_performances(aPerformance):
calculator = performance_calculator(aPerformance)
result = aPerformance
...
우선 이렇게 정의를 한다. 아직은 할 수 있는 일이 없다. 이제 여기에 기존 코드의 함수들을 옮길 것이다.
대충 옮긴 모습은 다음과 같다.
class performance_calculator:
def __init__(self, aPerformance, aPlay):
self.performance = aPerformance
self.play = aPlay
@property
def amount(self):
result = 0
if self.play['type'] == "tragedy":
result = 40000
if self.performance['audience'] > 30:
result += 1000 * (self.performance['audience'] - 30)
elif self.play['type'] == "comedy":
result = 30000
if self.performance['audience'] > 20:
result += 10000 + 500 * (self.performance['audience'] - 20)
result += 300 * self.performance['audience']
else:
raise Exception(f"알 수 없는 장르: ${self.play['type']}")
return result
@property
def volume_credits(self):
result = 0
result += max(self.performance['audience'] - 30, 0)
if self.performance['play']['type'] == "comedy":
result += self.performance['audience'] // 5
return result
def create_statement_data(invoice, plays):
...
def enrich_performances(aPerformance):
calculator = performance_calculator(aPerformance, play_for(aPerformance))
result = aPerformance
result['play'] = calculator.play
result['amount'] = calculator.amount
result['volume_credits'] = calculator.volume_credits
return result
공연료 계산기를 다형성 버전으로 만들기
클래스에 로직을 담았으니 이제 다형성을 지원하게 만들면 된다. 가장 먼저 할 일은 타입코드를 서브클래스로 바꾸기이다.
이렇게 하기 위해서는 performance_calculator의 서브클래스들을 준비하고, create_statement_data에서 적절한 것을 사용하게 만들어야 한다.
그리고 왠지 모르겠는데 생성자를 팩터리 함수로 바꿔야한다고 한다.
def create_performance_calculator(aPerformance, aPlay):
return performance_calculator(aPerformance, aPlay)
def create_statement_data(invoice, plays):
...
def enrich_performances(aPerformance):
calculator = create_performance_calculator(aPerformance, play_for(aPerformance))
result = aPerformance
result['play'] = calculator.play
result['amount'] = calculator.amount
result['volume_credits'] = calculator.volume_credits
return result
이렇게 하고, create_performance_calculator를 다음과 같이 변경한다.
class tragedy_calculator(performance_calculator):
class comedy_calculator(performance_calculator):
def create_performance_calculator(aPerformance, aPlay):
if aPlay['type'] == "tragedy":
return tragedy_calculator(aPerformance, aPlay)
elif aPlay['type'] == "comedy":
return comedy_calculator(aPerformance, aPlay)
else:
raise Exception(f"알 수 없는 장르: ${aPlay['type']}")
클래스를 다음과 같이 수정한다.
class performance_calculator:
def __init__(self, aPerformance, aPlay):
self.performance = aPerformance
self.play = aPlay
@property
def amount(self):
raise Exception("서브클래스에서 하기로 했습니다.")
@property
def volume_credits(self):
result = 0
result += max(self.performance['audience'] - 30, 0)
if self.performance['play']['type'] == "comedy":
result += self.performance['audience'] // 5
return result
class tragedy_calculator(performance_calculator):
@property
def amount(self):
result = 40000
if self.performance['audience'] > 30:
result += 1000 * (self.performance['audience'] - 30)
return result
class comedy_calculator(performance_calculator):
@property
def amount(self):
result = 30000
if self.performance['audience'] > 20:
result += 10000 + 500 * (self.performance['audience'] - 20)
result += 300 * self.performance['audience']
return result
그리고, volume_credits의 경우에는 comedy에서만 변동이 생기므로, 다음과 같이 comedy_calculator 클래스를 변경한다.
class performance_calculator:
...
@property
def volume_credits(self):
return max(self.performance['audience'] - 30, 0)
class comedy_calculator(performance_calculator):
...
@property
def volume_credits(self):
return super().volume_credits + self.performance['audience'] // 5
super()에 주의하자! 괄호 있다.
앞으로 새로운 장르가 들어오면 create_performance_calculator에 장르를 추가하고, 서브클래스를 작성하면 된다.
요약
이번 장에서는 함수 추출하기, 변수 인라인하기, 함수 옮기기, 조건부 로직을 다형성으로 바꾸기를 비롯한 다양한 리팩터링 기법을 선보였다.
이번 장에서는 크게 다음의 세 단계로 진행하였다고 할 수 있다.
- 원본 함수를 중첩함수 여러 개로 나눴다.
- 단계 쪼개기를 적용해서 계산 코드와 출력 코드를 분리했다.
- 계산 로직을 다형성으로 표현했다.
중간 중간 테스트를 하는 것으로 결과가 잘 유지되고 있는지 확인하는 것도 중요했다.
좋은 코드를 가늠하는 확실한 방법은 '얼마나 수정하기 쉬운가'다.
'컴퓨터 공부 > 리팩터링' 카테고리의 다른 글
3. 코드에서 나는 악취 | 리팩터링 (0) | 2021.01.29 |
---|---|
2. 리팩터링 원칙 | 리팩터링 (0) | 2021.01.25 |