diff --git a/README.md b/README.md index cae487b..4bfd035 100644 --- a/README.md +++ b/README.md @@ -3,43 +3,65 @@ [![Build Status](https://travis-ci.com/zedr/clean-code-python.svg?branch=master)](https://travis-ci.com/zedr/clean-code-python) [![](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/download/releases/3.8.3/) -## Table of Contents -1. [Introduction](#introduction) -2. [Variables](#variables) -3. [Functions](#functions) -5. [Classes](#classes) - 1. [S: Single Responsibility Principle (SRP)](#single-responsibility-principle-srp) - 2. [O: Open/Closed Principle (OCP)](#openclosed-principle-ocp) - 3. [L: Liskov Substitution Principle (LSP)](#liskov-substitution-principle-lsp) - 4. [I: Interface Segregation Principle (ISP)](#interface-segregation-principle-isp) - 5. [D: Dependency Inversion Principle (DIP)](#dependency-inversion-principle-dip) -6. [Don't repeat yourself (DRY)](#dont-repeat-yourself-dry) -7. [Translations](#translations) +

-## Introduction +## Author -Software engineering principles, from Robert C. Martin's book -[*Clean -Code*](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882) -, adapted for Python. This is not a style guide. It's a guide to producing -readable, reusable, and refactorable software in Python. +[@zedr](https://github.com/zedr), Thank you for making a great document!!! -Not every principle herein has to be strictly followed, and even fewer will be -universally agreed upon. These are guidelines and nothing more, but they are -ones codified over many years of collective experience by the authors of *Clean -Code*. +

-Adapted -from [clean-code-javascript](https://github.com/ryanmcdermott/clean-code-javascript) +## 목차 -Targets Python3.7+ +1. [소개](#소개) +2. [변수](#변수) +3. [함수](#함수) +4. [클래스 (객체지향 5원칙)](#클래스) + 1. [S: 단일 책임 원칙 (Single Responsibility Principle; SRP)](#단일-책임-원칙-single-responsibility-principle-srp) + 2. [O: 개방/폐쇄 원칙 (Open/Closed Principle; OCP)](#개방폐쇄-원칙-openclosed-principle-ocp) + 3. [L: 리스코프 치환 원칙 (Liskov Substitution Principle; LSP)](#리스코프-치환-원칙-liskov-substitution-principle-lsp) + 4. [I: 인터페이스 분리 원칙 (Interface Segregation Principle; ISP)](#인터페이스-분리-원칙-interface-segregation-principle-isp) + 5. [D: 의존성 역전 원칙 (Dependency Inversion Principle; DIP)](#의존성-역전-원칙-dependency-inversion-principle-dip) +5. [반복은 지양합시다. (Don't repeat yourself; DRY)](#반복은-지양합시다-dont-repeat-yourself-dry) +6. [Translations](#translations) -## **Variables** +

-### Use meaningful and pronounceable variable names +## 소개 -**Bad:** +Robert C. Martin의 책, [*Clean +Code*](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882)을 참고하였습니다. + +
+ +이 문서는 Python에 맞게 수정되었으며 style guide가 아닙니다. + +이 문서는 Python으로 읽을 수 있고(readable) 재사용 가능하며(reusable), 리펙토링 가능한(refactorable) 소프트웨어를 만들어 내기 위한 가이드라인을 제시합니다. + +
+ +이 문서의 모든 것을 완전히 따를 필요는 없으며, 각 구성원 간 보편적 합의에 따라가면 됩니다. + +다시 말하지만 이 문서에서 언급하는 것들은 모두 지침일 뿐입니다. +다만, *Clean Code*의 저자들에 의해 수년간의 경험에 의해 정립된 것들입니다. + +
+ +[clean-code-javascript](https://github.com/ryanmcdermott/clean-code-javascript)의 문서를 `Python 3.7+` 버전에 맞게 수정하였습니다. + + +

+ +## **변수** + +### 변수 이름은 의미가 있어야 하며, 발음할 수 있어야 합니다. +(meaningful, pronounceable) + + +
+ +**나쁜 예:** ```python import datetime @@ -47,10 +69,9 @@ import datetime ymdstr = datetime.date.today().strftime("%y-%m-%d") ``` -Additionally, there's no need to add the type of the variable (str) to its -name. +
-**Good**: +**좋은 예:** ```python import datetime @@ -58,12 +79,16 @@ import datetime current_date: str = datetime.date.today().strftime("%y-%m-%d") ``` -**[⬆ back to top](#table-of-contents)** +**[⬆ 목차로 이동](#목차)** + +

+ +### 동일 대상의 변수에 대해서는 동일 어휘를 사용합시다. -### Use the same vocabulary for the same type of variable +
-**Bad:** -Here we use three different names for the same underlying entity: +**나쁜 예:** +아래 예제는 동일 대상(entity)에 대해 3개의 다른 이름을 사용합니다. ```python def get_user_info(): pass @@ -75,9 +100,10 @@ def get_client_data(): pass def get_customer_record(): pass ``` -**Good**: -If the entity is the same, you should be consistent in referring to it in your -functions: +
+ +**좋은 예:** +만약 entity가 동일하다면, 일관성 있게(consistent) 변수나 함수의 이름을 짓는 것이 좋습니다. ```python def get_user_info(): pass @@ -89,10 +115,10 @@ def get_user_data(): pass def get_user_record(): pass ``` -**Even better** -Python is (also) an object oriented programming language. If it makes sense, -package the functions together with the concrete implementation of the entity -in your code, as instance attributes, property methods, or methods: +
+ +**참고하면 좋은 예:** +Python은 객체 지향 프로그래밍 언어입니다. 필요한 경우 인스턴스의 속성(attribute), 프로퍼티 메소드(property method)나 메소드(method)와 함께 코드에서 entity의 구체적인 구현 및 패키지화하는 것이 좋습니다. ```python from typing import Union, Dict @@ -113,16 +139,21 @@ class User: return Record() ``` -**[⬆ back to top](#table-of-contents)** +**[⬆ 목차로 이동](#목차)** + -### Use searchable names +

-We will read more code than we will ever write. It's important that the code we -do write is readable and searchable. By *not* naming variables that end up -being meaningful for understanding our program, we hurt our readers. Make your -names searchable. +### 검색에 용이한 이름을 사용합시다. +우리는 코딩을 하며 많은 코드를 읽습니다. 때문에 우리가 작성하는 코드를 읽기 쉽고 검색 가능한 이름으로 선언하는 것은 중요합니다. -**Bad:** +만약 변수를 선언할 때 의미가 없거나 검색에 어려움을 주는 이름으로 선언한다면, 우리의 코드를 읽는 다른 사람들이 힘들어할 것입니다. + +검색 가능한(유추 가능한) 이름을 사용합시다. + +
+ +**나쁜 예:** ```python import time @@ -131,7 +162,9 @@ import time time.sleep(86400) ``` -**Good**: +
+ +**좋은 예:** ```python import time @@ -141,11 +174,16 @@ SECONDS_IN_A_DAY = 60 * 60 * 24 time.sleep(SECONDS_IN_A_DAY) ``` -**[⬆ back to top](#table-of-contents)** +**[⬆ 목차로 이동](#목차)** + +

+ +### 변수는 독립적이어야 합니다. +(explanatory) -### Use explanatory variables +
-**Bad:** +**나쁜 예:** ```python import re @@ -158,25 +196,29 @@ if matches: print(f"{matches[1]}: {matches[2]}") ``` -**Not bad**: +
-It's better, but we are still heavily dependent on regex. +**나쁘지는 않은 예:** + +나쁘지는 않지만, 여전히 regex의 결과에 의존하고 있습니다. ```python import re address = "One Infinite Loop, Cupertino 95014" city_zip_code_regex = r"^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$" -matches = re.match(city_zip_code_regex, address) +matches = re.match(city_zip_code_regex, address) if matches: city, zip_code = matches.groups() print(f"{city}: {zip_code}") ``` -**Good**: +
+ +**좋은 예:** -Decrease dependence on regex by naming subpatterns. +하위 패턴의 이름을 지정함으로써 regex 결과에 대한 의존성을 줄일 수 있습니다. ```python import re @@ -189,14 +231,18 @@ if matches: print(f"{matches['city']}, {matches['zip_code']}") ``` -**[⬆ back to top](#table-of-contents)** +**[⬆ 목차로 이동](#목차)** -### Avoid Mental Mapping +

-Don’t force the reader of your code to translate what the variable means. -Explicit is better than implicit. +### 읽는 사람으로 하여금 기능을 유추하도록 만드는 이름을 짓지 마세요. +변수가 의미하는 바가 무엇인지를 코드를 상세히 보지 않아도 알 수 있도록 하세요. -**Bad:** +명시적인 것이 암묵적인 것보다 좋습니다. + +
+ +**나쁜 예:** ```python seq = ("Austin", "New York", "San Francisco") @@ -209,7 +255,9 @@ for item in seq: print(item) ``` -**Good**: +
+ +**좋은 예:** ```python locations = ("Austin", "New York", "San Francisco") @@ -221,14 +269,17 @@ for location in locations: print(location) ``` -**[⬆ back to top](#table-of-contents)** +**[⬆ 목차로 이동](#목차)** -### Don't add unneeded context +

-If your class/object name tells you something, don't repeat that in your -variable name. +### 불필요한 context는 추가하지 마세요. -**Bad:** +클래스/객체 이름이 무언가를 이미 알려주는 경우, 변수 이름에서 이를 반복하지 마세요. + +
+ +**나쁜 예:** ```python class Car: @@ -237,7 +288,9 @@ class Car: car_color: str ``` -**Good**: +
+ +**좋은 예**: ```python class Car: @@ -246,13 +299,17 @@ class Car: color: str ``` -**[⬆ back to top](#table-of-contents)** +**[⬆ 목차로 이동](#목차)** -### Use default arguments instead of short circuiting or conditionals +

-**Tricky** +### short circuiting 또는 conditionals 대신 default parameter를 사용하세요. -Why write: +여기서 `short circuiting`은 논리 연산(and, or)를 의미합니다. + +
+ +**다음과 같은 상황에서**: ```python import hashlib @@ -264,10 +321,15 @@ def create_micro_brewery(name): # etc. ``` -... when you can specify a default argument instead? This also makes it clear -that you are expecting a string as the argument. +
+ +만약 위와 같이 조건문을 사용하는 것 대신 매개변수만을 사용하더라도 함수의 동작에 아무런 영향이 없다는 것을 알 수 있습니다. -**Good**: +우리는 위 코드를 아래와 같이 수정하고 싶을 것입니다. + +
+ +**좋은 예**: ```python import hashlib @@ -278,19 +340,27 @@ def create_micro_brewery(name: str = "Hipster Brew Co."): # etc. ``` -**[⬆ back to top](#table-of-contents)** +**[⬆ 목차로 이동](#목차)** + +

+ +## **함수** + +### 함수는 작업의 단위입니다. + +함수는 소프트웨어 엔지니어링에서 가장 중요한 rule 중 하나입니다. -## **Functions** +함수들이 하나 이상의 작업을 수행한다면 관리, 테스트 및 추론에 어려움을 겪을 것입니다. -### Functions should do one thing +
-This is by far the most important rule in software engineering. When functions -do more than one thing, they are harder to compose, test, and reason about. -When you can isolate a function to just one action, they can be refactored -easily and your code will read much cleaner. If you take nothing else away from -this guide other than this, you'll be ahead of many developers. +함수를 하나의 작업으로 분리한다면, 리펙토링(refactoring)이 쉬워지고 코드를 훨씬 깨끗하게 만들 수 있습니다. -**Bad:** +만약 이 rule를 숙지하고 실천한다면, 여러분은 많은 개발자들을 앞서게 될 것입니다. + +

+ +**나쁜 예:** ```python from typing import List @@ -312,7 +382,9 @@ def email_clients(clients: List[Client]) -> None: email(client) ``` -**Good**: +

+ +**좋은 예 1**: ```python from typing import List @@ -339,9 +411,13 @@ def email_clients(clients: List[Client]) -> None: email(client) ``` -Do you see an opportunity for using generators now? +
-**Even better** +위 코드에서 generator를 사용할 수 있는 부분이 보이시나요? + +

+ +**좋은 예 2:** ```python from typing import Generator, Iterator @@ -367,33 +443,43 @@ def email_client(clients: Iterator[Client]) -> None: email(client) ``` -**[⬆ back to top](#table-of-contents)** +**[⬆ 목차로 이동](#목차)** + +

-### Function arguments (2 or fewer ideally) +### 함수의 매개변수 (이상적으로 2개 이하) -A large amount of parameters is usually the sign that a function is -doing too much (has more than one responsibility). Try to decompose it -into smaller functions having a reduced set of parameters, ideally less than -three. +매개변수의 수가 많다는 것은 일반적으로 함수가 너무 많은 일을 수행한다는 것을 의미합니다. (has more than one responsibility) -If the function has a single responsibility, consider if you can bundle -some or all parameters into a specialized object that will be passed as an -argument to the function. These parameters might be attributes of a single -entity that you can represent with a dedicated data structure. You may also -be able to reuse this entity elsewhere in your program. The reason why this is -a better arrangement is than having multiple parameters is that we may be able -to move some computations, done with those parameters inside the -function, into methods belonging to the new object, therefore reducing the -complexity of the function. +때문에 매개변수의 개수를 제한한다면 함수를 더 쉽게 테스트 할 수 있습니다. -**Bad:** +매개변수가 많은 함수를 매개변수가 적은 함수로 분해할 수 있다면 해보세요. 이상적으로는 3개 미만입니다. + +
+ +함수에 대해 단일 책임(single responsibility)이 있는 경우 여러 개의 매개변수를 하나의 특수한 개체로 묶을 수 있는지도 살펴보세요. + +프로그램에서 다른 곳에 매개변수를 재사용해야 하는 상황이 온다면 이 개체를 요긴하게 사용할 수 있습니다. + +
+ +또한 이 방법이 여러 개의 매개변수를 갖는 것 보다 더 나은 이유는 + +함수 내부의 매개변수를 사용하여 수행되는 연산들을 또 하나의 함수로 만들어 복잡성을 줄일 수 있기 때문입니다. + + +
+ +**나쁜 예:** ```python def create_menu(title, body, button_text, cancellable): pass ``` -**Java-esque**: +
+ +**java-esque (자바 표현법)**: ```python class Menu: @@ -413,7 +499,9 @@ menu = Menu( ) ``` -**Also good** +
+ +**좋은 예 1:** ```python class MenuConfig: @@ -447,7 +535,9 @@ config.cancellable = True create_menu(config) ``` -**Fancy** +
+ +**좋은 예 2:** ```python from typing import NamedTuple @@ -482,7 +572,9 @@ create_menu( ) ``` -**Even fancier** +
+ +**좋은 예 3:** ```python from dataclasses import astuple, dataclass @@ -518,7 +610,9 @@ create_menu( ) ``` -**Even fancier, Python3.8+ only** +
+ +**좋은 예 4 (Python3.8+ only)** ```python from typing import TypedDict @@ -555,11 +649,16 @@ create_menu( ) ``` -**[⬆ back to top](#table-of-contents)** +**[⬆ 목차로 이동](#목차)** + +

+ -### Function names should say what they do +### 함수의 이름은 함수가 수행하는 작업을 나타내야 합니다. -**Bad:** +
+ +**나쁜 예:** ```python class Email: @@ -572,7 +671,9 @@ message = Email() message.handle() ``` -**Good:** +
+ +**좋은 예:** ```python class Email: @@ -584,14 +685,19 @@ message = Email() message.send() ``` -**[⬆ back to top](#table-of-contents)** +**[⬆ 목차로 이동](#목차)** -### Functions should only be one level of abstraction +

-When you have more than one level of abstraction, your function is usually -doing too much. Splitting up functions leads to reusability and easier testing. +### 함수에는 추상화(abstraction)가 한 층만 있어야 합니다. -**Bad:** +만약 함수에 추상적인 층이 하나 이상 있다면, 함수가 너무 복잡해집니다. + +추상층이 여러 개 있다면, 그것들을 함수로 분해하여 재사용성을 높이고 테스트에 용이하도록 하는 것이 좋습니다. + +
+ +**나쁜 예:** ```python # type: ignore @@ -615,7 +721,9 @@ def parse_better_js_alternative(code: str) -> None: pass ``` -**Good:** +
+ +**좋은 예:** ```python from typing import Tuple, List, Dict @@ -651,15 +759,19 @@ def parse(tokens: List) -> List: return syntax_tree ``` -**[⬆ back to top](#table-of-contents)** +**[⬆ 목차로 이동](#목차)** + +

-### Don't use flags as function parameters +### 함수의 매개변수로 flag를 사용하지 마세요. -Flags tell your user that this function does more than one thing. Functions -should do one thing. Split your functions if they are following different code -paths based on a boolean. +flag는 사용자로 하여금 이 함수가 두가지 이상의 기능을 수행한다는 것으로 보여질 수 있습니다. -**Bad:** +함수는 한가지 일을 해야합니다. bool을 기준으로 함수의 기능이 완전히 바뀐다면 함수를 분할해보세요. + +
+ +**나쁜 예:** ```python from tempfile import gettempdir @@ -673,7 +785,9 @@ def create_file(name: str, temp: bool) -> None: Path(name).touch() ``` -**Good:** +
+ +**좋은 예:** ```python from tempfile import gettempdir @@ -688,29 +802,35 @@ def create_temp_file(name: str) -> None: (Path(gettempdir()) / name).touch() ``` -**[⬆ back to top](#table-of-contents)** +**[⬆ 목차로 이동](#목차)** + +

+ +### 함수는 부작용(side effect)을 피해야 합니다. + +여기서 말하는 부작용(side effect)은 부정적인 의미가 아닙니다. + +함수는 일반적으로 매개변수를 받은 후 일련의 작업을 거쳐 값을 반환합니다. + +만약 값을 반환하는 것 이외에 다른 작업을 추가로 수행하는 경우 이 행위를 부작용이라 부릅니다. + +
+ +예를 들어 부작용으로 파일에 글을 쓸 수도 있으며, 파일의 특정 변수를 수정할 수도 있고, 실수로 모든 돈을 낯선 사람에게 송금할 수도 있습니다. + +만약 부작용을 꼭 필요로 한다면, 부작용이 유발되는 위치를 표시해주는 것이 좋습니다. + +또한 다른 함수나 클래스가 동시에 동일한 파일을 조작하지 않도록 하고 특정 함수를 통해 파일을 이 파일을 조작하도록 합시다. -### Avoid side effects +
-A function produces a side effect if it does anything other than take a value -in and return another value or values. For example, a side effect could be -writing to a file, modifying some global variable, or accidentally wiring all -your money to a stranger. +주요 요점은 개체 간 상태 공유, 가변 데이터 등을 사용하여 모든 함수 또는 변수가 이러한 데이터(파일 혹은 파일 내 데이터)를 조작할 수 있게 되는 일반적인 함정은 피할 필요가 있습니다. -Now, you do need to have side effects in a program on occasion - for example, -like in the previous example, you might need to write to a file. In these -cases, you should centralize and indicate where you are incorporating side -effects. Don't have several functions and classes that write to a particular -file - rather, have one -(and only one) service that does it. +만약 이것을 잘 지킨다면, 다른 프로그래머들보다 오류를 찾기 더 수월해질 것입니다. -The main point is to avoid common pitfalls like sharing state between objects -without any structure, using mutable data types that can be written to by -anything, or using an instance of a class, and not centralizing where your side -effects occur. If you can do this, you will be happier than the vast majority -of other programmers. +
-**Bad:** +**나쁜 예:** ```python # type: ignore @@ -739,7 +859,9 @@ print(fullname) # ["Ryan", "McDermott"] # function again? ``` -**Good:** +
+ +**좋은 예 1:** ```python from typing import List, AnyStr @@ -755,7 +877,9 @@ name, surname = split_into_first_and_last_name(fullname) print(name, surname) # => Ryan McDermott ``` -**Also good** +
+ +**좋은 예 2:** ```python from dataclasses import dataclass @@ -776,23 +900,29 @@ print(person.name) # => "Ryan McDermott" print(person.name_as_first_and_last) # => ["Ryan", "McDermott"] ``` -**[⬆ back to top](#table-of-contents)** +**[⬆ 목차로 이동](#목차)** -## **Classes** +

-### **Single Responsibility Principle (SRP)** +## **클래스** -Robert C. Martin writes: +### 단일 책임 원칙 (Single Responsibility Principle; SRP) -> A class should have only one reason to change. +*설명하기에 앞서 책임(responsibility)를 이해를 위해 기능으로 해석했음을 미리 알려드리겠습니다.* -"Reasons to change" are, in essence, the responsibilities managed by a class or -function. +
-In the following example, we create an HTML element that represents a comment -with the version of the document: +Robert C. Martin이 말하기를...: -**Bad** +> Class가 변경될 이유는 단 하나여야 한다. (A class should have only one reason to change.) + +"변경되야 할 이유"는 클래스 또는 함수가 담당하는 기능에 대응합니다. + +다음 예제에서는 HTML 주석을 만들고 주석에 pip의 버전을 기록합니다: + +
+ +**나쁜 예:** ```python from importlib import metadata @@ -813,16 +943,18 @@ class VersionCommentElement: VersionCommentElement().render() ``` -This class has two responsibilities: +
-- Retrieve the version number of the Python package -- Render itself as an HTML element +위 클래스는 두가지 기능이 있습니다. +- pip 버전 정보를 획득합니다. +- HTML 주석을 생성합니다. -Any change to one or the other carries the risk of impacting the other. +다만 위 코드에서 특정 기능을 변경하면 다른 기능에 영향을 미칩니다. +우리는 이 두 기능을 분해할 수 있습니다. -We can rewrite the class and decouple these responsibilities: +
-**Good** +**좋은 예:** ```python from importlib import metadata @@ -847,37 +979,44 @@ class VersionCommentElement: VersionCommentElement(get_version("pip")).render() ``` -The result is that the class only needs to take care of rendering itself. It -receives the version text during instantiation and this text is generated by -calling a separate function, `get_version()`. Changing the class has no impact -on the other, and vice-versa, as long as the contract between them does not -change, i.e. the function provides a string and the class `__init__` method -accepts a string. +
-As an added bonus, the `get_version()` is now reusable elsewhere. +위와 같이 작성하면 이 클래스는 **HTML 요소를 생성하는 것에만 집중**하게 됩니다. -### **Open/Closed Principle (OCP)** +인스턴스화할 때 버전 번호가 초기 매개변수로 전달됩니다. (`get_version()`을 통해 버전 정보를 얻음) -> “Incorporate new features by extending the system, not by making -> modifications (to it)”, -> Uncle Bob. +클래스 및 함수는 서로 격리되어 있으며 버전 사항이 다른 항목에는 영향을 미치지 않습니다. -Objects should be open for extension, but closed to modification. It should be -possible to augment the functionality provided by an object (for example, a -class) -without changing its internal contracts. An object can enable this when it is -designed to be extended cleanly. +또한 `get_version()`은 재사용될 수 있습니다. -In the following example, we try to implement a simple web framework that -handles HTTP requests and returns responses. The `View` class has a single -method `.get()` that will be called when the HTTP server will receive a GET -request from a client. +**[⬆ 목차로 이동](#목차)** -`View` is intentionally simple and returns `text/plain` responses. We would -also like to return HTML responses based on a template file, so we subclass it -using the `TemplateView` class. +

-**Bad** +### 개방/폐쇄 원칙 (Open/Closed Principle; OCP) + +소프트웨어의 객체(클래스, 함수 등)는 확장(extension)에 대해 열려 있어야 하지만, 수정(modification)에는 닫혀있어야 합니다. + +클래스 같은 개체는 내부 논리를 수정하지 않고 새로운 기능을 추가할 수 있도록 보장해야합니다. +(원래 코드를 수정하지 않으면서 코드를 추가할 수 있어야 한다는 의미와 같습니다.) + +즉, 객체는 설계 초기에 확장성을 보장해야 합니다. + +다음 예에서는 HTTP 요청에 응답하는 간단한 웹 프레임워크를 구현하는 코드입니다. + +HTTP 서버에서 GET 요청을 받으면 `View` 클래스 `.get()` 메소드가 호출됩니다. + +
+ +`View`는 단순히 `text/plain`만 반환합니다. + +하지만 우리는 `text/HTML`의 형태로 받기를 원합니다. + +그래서 우리는 `View` 클래스를 상속받아 `TemplateView` 클래스를 만들었습니다. + +
+ +**나쁜 예:** ```python from dataclasses import dataclass @@ -918,17 +1057,22 @@ class TemplateView(View): ``` -The `TemplateView` class has modified the internal behaviour of its parent -class in order to enable the more advanced functionality. In doing so, it now -relies on the `View` to not change the implementation of the `.get()` -method, which now needs to be frozen in time. We cannot introduce, for example, -some additional checks in all our `View`-derived classes because the behaviour -is overridden in at least one subtype and we will need to update it. +
-Let's redesign our classes to fix this problem and let the `View` class be -extended (not modified) cleanly: +새로운 기능을 구현하기 위해 `TemplateView`는 `View`를 상속받고 `.get()` 메소드를 다시 썼습니다. -**Good** +위 코드는 부모 클래스의 `.get()`을 변경하지 않고 자식 클래스에서 오버라이딩 한 경우입니다. + +
+ +만약 위와 같은 방식으로 기능이 여러 개로 파생된다면, +테스트를 수행할 때 `View` 클래스의 모든 자식 클래스에 대해 테스트 기능을 추가해야할 가능성이 있습니다. + +이 문제를 해결하기 위해 코드를 다시 설계하고 `View` 클래스가 깨끗하게 확장되도록 합시다. + +
+ +**좋은 예 1:** ```python from dataclasses import dataclass @@ -972,30 +1116,32 @@ class TemplateView(View): with open(self.template_file) as fd: return fd.read() - ``` -Note that we did need to override the `render_body()` in order to change the -source of the body, but this method has a single, well defined responsibility -that **invites subtypes to override it**. It is designed to be extended by its -subtypes. +
+ +응답 내용을 변경하려면 `render_body()`를 재정의해야 하지만 +이 메소드는 하위 유형을 재정의하도록 요청하는 잘 정의된 단일 책임(single reponsibility)이 있습니다. + +그러나 이 방법은 자식 클래스가 기능을 확장하기 위해 여전히 재정의해야 합니다. + +상속(inheritance)과 컴포지션(composition)의 장점을 모두 사용하는 또 다른 좋은 방법은 +[Mixins](https://docs.djangoproject.com/en/4.1/topics/class-based-views/mixins/)을 사용하는 방법입니다. + +Mixins은 다른 관련 클래스들과는 독립적으로 사용 가능한 bare-bones classes입니다. -Another good way to use the strengths of both object inheritance and object -composition is to -use [Mixins](https://docs.djangoproject.com/en/4.1/topics/class-based-views/mixins/) -. +target의 동작(behaviour)을 변경하기 위해 다중 상속을 사용하여 target 클래스와 "mixed-in" 됩니다. -Mixins are bare-bones classes that are meant to be used exclusively with other -related classes. They are "mixed-in" with the target class using multiple -inheritance, in order to change the target's behaviour. +
-A few rules: +Rules: +- Mixins는 항상 `object`에서 상속되어야 합니다. +- Mixins는 항상 target 클래스 앞에 위치해야 합니다. + - e.g. `Foo(MixinA, MixinB, TargetClass)` -- Mixins should always inherit from `object` -- Mixins always come before the target class, - e.g. `class Foo(MixinA, MixinB, TargetClass): ...` +
-**Also good** +**좋은 예 2:** ```python from dataclasses import dataclass, field @@ -1068,35 +1214,44 @@ class TemplateView(TemplateRenderMixin, ContentLengthMixin, View): ``` -As you can see, Mixins make object composition easier by packaging together -related functionality into a highly reusable class with a single -responsibility, allowing clean decoupling. Class extension is achieved by " -mixing-in" the additional classes. +
-The popular Django project makes heavy use of Mixins to compose its class-based -views. +위 코드에서 볼 수 있듯이, `Mixins`는 관련 기능을 재사용 가능한 클래스로 캡슐화함으로써 -FIXME: re-enable typechecking for the line above once it's clear how to use -`typing.Protocol` to make the type checker work with Mixins. +더 쉽게 패키징할 수 있게 되었으며, 단일 책임 원칙(SRP)에도 부합합니다. -### **Liskov Substitution Principle (LSP)** +
-> “Functions that use pointers or references to base classes -> must be able to use objects of derived classes without knowing it”, -> Uncle Bob. +`Django`도 여러 가지의 `View` 클래스를 구성하기 위해 Mixins를 많이 사용했습니다. -This principle is named after Barbara Liskov, who collaborated with fellow -computer scientist Jeannette Wing on the seminal paper -*"A behavioral notion of subtyping" (1994). A core tenet of the paper is that -"a subtype (must) preserve the behaviour of the supertype methods and also all -invariant and history properties of its supertype". +FIXME: `typing.Protocol`의 사용 방식이 정립되었기 때문에 `Mixins`에 type 검사를 추가해야 합니다. -In essence, a function accepting a supertype should also accept all its -subtypes with no modification. +
-Can you spot the problem with the following code? +**[⬆ 목차로 이동](#목차)** -**Bad** +

+ +### 리스코프 치환 원칙 (Liskov Substitution Principle; LSP) + +
+ +> "부모 클래스의 포인터나 참조를 사용하는 함수는 +> 부모 클래스로부터 파생된 자식 클래스에 대해 몰라도 사용할 수 있어야 해.", Uncle Bob. + +
+ +이 원칙은 *A behavioral notion of subtyping (1994)* 논문의 저자 Jeannette Wing과 협력한 Barbara Liskov의 이름을 따서 명명되었습니다. + +이 논문의 핵심 원칙은 "subtype이 supertype와 동일한 방법과 동일한 기능과 동일 행동을 유지해야 한다"는 것입니다. + +다시 말해 supertype의 함수는 별도의 수정 없이 모든 subtype을 수용할 수 있어야 합니다. + +
+ +아래의 코드에서 어떤 문제가 있는지 확인해보도록 합시다. + +**나쁜 예:** ```python from dataclasses import dataclass @@ -1150,16 +1305,26 @@ def render(view: View, request) -> Response: ``` -The expectation is that `render()` function will be able to work with `View` -and its subtype `TemplateView`, but the latter has broken compatibility by -modifying the signature of the `.get()` method. The function will raise -a `TypeError` -exception when used with `TemplateView`. +
+ +`render()` 메소드는 `View` 클래스 및 하위 클래스인 `TemplateView`와 함께 사용할 수 있어야 합니다. + +하지만 `TemplateView`는 상속 시 `.get()` 메소드의 signature(메소드의 입/출력)을 변경했습니다. + +`TemplateView`의 `render()`를 사용할 경우 오류가 발생할 것입니다. + +
+ +만약 우리가 `render()`를 `View`와 `View`의 파생 클래스에서 사용할 수 있기를 원한다면, + +우리는 외부 인터페이스가 손상되지 않도록 주의해야 할 필요가 있습니다. + +그런데 주어진 클래스에의 구성을 어떻게 알 수 있을까요? + +[mypy](https://mypy.readthedocs.io/en/stable/)와 같은 type 검사 도구를 사용하면 +이와 비슷한 문제가 발생할 때의 오류를 확인할 수 있습니다. -If we want the `render()` function to work with any subtype of `View`, we must -pay attention not to break its public-facing protocol. But how do we know what -constitutes it for a given class? Type hinters like *mypy* will raise an error -when it detects mistakes like this: +
``` error: Signature of "get" incompatible with supertype "View" @@ -1169,23 +1334,39 @@ error: Signature of "get" incompatible with supertype "View" :36: note: def get(self, request: Any, template_file: str) -> Response ``` -### **Interface Segregation Principle (ISP)** +
-> “Keep interfaces small -> so that users don’t end up depending on things they don’t need.”, -> Uncle Bob. +**[⬆ 목차로 이동](#목차)** -Several well known object oriented programming languages, like Java and Go, -have a concept called interfaces. An interface defines the public methods and -properties of an object without implementing them. They are useful when we -don't want to couple the signature of a function to a concrete object; we'd -rather say "I don't care what object you give me, as long as it has certain -methods and attributes I expect to make use of". +

-Python does not have interfaces. We have Abstract Base Classes instead, which -are a little different, but can serve the same purpose. +### 인터페이스 분리 원칙 (Interface Segregation Principle; ISP) -**Good** +
+ +> “사용자가 필요없는 것에 의존하지 않도록 인터페이스를 간결하게 만드는 건 어때?", Uncle Bob. + +
+ +Java나 Go와 같은 유명한 객체 지향 프로그래밍 언어에서는 인터페이스(interface)라는 개념이 있습니다. + +인터페이스 클래스는 공개 메소드와 속성을 구현하지 않고 정의합니다. + +함수의 signature(함수의 입/출력)를 정의하고 싶지만 구체적으로 구현하고 싶지 않을 때 인터페이스는 매우 유용하게 사용됩니다. + +
+ +우리는 "당신이 나에게 전달한 대상의 세부 사항에 대해서는 관심이 없고, 내가 사용할 수 있는 방법이나 속성에만 관심이 있다."고 말할 수 있습니다. + +
+ +*Python에는 인터페이스가 없습니다.* + +다만 인터페이스와는 약간 다르지만 추상 클래스를 사용하여 동일한 기능을 구현할 수 있습니다. + +
+ +**좋은 예:** ```python @@ -1219,13 +1400,19 @@ def welcome_user(user_name: str, actor: Greeter): welcome_user("Barbara", FriendlyActor()) ``` -Now imagine the following scenario: we have a certain number of PDF documents -that we author and want to serve to our web site visitors. We are using a -Python web framework and we might be tempted to design a class to manage these -documents, so we go ahead and design a comprehensive abstract base class for -our document. +
+ +이제 다음의 시나리오를 상상해봅시다. + +PDF 문서가 몇 개 있는데, 우리 웹 사이트 사용자에게 PDF 파일을 제공하고 싶습니다. + +우리는 파이썬 웹 프레임워크를 사용하여 이러한 문서를 관리하기 위한 클래스를 설계하고자 합니다. -**Error** +그래서 우리는 문서에 추상 클래스를 설계했는데, 이 클래스에 사용할 수 있는 모든 기능들을 적어두었습니다. + +
+ +**에러 발생:** ```python import abc @@ -1274,28 +1461,31 @@ def view(request): ``` -But we can't! If we don't implement the `.save()` method, an exception will be -raised: +
+ +하지만 안되더라고요! `.save()` 메소드를 구현하지 않으면 예외가 발생합니다. ``` Can't instantiate abstract class PDFDocument with abstract method save. ``` -That's annoying. We don't really need to implement `.save()` here. We could -implement a dummy method that does nothing or raises `NotImplementedError`, but -that's useless code that we will need to maintain. +이건 짜증나네요. 우리는 `.save()`를 구현할 필요가 없습니다. -At the same time, if we remove `.save()` from the abstract class now we will -need to add it back when we will later implement a way for users to submit -their documents, bringing us back to the same situation as before. +우리는 아무것도 하지 않거나 `NotImplementedError`를 발생시키는 더미 메소드를 구현할 수 있지만, 그것또한 쓸모없는 코드가 됩니다. -The problem is that we have written an *interface* that has features we don't -need right now as we are not using them. +
-The solution is to decompose the interface into smaller and composable -interfaces that segregate each feature. +동시에 만약 우리가 추상 클래스에서 `.save()`를 제거한다면, 사용자가 문서를 save 할 때 다시 추가해야 합니다. -**Good** +
+ +문제를 요약하면, 우리는 인터페이스를 썼고, 이 인터페이스에는 현재 사용할 수 없는 몇가지 특성이 있다는 것입니다. + +이 문제에 대한 해결방식은 이 인터페이스를 더 작은 인터페이스로 분해하고, 각각의 새로운 인터페이스가 일부 내용을 담당하도록 만드는 것입니다. + +
+ +**좋은 예:** ```python import abc @@ -1347,14 +1537,25 @@ def view(request): ``` -### **Dependency Inversion Principle (DIP)** +
+ +**[⬆ 목차로 이동](#목차)** -> “Depend upon abstractions, not concrete details”, -> Uncle Bob. +

-Imagine we wanted to write a web view that returns an HTTP response that -streams rows of a CSV file we create on the fly. We want to use the CSV writer -that is provided by the standard library. +### 의존성 역전 원칙 (Dependency Inversion Principle; DIP) + +
+ +> "구체적인 세부 사항(details)보다는 추상(abstractions)에 의존하는 건 어때?", Uncle Bob. + +
+ +CSV 파일의 행을 즉시 스트리밍하는 HTTP Response를 반환하는 web view를 작성하고 싶다고 생각해보세요. + +우리는 파이썬 표준 라이브러리에서 제공하는 CSV writer를 사용하고자 합니다. + +
**Bad** @@ -1395,16 +1596,21 @@ def some_view(request): ``` -Our first implementation works around the CSV's writer interface by -manipulating a `StringIO` object (which is file-like) and performing several -low level operations in order to farm out the rows from the writer. It's a lot -of work and not very elegant. +
+ +첫 구현은 CSV writer 인터페이스를 사용했습니다. + +일부 하위 작업은 파일처럼 String I/O 객체를 조작하여 writer에 데이터를 썼습니다. + +이 방법은 번잡하고 우아하지 않습니다. + +
+ +더 좋은 방법은 writer가 `.write()` 메소드를 포함하는 객체만 필요로 한다는 것을 이해하는 것입니다. + +`StreamingHttpResponse` 클래스가 즉시 클라이언트로 다시 스트리밍할 수 있도록 새로운 행 데이터를 즉시 반환하는 dummy 객체를 전달하는 것은 어떤가요? -A better way is to leverage the fact that the writer just needs an object with -a `.write()` method to do our bidding. Why not pass it a dummy object that -immediately returns the newly assembled row, so that -the `StreamingHttpResponse` -class can immediate stream it back to the client? +
**Good** @@ -1438,44 +1644,67 @@ def some_streaming_csv_view(request): ``` -Much better, and it works like a charm! The reason it's superior to the -previous implementation should be obvious: less code (and more performant) to -achieve the same result. We decided to leverage the fact that the writer class -depends on the `.write()` abstraction of the object it receives, without caring -about the low level, concrete details of what the method actually does. +
-This example was taken from -[a submission made to the Django documentation](https://code.djangoproject.com/ticket/21179) -by this author. +위와 같이 구현하면 이전의 것보다 훨씬 낫고 우아해집니다. -**[⬆ back to top](#table-of-contents)** +더 적은 코드로 동일한 기능을 구현했다는 것은 장점이 분명합니다. -## **Don't repeat yourself (DRY)** +우리는 writer 클래스에서 `.write()`라는 추상적인 방법에만 관심이 있고 내부 세부 사항에는 관심이 없다는 것을 활용했습니다. -Try to observe the [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) -principle. +이 예제는 [a submission made to the Django document](https://code.djangoproject.com/ticket/21179)에서 가지고 온 것입니다. -Do your absolute best to avoid duplicate code. Duplicate code is bad because it -means that there's more than one place to alter something if you need to change -some logic. +
-Imagine if you run a restaurant and you keep track of your inventory: all your -tomatoes, onions, garlic, spices, etc. If you have multiple lists that you keep -this on, then all have to be updated when you serve a dish with tomatoes in -them. If you only have one list, there's only one place to update! +**[⬆ 목차로 이동](#목차)** -Often you have duplicate code because you have two or more slightly different -things, that share a lot in common, but their differences force you to have two -or more separate functions that do much of the same things. Removing duplicate -code means creating an abstraction that can handle this set of different things -with just one function/module/class. +

-Getting the abstraction right is critical. Bad abstractions can be worse than -duplicate code, so be careful! Having said this, if you can make a good -abstraction, do it! Don't repeat yourself, otherwise you'll find yourself -updating multiple places any time you want to change one thing. +## **반복은 지양합시다. (Don't repeat yourself; DRY)** -**Bad:** +위키피디아의 [중복 배제 원칙](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) 문서를 살펴보고 오세요. + +
+ +중복 코드는 코드 로직을 수정할 때 중복되는 부분도 동시에 수정해야 한다는 것을 의미합니다. + +중복 코드가 많으면 많을 수록 수정 작업량이 많아질 수 밖에 없고 오류 발생 가능성 또한 높아지게 됩니다. + +
+ +식당을 운영하고 토마토, 양파, 마늘, 향신료 등 재고를 조사한다고 생각해봅시다. + +리스트가 여러 개 있으면 토마토 하나로 토마토가 들어간 요리를 만들었을 때 여러 개의 리스트를 전부 업데이트 해야 합니다. + +반대로 목록이 하나만 있으면 하나만 업데이트 하면 됩니다. + +
+ +공통점이 많지만 코드 상에 약간 다른 것이 있어서 중복 코드를 사용하는 경우가 종종 있습니다. + +하지만 그 차이로 인해 동일한 작업을 수행하는 두 개 이상의 개별 함수가 필요합니다. + +중복 코드를 제거하려면 먼저 공통 부분을 추상화한 다음 하나의 함수/모듈/클래스로 다른 부분을 처리해야 합니다. + +
+ +추상적 사고를 잘 하는 것은 프로그래머에게 있어 매우 중요한 스킬 중 하나입니다. + +나쁜 추상적 사고로 인한 피해는 때때로 중복 코드보다 더 심각한 문제에 직면할 수 있습니다. + +만약 추상적 사고를 잘 할 수 있다면, 그렇게 하셔야 합니다! 중복 코드를 작성하지 맙시다. + +이를 지키지 않는다면 로직을 변경하고자 할 때 변경해야할 부분이 많다는 것을 곧 알게 될 것입니다. + +
+ +**추상적 사고를 잘 한다는 것:** +번역을 하며 제일 이해하기 어려웠던 부분이 바로 abstraction이라는 말인데, +맥락을 보면 공통적이고 본질적인 부분만 추출하고 개별적인 부분은 버린다는 의미 같습니다. + +
+ +**나쁜 예:** ```python from typing import List, Dict @@ -1547,6 +1776,8 @@ company_managers = [ company_managers_list = get_manager_list(managers=company_managers) ``` +
+ **Good:** ```python @@ -1594,11 +1825,15 @@ company_managers = [ company_managers_list = get_employee_list(employees=company_managers) ``` -**[⬆ back to top](#table-of-contents)** +
+ +**[⬆ 목차로 이동](#목차)** + +

## **Translations** -This document is also available in other languages: +이 문서는 다양한 언어로 번역되었습니다: - 🇨🇳 ** Chinese** [yinruiqing/clean-code-python](https://github.com/yinruiqing/clean-code-python) @@ -1608,4 +1843,8 @@ This document is also available in other languages: - 🇮🇷 ** Persian:** [SepehrRasouli/clean-code-python](https://github.com/SepehrRasouli/clean-code-python) -**[⬆ back to top](#table-of-contents)** +
+ +**[⬆ 목차로 이동](#목차)** + +