Skip to content

๐Ÿ› Clean Code concepts adapted for Python

License

Notifications You must be signed in to change notification settings

wooy0ng/clean-code-python

ย 
ย 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

clean-code-python

Build Status



Author

@zedr, Thank you for making a great document!!!



๋ชฉ์ฐจ

  1. ์†Œ๊ฐœ
  2. ๋ณ€์ˆ˜
  3. ํ•จ์ˆ˜
  4. ํด๋ž˜์Šค (๊ฐ์ฒด์ง€ํ–ฅ 5์›์น™)
    1. S: ๋‹จ์ผ ์ฑ…์ž„ ์›์น™ (Single Responsibility Principle; SRP)
    2. O: ๊ฐœ๋ฐฉ/ํ์‡„ ์›์น™ (Open/Closed Principle; OCP)
    3. L: ๋ฆฌ์Šค์ฝ”ํ”„ ์น˜ํ™˜ ์›์น™ (Liskov Substitution Principle; LSP)
    4. I: ์ธํ„ฐํŽ˜์ด์Šค ๋ถ„๋ฆฌ ์›์น™ (Interface Segregation Principle; ISP)
    5. D: ์˜์กด์„ฑ ์—ญ์ „ ์›์น™ (Dependency Inversion Principle; DIP)
  5. ๋ฐ˜๋ณต์€ ์ง€์–‘ํ•ฉ์‹œ๋‹ค. (Don't repeat yourself; DRY)
  6. Translations



์†Œ๊ฐœ

Robert C. Martin์˜ ์ฑ…, Clean Code์„ ์ฐธ๊ณ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.


์ด ๋ฌธ์„œ๋Š” Python์— ๋งž๊ฒŒ ์ˆ˜์ •๋˜์—ˆ์œผ๋ฉฐ style guide๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค.

์ด ๋ฌธ์„œ๋Š” Python์œผ๋กœ ์ฝ์„ ์ˆ˜ ์žˆ๊ณ (readable) ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋ฉฐ(reusable), ๋ฆฌํŽ™ํ† ๋ง ๊ฐ€๋Šฅํ•œ(refactorable) ์†Œํ”„ํŠธ์›จ์–ด๋ฅผ ๋งŒ๋“ค์–ด ๋‚ด๊ธฐ ์œ„ํ•œ ๊ฐ€์ด๋“œ๋ผ์ธ์„ ์ œ์‹œํ•ฉ๋‹ˆ๋‹ค.


์ด ๋ฌธ์„œ์˜ ๋ชจ๋“  ๊ฒƒ์„ ์™„์ „ํžˆ ๋”ฐ๋ฅผ ํ•„์š”๋Š” ์—†์œผ๋ฉฐ, ๊ฐ ๊ตฌ์„ฑ์› ๊ฐ„ ๋ณดํŽธ์  ํ•ฉ์˜์— ๋”ฐ๋ผ๊ฐ€๋ฉด ๋ฉ๋‹ˆ๋‹ค.

๋‹ค์‹œ ๋งํ•˜์ง€๋งŒ ์ด ๋ฌธ์„œ์—์„œ ์–ธ๊ธ‰ํ•˜๋Š” ๊ฒƒ๋“ค์€ ๋ชจ๋‘ ์ง€์นจ์ผ ๋ฟ์ž…๋‹ˆ๋‹ค.
๋‹ค๋งŒ, Clean Code์˜ ์ €์ž๋“ค์— ์˜ํ•ด ์ˆ˜๋…„๊ฐ„์˜ ๊ฒฝํ—˜์— ์˜ํ•ด ์ •๋ฆฝ๋œ ๊ฒƒ๋“ค์ž…๋‹ˆ๋‹ค.


clean-code-javascript์˜ ๋ฌธ์„œ๋ฅผ Python 3.7+ ๋ฒ„์ „์— ๋งž๊ฒŒ ์ˆ˜์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค.



๋ณ€์ˆ˜

๋ณ€์ˆ˜ ์ด๋ฆ„์€ ์˜๋ฏธ๊ฐ€ ์žˆ์–ด์•ผ ํ•˜๋ฉฐ, ๋ฐœ์Œํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

(meaningful, pronounceable)


๋‚˜์œ ์˜ˆ:

import datetime

ymdstr = datetime.date.today().strftime("%y-%m-%d")

์ข‹์€ ์˜ˆ:

import datetime

current_date: str = datetime.date.today().strftime("%y-%m-%d")

โฌ† ๋ชฉ์ฐจ๋กœ ์ด๋™



๋™์ผ ๋Œ€์ƒ์˜ ๋ณ€์ˆ˜์— ๋Œ€ํ•ด์„œ๋Š” ๋™์ผ ์–ดํœ˜๋ฅผ ์‚ฌ์šฉํ•ฉ์‹œ๋‹ค.


๋‚˜์œ ์˜ˆ: ์•„๋ž˜ ์˜ˆ์ œ๋Š” ๋™์ผ ๋Œ€์ƒ(entity)์— ๋Œ€ํ•ด 3๊ฐœ์˜ ๋‹ค๋ฅธ ์ด๋ฆ„์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

def get_user_info(): pass


def get_client_data(): pass


def get_customer_record(): pass

์ข‹์€ ์˜ˆ:
๋งŒ์•ฝ entity๊ฐ€ ๋™์ผํ•˜๋‹ค๋ฉด, ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ(consistent) ๋ณ€์ˆ˜๋‚˜ ํ•จ์ˆ˜์˜ ์ด๋ฆ„์„ ์ง“๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

def get_user_info(): pass


def get_user_data(): pass


def get_user_record(): pass

์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์€ ์˜ˆ:
Python์€ ๊ฐ์ฒด ์ง€ํ–ฅ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด์ž…๋‹ˆ๋‹ค. ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์ธ์Šคํ„ด์Šค์˜ ์†์„ฑ(attribute), ํ”„๋กœํผํ‹ฐ ๋ฉ”์†Œ๋“œ(property method)๋‚˜ ๋ฉ”์†Œ๋“œ(method)์™€ ํ•จ๊ป˜ ์ฝ”๋“œ์—์„œ entity์˜ ๊ตฌ์ฒด์ ์ธ ๊ตฌํ˜„ ๋ฐ ํŒจํ‚ค์ง€ํ™”ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

from typing import Union, Dict


class Record:
    pass


class User:
    info: str

    @property
    def data(self) -> Dict[str, str]:
        return {}

    def get_record(self) -> Union[Record, None]:
        return Record()

โฌ† ๋ชฉ์ฐจ๋กœ ์ด๋™



๊ฒ€์ƒ‰์— ์šฉ์ดํ•œ ์ด๋ฆ„์„ ์‚ฌ์šฉํ•ฉ์‹œ๋‹ค.

์šฐ๋ฆฌ๋Š” ์ฝ”๋”ฉ์„ ํ•˜๋ฉฐ ๋งŽ์€ ์ฝ”๋“œ๋ฅผ ์ฝ์Šต๋‹ˆ๋‹ค. ๋•Œ๋ฌธ์— ์šฐ๋ฆฌ๊ฐ€ ์ž‘์„ฑํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ฝ๊ธฐ ์‰ฝ๊ณ  ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•œ ์ด๋ฆ„์œผ๋กœ ์„ ์–ธํ•˜๋Š” ๊ฒƒ์€ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

๋งŒ์•ฝ ๋ณ€์ˆ˜๋ฅผ ์„ ์–ธํ•  ๋•Œ ์˜๋ฏธ๊ฐ€ ์—†๊ฑฐ๋‚˜ ๊ฒ€์ƒ‰์— ์–ด๋ ค์›€์„ ์ฃผ๋Š” ์ด๋ฆ„์œผ๋กœ ์„ ์–ธํ•œ๋‹ค๋ฉด, ์šฐ๋ฆฌ์˜ ์ฝ”๋“œ๋ฅผ ์ฝ๋Š” ๋‹ค๋ฅธ ์‚ฌ๋žŒ๋“ค์ด ํž˜๋“ค์–ดํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•œ(์œ ์ถ” ๊ฐ€๋Šฅํ•œ) ์ด๋ฆ„์„ ์‚ฌ์šฉํ•ฉ์‹œ๋‹ค.


๋‚˜์œ ์˜ˆ:

import time

# What is the number 86400 for again?
time.sleep(86400)

์ข‹์€ ์˜ˆ:

import time

# Declare them in the global namespace for the module.
SECONDS_IN_A_DAY = 60 * 60 * 24
time.sleep(SECONDS_IN_A_DAY)

โฌ† ๋ชฉ์ฐจ๋กœ ์ด๋™



๋ณ€์ˆ˜๋Š” ๋…๋ฆฝ์ ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

(explanatory)


๋‚˜์œ ์˜ˆ:

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)
if matches:
    print(f"{matches[1]}: {matches[2]}")

๋‚˜์˜์ง€๋Š” ์•Š์€ ์˜ˆ:

๋‚˜์˜์ง€๋Š” ์•Š์ง€๋งŒ, ์—ฌ์ „ํžˆ regex์˜ ๊ฒฐ๊ณผ์— ์˜์กดํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

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)
if matches:
    city, zip_code = matches.groups()
    print(f"{city}: {zip_code}")

์ข‹์€ ์˜ˆ:

ํ•˜์œ„ ํŒจํ„ด์˜ ์ด๋ฆ„์„ ์ง€์ •ํ•จ์œผ๋กœ์จ regex ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ์˜์กด์„ฑ์„ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import re

address = "One Infinite Loop, Cupertino 95014"
city_zip_code_regex = r"^[^,\\]+[,\\\s]+(?P<city>.+?)\s*(?P<zip_code>\d{5})?$"

matches = re.match(city_zip_code_regex, address)
if matches:
    print(f"{matches['city']}, {matches['zip_code']}")

โฌ† ๋ชฉ์ฐจ๋กœ ์ด๋™



์ฝ๋Š” ์‚ฌ๋žŒ์œผ๋กœ ํ•˜์—ฌ๊ธˆ ๊ธฐ๋Šฅ์„ ์œ ์ถ”ํ•˜๋„๋ก ๋งŒ๋“œ๋Š” ์ด๋ฆ„์„ ์ง“์ง€ ๋งˆ์„ธ์š”.

๋ณ€์ˆ˜๊ฐ€ ์˜๋ฏธํ•˜๋Š” ๋ฐ”๊ฐ€ ๋ฌด์—‡์ธ์ง€๋ฅผ ์ฝ”๋“œ๋ฅผ ์ƒ์„ธํžˆ ๋ณด์ง€ ์•Š์•„๋„ ์•Œ ์ˆ˜ ์žˆ๋„๋ก ํ•˜์„ธ์š”.

๋ช…์‹œ์ ์ธ ๊ฒƒ์ด ์•”๋ฌต์ ์ธ ๊ฒƒ๋ณด๋‹ค ์ข‹์Šต๋‹ˆ๋‹ค.


๋‚˜์œ ์˜ˆ:

seq = ("Austin", "New York", "San Francisco")

for item in seq:
    # do_stuff()
    # do_some_other_stuff()

    # Wait, what's `item` again?
    print(item)

์ข‹์€ ์˜ˆ:

locations = ("Austin", "New York", "San Francisco")

for location in locations:
    # do_stuff()
    # do_some_other_stuff()
    # ...
    print(location)

โฌ† ๋ชฉ์ฐจ๋กœ ์ด๋™



๋ถˆํ•„์š”ํ•œ context๋Š” ์ถ”๊ฐ€ํ•˜์ง€ ๋งˆ์„ธ์š”.

ํด๋ž˜์Šค/๊ฐ์ฒด ์ด๋ฆ„์ด ๋ฌด์–ธ๊ฐ€๋ฅผ ์ด๋ฏธ ์•Œ๋ ค์ฃผ๋Š” ๊ฒฝ์šฐ, ๋ณ€์ˆ˜ ์ด๋ฆ„์—์„œ ์ด๋ฅผ ๋ฐ˜๋ณตํ•˜์ง€ ๋งˆ์„ธ์š”.


๋‚˜์œ ์˜ˆ:

class Car:
    car_make: str
    car_model: str
    car_color: str

์ข‹์€ ์˜ˆ:

class Car:
    make: str
    model: str
    color: str

โฌ† ๋ชฉ์ฐจ๋กœ ์ด๋™



short circuiting ๋˜๋Š” conditionals ๋Œ€์‹  default parameter๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.

์—ฌ๊ธฐ์„œ short circuiting์€ ๋…ผ๋ฆฌ ์—ฐ์‚ฐ(and, or)๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.


๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ƒํ™ฉ์—์„œ:

import hashlib


def create_micro_brewery(name):
    name = "Hipster Brew Co." if name is None else name
    slug = hashlib.sha1(name.encode()).hexdigest()
    # etc.

๋งŒ์•ฝ ์œ„์™€ ๊ฐ™์ด ์กฐ๊ฑด๋ฌธ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ ๋Œ€์‹  ๋งค๊ฐœ๋ณ€์ˆ˜๋งŒ์„ ์‚ฌ์šฉํ•˜๋”๋ผ๋„ ํ•จ์ˆ˜์˜ ๋™์ž‘์— ์•„๋ฌด๋Ÿฐ ์˜ํ–ฅ์ด ์—†๋‹ค๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์šฐ๋ฆฌ๋Š” ์œ„ ์ฝ”๋“œ๋ฅผ ์•„๋ž˜์™€ ๊ฐ™์ด ์ˆ˜์ •ํ•˜๊ณ  ์‹ถ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.


์ข‹์€ ์˜ˆ:

import hashlib


def create_micro_brewery(name: str = "Hipster Brew Co."):
    slug = hashlib.sha1(name.encode()).hexdigest()
    # etc.

โฌ† ๋ชฉ์ฐจ๋กœ ์ด๋™



ํ•จ์ˆ˜

ํ•จ์ˆ˜๋Š” ์ž‘์—…์˜ ๋‹จ์œ„์ž…๋‹ˆ๋‹ค.

ํ•จ์ˆ˜๋Š” ์†Œํ”„ํŠธ์›จ์–ด ์—”์ง€๋‹ˆ์–ด๋ง์—์„œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ rule ์ค‘ ํ•˜๋‚˜์ž…๋‹ˆ๋‹ค.

ํ•จ์ˆ˜๋“ค์ด ํ•˜๋‚˜ ์ด์ƒ์˜ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•œ๋‹ค๋ฉด ๊ด€๋ฆฌ, ํ…Œ์ŠคํŠธ ๋ฐ ์ถ”๋ก ์— ์–ด๋ ค์›€์„ ๊ฒช์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.


ํ•จ์ˆ˜๋ฅผ ํ•˜๋‚˜์˜ ์ž‘์—…์œผ๋กœ ๋ถ„๋ฆฌํ•œ๋‹ค๋ฉด, ๋ฆฌํŽ™ํ† ๋ง(refactoring)์ด ์‰ฌ์›Œ์ง€๊ณ  ์ฝ”๋“œ๋ฅผ ํ›จ์”ฌ ๊นจ๋—ํ•˜๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋งŒ์•ฝ ์ด rule๋ฅผ ์ˆ™์ง€ํ•˜๊ณ  ์‹ค์ฒœํ•œ๋‹ค๋ฉด, ์—ฌ๋Ÿฌ๋ถ„์€ ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์„ ์•ž์„œ๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.



๋‚˜์œ ์˜ˆ:

from typing import List


class Client:
    active: bool


def email(client: Client) -> None:
    pass


def email_clients(clients: List[Client]) -> None:
    """Filter active clients and send them an email.
    """
    for client in clients:
        if client.active:
            email(client)



์ข‹์€ ์˜ˆ 1:

from typing import List


class Client:
    active: bool


def email(client: Client) -> None:
    pass


def get_active_clients(clients: List[Client]) -> List[Client]:
    """Filter active clients.
    """
    return [client for client in clients if client.active]


def email_clients(clients: List[Client]) -> None:
    """Send an email to a given list of clients.
    """
    for client in get_active_clients(clients):
        email(client)

์œ„ ์ฝ”๋“œ์—์„œ generator๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ถ€๋ถ„์ด ๋ณด์ด์‹œ๋‚˜์š”?



์ข‹์€ ์˜ˆ 2:

from typing import Generator, Iterator


class Client:
    active: bool


def email(client: Client):
    pass


def active_clients(clients: Iterator[Client]) -> Generator[Client, None, None]:
    """Only active clients"""
    return (client for client in clients if client.active)


def email_client(clients: Iterator[Client]) -> None:
    """Send an email to a given list of clients.
    """
    for client in active_clients(clients):
        email(client)

โฌ† ๋ชฉ์ฐจ๋กœ ์ด๋™



ํ•จ์ˆ˜์˜ ๋งค๊ฐœ๋ณ€์ˆ˜ (์ด์ƒ์ ์œผ๋กœ 2๊ฐœ ์ดํ•˜)

๋งค๊ฐœ๋ณ€์ˆ˜์˜ ์ˆ˜๊ฐ€ ๋งŽ๋‹ค๋Š” ๊ฒƒ์€ ์ผ๋ฐ˜์ ์œผ๋กœ ํ•จ์ˆ˜๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์€ ์ผ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. (has more than one responsibility)

๋•Œ๋ฌธ์— ๋งค๊ฐœ๋ณ€์ˆ˜์˜ ๊ฐœ์ˆ˜๋ฅผ ์ œํ•œํ•œ๋‹ค๋ฉด ํ•จ์ˆ˜๋ฅผ ๋” ์‰ฝ๊ฒŒ ํ…Œ์ŠคํŠธ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋งค๊ฐœ๋ณ€์ˆ˜๊ฐ€ ๋งŽ์€ ํ•จ์ˆ˜๋ฅผ ๋งค๊ฐœ๋ณ€์ˆ˜๊ฐ€ ์ ์€ ํ•จ์ˆ˜๋กœ ๋ถ„ํ•ดํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด ํ•ด๋ณด์„ธ์š”. ์ด์ƒ์ ์œผ๋กœ๋Š” 3๊ฐœ ๋ฏธ๋งŒ์ž…๋‹ˆ๋‹ค.


ํ•จ์ˆ˜์— ๋Œ€ํ•ด ๋‹จ์ผ ์ฑ…์ž„(single responsibility)์ด ์žˆ๋Š” ๊ฒฝ์šฐ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ํ•˜๋‚˜์˜ ํŠน์ˆ˜ํ•œ ๊ฐœ์ฒด๋กœ ๋ฌถ์„ ์ˆ˜ ์žˆ๋Š”์ง€๋„ ์‚ดํŽด๋ณด์„ธ์š”.

ํ”„๋กœ๊ทธ๋žจ์—์„œ ๋‹ค๋ฅธ ๊ณณ์— ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์žฌ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ์ด ์˜จ๋‹ค๋ฉด ์ด ๊ฐœ์ฒด๋ฅผ ์š”๊ธดํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


๋˜ํ•œ ์ด ๋ฐฉ๋ฒ•์ด ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๊ฐ–๋Š” ๊ฒƒ ๋ณด๋‹ค ๋” ๋‚˜์€ ์ด์œ ๋Š”

ํ•จ์ˆ˜ ๋‚ด๋ถ€์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ˆ˜ํ–‰๋˜๋Š” ์—ฐ์‚ฐ๋“ค์„ ๋˜ ํ•˜๋‚˜์˜ ํ•จ์ˆ˜๋กœ ๋งŒ๋“ค์–ด ๋ณต์žก์„ฑ์„ ์ค„์ผ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.


๋‚˜์œ ์˜ˆ:

def create_menu(title, body, button_text, cancellable):
    pass

java-esque (์ž๋ฐ” ํ‘œํ˜„๋ฒ•):

class Menu:
    def __init__(self, config: dict):
        self.title = config["title"]
        self.body = config["body"]
        # ...


menu = Menu(
    {
        "title": "My Menu",
        "body": "Something about my menu",
        "button_text": "OK",
        "cancellable": False
    }
)

์ข‹์€ ์˜ˆ 1:

class MenuConfig:
    """A configuration for the Menu.

    Attributes:
        title: The title of the Menu.
        body: The body of the Menu.
        button_text: The text for the button label.
        cancellable: Can it be cancelled?
    """
    title: str
    body: str
    button_text: str
    cancellable: bool = False


def create_menu(config: MenuConfig) -> None:
    title = config.title
    body = config.body
    # ...


config = MenuConfig()
config.title = "My delicious menu"
config.body = "A description of the various items on the menu"
config.button_text = "Order now!"
# The instance attribute overrides the default class attribute.
config.cancellable = True

create_menu(config)

์ข‹์€ ์˜ˆ 2:

from typing import NamedTuple


class MenuConfig(NamedTuple):
    """A configuration for the Menu.

    Attributes:
        title: The title of the Menu.
        body: The body of the Menu.
        button_text: The text for the button label.
        cancellable: Can it be cancelled?
    """
    title: str
    body: str
    button_text: str
    cancellable: bool = False


def create_menu(config: MenuConfig):
    title, body, button_text, cancellable = config
    # ...


create_menu(
    MenuConfig(
        title="My delicious menu",
        body="A description of the various items on the menu",
        button_text="Order now!"
    )
)

์ข‹์€ ์˜ˆ 3:

from dataclasses import astuple, dataclass


@dataclass
class MenuConfig:
    """A configuration for the Menu.

    Attributes:
        title: The title of the Menu.
        body: The body of the Menu.
        button_text: The text for the button label.
        cancellable: Can it be cancelled?
    """
    title: str
    body: str
    button_text: str
    cancellable: bool = False


def create_menu(config: MenuConfig):
    title, body, button_text, cancellable = astuple(config)
    # ...


create_menu(
    MenuConfig(
        title="My delicious menu",
        body="A description of the various items on the menu",
        button_text="Order now!"
    )
)

์ข‹์€ ์˜ˆ 4 (Python3.8+ only)

from typing import TypedDict


class MenuConfig(TypedDict):
    """A configuration for the Menu.

    Attributes:
        title: The title of the Menu.
        body: The body of the Menu.
        button_text: The text for the button label.
        cancellable: Can it be cancelled?
    """
    title: str
    body: str
    button_text: str
    cancellable: bool


def create_menu(config: MenuConfig):
    title = config["title"]
    # ...


create_menu(
    # You need to supply all the parameters
    MenuConfig(
        title="My delicious menu",
        body="A description of the various items on the menu",
        button_text="Order now!",
        cancellable=True
    )
)

โฌ† ๋ชฉ์ฐจ๋กœ ์ด๋™



ํ•จ์ˆ˜์˜ ์ด๋ฆ„์€ ํ•จ์ˆ˜๊ฐ€ ์ˆ˜ํ–‰ํ•˜๋Š” ์ž‘์—…์„ ๋‚˜ํƒ€๋‚ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


๋‚˜์œ ์˜ˆ:

class Email:
    def handle(self) -> None:
        pass


message = Email()
# What is this supposed to do again?
message.handle()

์ข‹์€ ์˜ˆ:

class Email:
    def send(self) -> None:
        """Send this message"""


message = Email()
message.send()

โฌ† ๋ชฉ์ฐจ๋กœ ์ด๋™



ํ•จ์ˆ˜์—๋Š” ์ถ”์ƒํ™”(abstraction)๊ฐ€ ํ•œ ์ธต๋งŒ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋งŒ์•ฝ ํ•จ์ˆ˜์— ์ถ”์ƒ์ ์ธ ์ธต์ด ํ•˜๋‚˜ ์ด์ƒ ์žˆ๋‹ค๋ฉด, ํ•จ์ˆ˜๊ฐ€ ๋„ˆ๋ฌด ๋ณต์žกํ•ด์ง‘๋‹ˆ๋‹ค.

์ถ”์ƒ์ธต์ด ์—ฌ๋Ÿฌ ๊ฐœ ์žˆ๋‹ค๋ฉด, ๊ทธ๊ฒƒ๋“ค์„ ํ•จ์ˆ˜๋กœ ๋ถ„ํ•ดํ•˜์—ฌ ์žฌ์‚ฌ์šฉ์„ฑ์„ ๋†’์ด๊ณ  ํ…Œ์ŠคํŠธ์— ์šฉ์ดํ•˜๋„๋ก ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.


๋‚˜์œ ์˜ˆ:

# type: ignore

def parse_better_js_alternative(code: str) -> None:
    regexes = [
        # ...
    ]

    statements = code.split('\n')
    tokens = []
    for regex in regexes:
        for statement in statements:
            pass

    ast = []
    for token in tokens:
        pass

    for node in ast:
        pass

์ข‹์€ ์˜ˆ:

from typing import Tuple, List, Dict

REGEXES: Tuple = (
    # ...
)


def parse_better_js_alternative(code: str) -> None:
    tokens: List = tokenize(code)
    syntax_tree: List = parse(tokens)

    for node in syntax_tree:
        pass


def tokenize(code: str) -> List:
    statements = code.split()
    tokens: List[Dict] = []
    for regex in REGEXES:
        for statement in statements:
            pass

    return tokens


def parse(tokens: List) -> List:
    syntax_tree: List[Dict] = []
    for token in tokens:
        pass

    return syntax_tree

โฌ† ๋ชฉ์ฐจ๋กœ ์ด๋™



ํ•จ์ˆ˜์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ flag๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ๋งˆ์„ธ์š”.

flag๋Š” ์‚ฌ์šฉ์ž๋กœ ํ•˜์—ฌ๊ธˆ ์ด ํ•จ์ˆ˜๊ฐ€ ๋‘๊ฐ€์ง€ ์ด์ƒ์˜ ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค๋Š” ๊ฒƒ์œผ๋กœ ๋ณด์—ฌ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•จ์ˆ˜๋Š” ํ•œ๊ฐ€์ง€ ์ผ์„ ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค. bool์„ ๊ธฐ์ค€์œผ๋กœ ํ•จ์ˆ˜์˜ ๊ธฐ๋Šฅ์ด ์™„์ „ํžˆ ๋ฐ”๋€๋‹ค๋ฉด ํ•จ์ˆ˜๋ฅผ ๋ถ„ํ• ํ•ด๋ณด์„ธ์š”.


๋‚˜์œ ์˜ˆ:

from tempfile import gettempdir
from pathlib import Path


def create_file(name: str, temp: bool) -> None:
    if temp:
        (Path(gettempdir()) / name).touch()
    else:
        Path(name).touch()

์ข‹์€ ์˜ˆ:

from tempfile import gettempdir
from pathlib import Path


def create_file(name: str) -> None:
    Path(name).touch()


def create_temp_file(name: str) -> None:
    (Path(gettempdir()) / name).touch()

โฌ† ๋ชฉ์ฐจ๋กœ ์ด๋™



ํ•จ์ˆ˜๋Š” ๋ถ€์ž‘์šฉ(side effect)์„ ํ”ผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์—ฌ๊ธฐ์„œ ๋งํ•˜๋Š” ๋ถ€์ž‘์šฉ(side effect)์€ ๋ถ€์ •์ ์ธ ์˜๋ฏธ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค.

ํ•จ์ˆ˜๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๋ฐ›์€ ํ›„ ์ผ๋ จ์˜ ์ž‘์—…์„ ๊ฑฐ์ณ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

๋งŒ์•ฝ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒƒ ์ด์™ธ์— ๋‹ค๋ฅธ ์ž‘์—…์„ ์ถ”๊ฐ€๋กœ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒฝ์šฐ ์ด ํ–‰์œ„๋ฅผ ๋ถ€์ž‘์šฉ์ด๋ผ ๋ถ€๋ฆ…๋‹ˆ๋‹ค.


์˜ˆ๋ฅผ ๋“ค์–ด ๋ถ€์ž‘์šฉ์œผ๋กœ ํŒŒ์ผ์— ๊ธ€์„ ์“ธ ์ˆ˜๋„ ์žˆ์œผ๋ฉฐ, ํŒŒ์ผ์˜ ํŠน์ • ๋ณ€์ˆ˜๋ฅผ ์ˆ˜์ •ํ•  ์ˆ˜๋„ ์žˆ๊ณ , ์‹ค์ˆ˜๋กœ ๋ชจ๋“  ๋ˆ์„ ๋‚ฏ์„  ์‚ฌ๋žŒ์—๊ฒŒ ์†ก๊ธˆํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

๋งŒ์•ฝ ๋ถ€์ž‘์šฉ์„ ๊ผญ ํ•„์š”๋กœ ํ•œ๋‹ค๋ฉด, ๋ถ€์ž‘์šฉ์ด ์œ ๋ฐœ๋˜๋Š” ์œ„์น˜๋ฅผ ํ‘œ์‹œํ•ด์ฃผ๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ ๋‹ค๋ฅธ ํ•จ์ˆ˜๋‚˜ ํด๋ž˜์Šค๊ฐ€ ๋™์‹œ์— ๋™์ผํ•œ ํŒŒ์ผ์„ ์กฐ์ž‘ํ•˜์ง€ ์•Š๋„๋ก ํ•˜๊ณ  ํŠน์ • ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ํŒŒ์ผ์„ ์ด ํŒŒ์ผ์„ ์กฐ์ž‘ํ•˜๋„๋ก ํ•ฉ์‹œ๋‹ค.


์ฃผ์š” ์š”์ ์€ ๊ฐœ์ฒด ๊ฐ„ ์ƒํƒœ ๊ณต์œ , ๊ฐ€๋ณ€ ๋ฐ์ดํ„ฐ ๋“ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ชจ๋“  ํ•จ์ˆ˜ ๋˜๋Š” ๋ณ€์ˆ˜๊ฐ€ ์ด๋Ÿฌํ•œ ๋ฐ์ดํ„ฐ(ํŒŒ์ผ ํ˜น์€ ํŒŒ์ผ ๋‚ด ๋ฐ์ดํ„ฐ)๋ฅผ ์กฐ์ž‘ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋Š” ์ผ๋ฐ˜์ ์ธ ํ•จ์ •์€ ํ”ผํ•  ํ•„์š”๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

๋งŒ์•ฝ ์ด๊ฒƒ์„ ์ž˜ ์ง€ํ‚จ๋‹ค๋ฉด, ๋‹ค๋ฅธ ํ”„๋กœ๊ทธ๋ž˜๋จธ๋“ค๋ณด๋‹ค ์˜ค๋ฅ˜๋ฅผ ์ฐพ๊ธฐ ๋” ์ˆ˜์›”ํ•ด์งˆ ๊ฒƒ์ž…๋‹ˆ๋‹ค.


๋‚˜์œ ์˜ˆ:

# type: ignore

# This is a module-level name.
# It's good practice to define these as immutable values, such as a string.
# However...
fullname = "Ryan McDermott"


def split_into_first_and_last_name() -> None:
    # The use of the global keyword here is changing the meaning of the
    # the following line. This function is now mutating the module-level
    # state and introducing a side-effect!
    global fullname
    fullname = fullname.split()


split_into_first_and_last_name()

# MyPy will spot the problem, complaining about 'Incompatible types in
# assignment: (expression has type "List[str]", variable has type "str")'
print(fullname)  # ["Ryan", "McDermott"]

# OK. It worked the first time, but what will happen if we call the
# function again?

์ข‹์€ ์˜ˆ 1:

from typing import List, AnyStr


def split_into_first_and_last_name(name: AnyStr) -> List[AnyStr]:
    return name.split()


fullname = "Ryan McDermott"
name, surname = split_into_first_and_last_name(fullname)

print(name, surname)  # => Ryan McDermott

์ข‹์€ ์˜ˆ 2:

from dataclasses import dataclass


@dataclass
class Person:
    name: str

    @property
    def name_as_first_and_last(self) -> list:
        return self.name.split()


# The reason why we create instances of classes is to manage state!
person = Person("Ryan McDermott")
print(person.name)  # => "Ryan McDermott"
print(person.name_as_first_and_last)  # => ["Ryan", "McDermott"]

โฌ† ๋ชฉ์ฐจ๋กœ ์ด๋™



ํด๋ž˜์Šค

๋‹จ์ผ ์ฑ…์ž„ ์›์น™ (Single Responsibility Principle; SRP)

์„ค๋ช…ํ•˜๊ธฐ์— ์•ž์„œ ์ฑ…์ž„(responsibility)๋ฅผ ์ดํ•ด๋ฅผ ์œ„ํ•ด ๊ธฐ๋Šฅ์œผ๋กœ ํ•ด์„ํ–ˆ์Œ์„ ๋ฏธ๋ฆฌ ์•Œ๋ ค๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.


Robert C. Martin์ด ๋งํ•˜๊ธฐ๋ฅผ...:

Class๊ฐ€ ๋ณ€๊ฒฝ๋  ์ด์œ ๋Š” ๋‹จ ํ•˜๋‚˜์—ฌ์•ผ ํ•œ๋‹ค. (A class should have only one reason to change.)

"๋ณ€๊ฒฝ๋˜์•ผ ํ•  ์ด์œ "๋Š” ํด๋ž˜์Šค ๋˜๋Š” ํ•จ์ˆ˜๊ฐ€ ๋‹ด๋‹นํ•˜๋Š” ๊ธฐ๋Šฅ์— ๋Œ€์‘ํ•ฉ๋‹ˆ๋‹ค.

๋‹ค์Œ ์˜ˆ์ œ์—์„œ๋Š” HTML ์ฃผ์„์„ ๋งŒ๋“ค๊ณ  ์ฃผ์„์— pip์˜ ๋ฒ„์ „์„ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค:


๋‚˜์œ ์˜ˆ:

from importlib import metadata


class VersionCommentElement:
     """An element that renders an HTML comment with the program's version number
     """

     def get_version(self) -> str:
          """Get the package version"""
          return metadata.version("pip")

     def render(self) -> None:
          print(f'<!-- Version: {self.get_version()} -->')


VersionCommentElement().render()

์œ„ ํด๋ž˜์Šค๋Š” ๋‘๊ฐ€์ง€ ๊ธฐ๋Šฅ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

  • pip ๋ฒ„์ „ ์ •๋ณด๋ฅผ ํš๋“ํ•ฉ๋‹ˆ๋‹ค.
  • HTML ์ฃผ์„์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

๋‹ค๋งŒ ์œ„ ์ฝ”๋“œ์—์„œ ํŠน์ • ๊ธฐ๋Šฅ์„ ๋ณ€๊ฒฝํ•˜๋ฉด ๋‹ค๋ฅธ ๊ธฐ๋Šฅ์— ์˜ํ–ฅ์„ ๋ฏธ์นฉ๋‹ˆ๋‹ค.
์šฐ๋ฆฌ๋Š” ์ด ๋‘ ๊ธฐ๋Šฅ์„ ๋ถ„ํ•ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


์ข‹์€ ์˜ˆ:

from importlib import metadata


def get_version(pkg_name: str) -> str:
     """Retrieve the version of a given package"""
     return metadata.version(pkg_name)


class VersionCommentElement:
     """An element that renders an HTML comment with the program's version number
     """

     def __init__(self, version: str):
          self.version = version

     def render(self) -> None:
          print(f'<!-- Version: {self.version} -->')


VersionCommentElement(get_version("pip")).render()

์œ„์™€ ๊ฐ™์ด ์ž‘์„ฑํ•˜๋ฉด ์ด ํด๋ž˜์Šค๋Š” HTML ์š”์†Œ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์—๋งŒ ์ง‘์ค‘ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์ธ์Šคํ„ด์Šคํ™”ํ•  ๋•Œ ๋ฒ„์ „ ๋ฒˆํ˜ธ๊ฐ€ ์ดˆ๊ธฐ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค. (get_version()์„ ํ†ตํ•ด ๋ฒ„์ „ ์ •๋ณด๋ฅผ ์–ป์Œ)

ํด๋ž˜์Šค ๋ฐ ํ•จ์ˆ˜๋Š” ์„œ๋กœ ๊ฒฉ๋ฆฌ๋˜์–ด ์žˆ์œผ๋ฉฐ ๋ฒ„์ „ ์‚ฌํ•ญ์ด ๋‹ค๋ฅธ ํ•ญ๋ชฉ์—๋Š” ์˜ํ–ฅ์„ ๋ฏธ์น˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ get_version()์€ ์žฌ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

โฌ† ๋ชฉ์ฐจ๋กœ ์ด๋™



๊ฐœ๋ฐฉ/ํ์‡„ ์›์น™ (Open/Closed Principle; OCP)

์†Œํ”„ํŠธ์›จ์–ด์˜ ๊ฐ์ฒด(ํด๋ž˜์Šค, ํ•จ์ˆ˜ ๋“ฑ)๋Š” ํ™•์žฅ(extension)์— ๋Œ€ํ•ด ์—ด๋ ค ์žˆ์–ด์•ผ ํ•˜์ง€๋งŒ, ์ˆ˜์ •(modification)์—๋Š” ๋‹ซํ˜€์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

ํด๋ž˜์Šค ๊ฐ™์€ ๊ฐœ์ฒด๋Š” ๋‚ด๋ถ€ ๋…ผ๋ฆฌ๋ฅผ ์ˆ˜์ •ํ•˜์ง€ ์•Š๊ณ  ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ณด์žฅํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.
(์›๋ž˜ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•˜์ง€ ์•Š์œผ๋ฉด์„œ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค๋Š” ์˜๋ฏธ์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.)

์ฆ‰, ๊ฐ์ฒด๋Š” ์„ค๊ณ„ ์ดˆ๊ธฐ์— ํ™•์žฅ์„ฑ์„ ๋ณด์žฅํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋‹ค์Œ ์˜ˆ์—์„œ๋Š” HTTP ์š”์ฒญ์— ์‘๋‹ตํ•˜๋Š” ๊ฐ„๋‹จํ•œ ์›น ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.

HTTP ์„œ๋ฒ„์—์„œ GET ์š”์ฒญ์„ ๋ฐ›์œผ๋ฉด View ํด๋ž˜์Šค .get() ๋ฉ”์†Œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.


View๋Š” ๋‹จ์ˆœํžˆ text/plain๋งŒ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์šฐ๋ฆฌ๋Š” text/HTML์˜ ํ˜•ํƒœ๋กœ ๋ฐ›๊ธฐ๋ฅผ ์›ํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์šฐ๋ฆฌ๋Š” View ํด๋ž˜์Šค๋ฅผ ์ƒ์†๋ฐ›์•„ TemplateView ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.


๋‚˜์œ ์˜ˆ:

from dataclasses import dataclass


@dataclass
class Response:
     """An HTTP response"""

     status: int
     content_type: str
     body: str


class View:
     """A simple view that returns plain text responses"""

     def get(self, request) -> Response:
          """Handle a GET request and return a message in the response"""
          return Response(
               status=200,
               content_type='text/plain',
               body="Welcome to my web site"
          )


class TemplateView(View):
     """A view that returns HTML responses based on a template file."""

     def get(self, request) -> Response:
          """Handle a GET request and return an HTML document in the response"""
          with open("index.html") as fd:
               return Response(
                    status=200,
                    content_type='text/html',
                    body=fd.read()
               )

์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด TemplateView๋Š” View๋ฅผ ์ƒ์†๋ฐ›๊ณ  .get() ๋ฉ”์†Œ๋“œ๋ฅผ ๋‹ค์‹œ ์ผ์Šต๋‹ˆ๋‹ค.

์œ„ ์ฝ”๋“œ๋Š” ๋ถ€๋ชจ ํด๋ž˜์Šค์˜ .get()์„ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๊ณ  ์ž์‹ ํด๋ž˜์Šค์—์„œ ์˜ค๋ฒ„๋ผ์ด๋”ฉ ํ•œ ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค.


๋งŒ์•ฝ ์œ„์™€ ๊ฐ™์€ ๋ฐฉ์‹์œผ๋กœ ๊ธฐ๋Šฅ์ด ์—ฌ๋Ÿฌ ๊ฐœ๋กœ ํŒŒ์ƒ๋œ๋‹ค๋ฉด,
ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•  ๋•Œ View ํด๋ž˜์Šค์˜ ๋ชจ๋“  ์ž์‹ ํด๋ž˜์Šค์— ๋Œ€ํ•ด ํ…Œ์ŠคํŠธ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด์•ผํ•  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์ฝ”๋“œ๋ฅผ ๋‹ค์‹œ ์„ค๊ณ„ํ•˜๊ณ  View ํด๋ž˜์Šค๊ฐ€ ๊นจ๋—ํ•˜๊ฒŒ ํ™•์žฅ๋˜๋„๋ก ํ•ฉ์‹œ๋‹ค.


์ข‹์€ ์˜ˆ 1:

from dataclasses import dataclass


@dataclass
class Response:
     """An HTTP response"""

     status: int
     content_type: str
     body: str


class View:
     """A simple view that returns plain text responses"""

     content_type = "text/plain"

     def render_body(self) -> str:
          """Render the message body of the response"""
          return "Welcome to my web site"

     def get(self, request) -> Response:
          """Handle a GET request and return a message in the response"""
          return Response(
               status=200,
               content_type=self.content_type,
               body=self.render_body()
          )


class TemplateView(View):
     """A view that returns HTML responses based on a template file."""

     content_type = "text/html"
     template_file = "index.html"

     def render_body(self) -> str:
          """Render the message body as HTML"""
          with open(self.template_file) as fd:
               return fd.read()

์‘๋‹ต ๋‚ด์šฉ์„ ๋ณ€๊ฒฝํ•˜๋ ค๋ฉด render_body()๋ฅผ ์žฌ์ •์˜ํ•ด์•ผ ํ•˜์ง€๋งŒ
์ด ๋ฉ”์†Œ๋“œ๋Š” ํ•˜์œ„ ์œ ํ˜•์„ ์žฌ์ •์˜ํ•˜๋„๋ก ์š”์ฒญํ•˜๋Š” ์ž˜ ์ •์˜๋œ ๋‹จ์ผ ์ฑ…์ž„(single reponsibility)์ด ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ์ด ๋ฐฉ๋ฒ•์€ ์ž์‹ ํด๋ž˜์Šค๊ฐ€ ๊ธฐ๋Šฅ์„ ํ™•์žฅํ•˜๊ธฐ ์œ„ํ•ด ์—ฌ์ „ํžˆ ์žฌ์ •์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ƒ์†(inheritance)๊ณผ ์ปดํฌ์ง€์…˜(composition)์˜ ์žฅ์ ์„ ๋ชจ๋‘ ์‚ฌ์šฉํ•˜๋Š” ๋˜ ๋‹ค๋ฅธ ์ข‹์€ ๋ฐฉ๋ฒ•์€
Mixins์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

Mixins์€ ๋‹ค๋ฅธ ๊ด€๋ จ ํด๋ž˜์Šค๋“ค๊ณผ๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ bare-bones classes์ž…๋‹ˆ๋‹ค.

target์˜ ๋™์ž‘(behaviour)์„ ๋ณ€๊ฒฝํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์ค‘ ์ƒ์†์„ ์‚ฌ์šฉํ•˜์—ฌ target ํด๋ž˜์Šค์™€ "mixed-in" ๋ฉ๋‹ˆ๋‹ค.


Rules:

  • Mixins๋Š” ํ•ญ์ƒ object์—์„œ ์ƒ์†๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • Mixins๋Š” ํ•ญ์ƒ target ํด๋ž˜์Šค ์•ž์— ์œ„์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
    • e.g. Foo(MixinA, MixinB, TargetClass)

์ข‹์€ ์˜ˆ 2:

from dataclasses import dataclass, field
from typing import Protocol


@dataclass
class Response:
     """An HTTP response"""

     status: int
     content_type: str
     body: str
     headers: dict = field(default_factory=dict)


class View:
     """A simple view that returns plain text responses"""

     content_type = "text/plain"

     def render_body(self) -> str:
          """Render the message body of the response"""
          return "Welcome to my web site"

     def get(self, request) -> Response:
          """Handle a GET request and return a message in the response"""
          return Response(
               status=200,
               content_type=self.content_type,
               body=self.render_body()
          )


class TemplateRenderMixin:
     """A mixin class for views that render HTML documents using a template file
 
     Not to be used by itself!
     """
     template_file: str = ""

     def render_body(self) -> str:
          """Render the message body as HTML"""
          if not self.template_file:
               raise ValueError("The path to a template file must be given.")

          with open(self.template_file) as fd:
               return fd.read()


class ContentLengthMixin:
     """A mixin class for views that injects a Content-Length header in the
     response
 
     Not to be used by itself!
     """

     def get(self, request) -> Response:
          """Introspect and amend the response to inject the new header"""
          response = super().get(request)  # type: ignore
          response.headers['Content-Length'] = len(response.body)
          return response


class TemplateView(TemplateRenderMixin, ContentLengthMixin, View):
     """A view that returns HTML responses based on a template file."""

     content_type = "text/html"
     template_file = "index.html"

์œ„ ์ฝ”๋“œ์—์„œ ๋ณผ ์ˆ˜ ์žˆ๋“ฏ์ด, Mixins๋Š” ๊ด€๋ จ ๊ธฐ๋Šฅ์„ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํด๋ž˜์Šค๋กœ ์บก์Šํ™”ํ•จ์œผ๋กœ์จ

๋” ์‰ฝ๊ฒŒ ํŒจํ‚ค์ง•ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์œผ๋ฉฐ, ๋‹จ์ผ ์ฑ…์ž„ ์›์น™(SRP)์—๋„ ๋ถ€ํ•ฉํ•ฉ๋‹ˆ๋‹ค.


Django๋„ ์—ฌ๋Ÿฌ ๊ฐ€์ง€์˜ View ํด๋ž˜์Šค๋ฅผ ๊ตฌ์„ฑํ•˜๊ธฐ ์œ„ํ•ด Mixins๋ฅผ ๋งŽ์ด ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

FIXME: typing.Protocol์˜ ์‚ฌ์šฉ ๋ฐฉ์‹์ด ์ •๋ฆฝ๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์— Mixins์— type ๊ฒ€์‚ฌ๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


โฌ† ๋ชฉ์ฐจ๋กœ ์ด๋™



๋ฆฌ์Šค์ฝ”ํ”„ ์น˜ํ™˜ ์›์น™ (Liskov Substitution Principle; LSP)


"๋ถ€๋ชจ ํด๋ž˜์Šค์˜ ํฌ์ธํ„ฐ๋‚˜ ์ฐธ์กฐ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํ•จ์ˆ˜๋Š”
๋ถ€๋ชจ ํด๋ž˜์Šค๋กœ๋ถ€ํ„ฐ ํŒŒ์ƒ๋œ ์ž์‹ ํด๋ž˜์Šค์— ๋Œ€ํ•ด ๋ชฐ๋ผ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ด.", Uncle Bob.


์ด ์›์น™์€ A behavioral notion of subtyping (1994) ๋…ผ๋ฌธ์˜ ์ €์ž Jeannette Wing๊ณผ ํ˜‘๋ ฅํ•œ Barbara Liskov์˜ ์ด๋ฆ„์„ ๋”ฐ์„œ ๋ช…๋ช…๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์ด ๋…ผ๋ฌธ์˜ ํ•ต์‹ฌ ์›์น™์€ "subtype์ด supertype์™€ ๋™์ผํ•œ ๋ฐฉ๋ฒ•๊ณผ ๋™์ผํ•œ ๊ธฐ๋Šฅ๊ณผ ๋™์ผ ํ–‰๋™์„ ์œ ์ง€ํ•ด์•ผ ํ•œ๋‹ค"๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๋‹ค์‹œ ๋งํ•ด supertype์˜ ํ•จ์ˆ˜๋Š” ๋ณ„๋„์˜ ์ˆ˜์ • ์—†์ด ๋ชจ๋“  subtype์„ ์ˆ˜์šฉํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


์•„๋ž˜์˜ ์ฝ”๋“œ์—์„œ ์–ด๋–ค ๋ฌธ์ œ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ•ด๋ณด๋„๋ก ํ•ฉ์‹œ๋‹ค.

๋‚˜์œ ์˜ˆ:

from dataclasses import dataclass


@dataclass
class Response:
     """An HTTP response"""

     status: int
     content_type: str
     body: str


class View:
     """A simple view that returns plain text responses"""

     content_type = "text/plain"

     def render_body(self) -> str:
          """Render the message body of the response"""
          return "Welcome to my web site"

     def get(self, request) -> Response:
          """Handle a GET request and return a message in the response"""
          return Response(
               status=200,
               content_type=self.content_type,
               body=self.render_body()
          )


class TemplateView(View):
     """A view that returns HTML responses based on a template file."""

     content_type = "text/html"

     def get(self, request, template_file: str) -> Response:  # type: ignore
          """Render the message body as HTML"""
          with open(template_file) as fd:
               return Response(
                    status=200,
                    content_type=self.content_type,
                    body=fd.read()
               )


def render(view: View, request) -> Response:
     """Render a View"""
     return view.get(request)

render() ๋ฉ”์†Œ๋“œ๋Š” View ํด๋ž˜์Šค ๋ฐ ํ•˜์œ„ ํด๋ž˜์Šค์ธ TemplateView์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ TemplateView๋Š” ์ƒ์† ์‹œ .get() ๋ฉ”์†Œ๋“œ์˜ signature(๋ฉ”์†Œ๋“œ์˜ ์ž…/์ถœ๋ ฅ)์„ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

TemplateView์˜ render()๋ฅผ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.


๋งŒ์•ฝ ์šฐ๋ฆฌ๊ฐ€ render()๋ฅผ View์™€ View์˜ ํŒŒ์ƒ ํด๋ž˜์Šค์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ธฐ๋ฅผ ์›ํ•œ๋‹ค๋ฉด,

์šฐ๋ฆฌ๋Š” ์™ธ๋ถ€ ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์†์ƒ๋˜์ง€ ์•Š๋„๋ก ์ฃผ์˜ํ•ด์•ผ ํ•  ํ•„์š”๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ ์ฃผ์–ด์ง„ ํด๋ž˜์Šค์—์˜ ๊ตฌ์„ฑ์„ ์–ด๋–ป๊ฒŒ ์•Œ ์ˆ˜ ์žˆ์„๊นŒ์š”?

mypy์™€ ๊ฐ™์€ type ๊ฒ€์‚ฌ ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด
์ด์™€ ๋น„์Šทํ•œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


error: Signature of "get" incompatible with supertype "View"
<string>:36: note:      Superclass:
<string>:36: note:          def get(self, request: Any) -> Response
<string>:36: note:      Subclass:
<string>:36: note:          def get(self, request: Any, template_file: str) -> Response

โฌ† ๋ชฉ์ฐจ๋กœ ์ด๋™



์ธํ„ฐํŽ˜์ด์Šค ๋ถ„๋ฆฌ ์›์น™ (Interface Segregation Principle; ISP)


โ€œ์‚ฌ์šฉ์ž๊ฐ€ ํ•„์š”์—†๋Š” ๊ฒƒ์— ์˜์กดํ•˜์ง€ ์•Š๋„๋ก ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ๊ฑด ์–ด๋•Œ?", Uncle Bob.


Java๋‚˜ Go์™€ ๊ฐ™์€ ์œ ๋ช…ํ•œ ๊ฐ์ฒด ์ง€ํ–ฅ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด์—์„œ๋Š” ์ธํ„ฐํŽ˜์ด์Šค(interface)๋ผ๋Š” ๊ฐœ๋…์ด ์žˆ์Šต๋‹ˆ๋‹ค.

์ธํ„ฐํŽ˜์ด์Šค ํด๋ž˜์Šค๋Š” ๊ณต๊ฐœ ๋ฉ”์†Œ๋“œ์™€ ์†์„ฑ์„ ๊ตฌํ˜„ํ•˜์ง€ ์•Š๊ณ  ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

ํ•จ์ˆ˜์˜ signature(ํ•จ์ˆ˜์˜ ์ž…/์ถœ๋ ฅ)๋ฅผ ์ •์˜ํ•˜๊ณ  ์‹ถ์ง€๋งŒ ๊ตฌ์ฒด์ ์œผ๋กœ ๊ตฌํ˜„ํ•˜๊ณ  ์‹ถ์ง€ ์•Š์„ ๋•Œ ์ธํ„ฐํŽ˜์ด์Šค๋Š” ๋งค์šฐ ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.


์šฐ๋ฆฌ๋Š” "๋‹น์‹ ์ด ๋‚˜์—๊ฒŒ ์ „๋‹ฌํ•œ ๋Œ€์ƒ์˜ ์„ธ๋ถ€ ์‚ฌํ•ญ์— ๋Œ€ํ•ด์„œ๋Š” ๊ด€์‹ฌ์ด ์—†๊ณ , ๋‚ด๊ฐ€ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ด๋‚˜ ์†์„ฑ์—๋งŒ ๊ด€์‹ฌ์ด ์žˆ๋‹ค."๊ณ  ๋งํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


Python์—๋Š” ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

๋‹ค๋งŒ ์ธํ„ฐํŽ˜์ด์Šค์™€๋Š” ์•ฝ๊ฐ„ ๋‹ค๋ฅด์ง€๋งŒ ์ถ”์ƒ ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋™์ผํ•œ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


์ข‹์€ ์˜ˆ:

from abc import ABCMeta, abstractmethod


# Define the Abstract Class for a generic Greeter object
class Greeter(metaclass=ABCMeta):
     """An object that can perform a greeting action."""

     @staticmethod
     @abstractmethod
     def greet(name: str) -> None:
          """Display a greeting for the user with the given name"""


class FriendlyActor(Greeter):
     """An actor that greets the user with a friendly salutation"""

     @staticmethod
     def greet(name: str) -> None:
          """Greet a person by name"""
          print(f"Hello {name}!")


def welcome_user(user_name: str, actor: Greeter):
     """Welcome a user with a given name using the provided actor"""
     actor.greet(user_name)


welcome_user("Barbara", FriendlyActor())

์ด์ œ ๋‹ค์Œ์˜ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ƒ์ƒํ•ด๋ด…์‹œ๋‹ค.

PDF ๋ฌธ์„œ๊ฐ€ ๋ช‡ ๊ฐœ ์žˆ๋Š”๋ฐ, ์šฐ๋ฆฌ ์›น ์‚ฌ์ดํŠธ ์‚ฌ์šฉ์ž์—๊ฒŒ PDF ํŒŒ์ผ์„ ์ œ๊ณตํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.

์šฐ๋ฆฌ๋Š” ํŒŒ์ด์ฌ ์›น ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋Ÿฌํ•œ ๋ฌธ์„œ๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ํด๋ž˜์Šค๋ฅผ ์„ค๊ณ„ํ•˜๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์šฐ๋ฆฌ๋Š” ๋ฌธ์„œ์— ์ถ”์ƒ ํด๋ž˜์Šค๋ฅผ ์„ค๊ณ„ํ–ˆ๋Š”๋ฐ, ์ด ํด๋ž˜์Šค์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ชจ๋“  ๊ธฐ๋Šฅ๋“ค์„ ์ ์–ด๋‘์—ˆ์Šต๋‹ˆ๋‹ค.


์—๋Ÿฌ ๋ฐœ์ƒ:

import abc


class Persistable(metaclass=abc.ABCMeta):
     """Serialize a file to data and back"""

     @property
     @abc.abstractmethod
     def data(self) -> bytes:
          """The raw data of the file"""

     @classmethod
     @abc.abstractmethod
     def load(cls, name: str):
          """Load the file from disk"""

     @abc.abstractmethod
     def save(self) -> None:
          """Save the file to disk"""


# We just want to serve the documents, so our concrete PDF document
# implementation just needs to implement the `.load()` method and have
# a public attribute named `data`.

class PDFDocument(Persistable):
     """A PDF document"""

     @property
     def data(self) -> bytes:
          """The raw bytes of the PDF document"""
          ...  # Code goes here - omitted for brevity

     @classmethod
     def load(cls, name: str):
          """Load the file from the local filesystem"""
          ...  # Code goes here - omitted for brevity


def view(request):
     """A web view that handles a GET request for a document"""
     requested_name = request.qs['name']  # We want to validate this!
     return PDFDocument.load(requested_name).data

ํ•˜์ง€๋งŒ ์•ˆ๋˜๋”๋ผ๊ณ ์š”! .save() ๋ฉ”์†Œ๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์œผ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

Can't instantiate abstract class PDFDocument with abstract method save.

์ด๊ฑด ์งœ์ฆ๋‚˜๋„ค์š”. ์šฐ๋ฆฌ๋Š” .save()๋ฅผ ๊ตฌํ˜„ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

์šฐ๋ฆฌ๋Š” ์•„๋ฌด๊ฒƒ๋„ ํ•˜์ง€ ์•Š๊ฑฐ๋‚˜ NotImplementedError๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋Š” ๋”๋ฏธ ๋ฉ”์†Œ๋“œ๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ๊ทธ๊ฒƒ๋˜ํ•œ ์“ธ๋ชจ์—†๋Š” ์ฝ”๋“œ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.


๋™์‹œ์— ๋งŒ์•ฝ ์šฐ๋ฆฌ๊ฐ€ ์ถ”์ƒ ํด๋ž˜์Šค์—์„œ .save()๋ฅผ ์ œ๊ฑฐํ•œ๋‹ค๋ฉด, ์‚ฌ์šฉ์ž๊ฐ€ ๋ฌธ์„œ๋ฅผ save ํ•  ๋•Œ ๋‹ค์‹œ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


๋ฌธ์ œ๋ฅผ ์š”์•ฝํ•˜๋ฉด, ์šฐ๋ฆฌ๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ผ๊ณ , ์ด ์ธํ„ฐํŽ˜์ด์Šค์—๋Š” ํ˜„์žฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋Š” ๋ช‡๊ฐ€์ง€ ํŠน์„ฑ์ด ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์ด ๋ฌธ์ œ์— ๋Œ€ํ•œ ํ•ด๊ฒฐ๋ฐฉ์‹์€ ์ด ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋” ์ž‘์€ ์ธํ„ฐํŽ˜์ด์Šค๋กœ ๋ถ„ํ•ดํ•˜๊ณ , ๊ฐ๊ฐ์˜ ์ƒˆ๋กœ์šด ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์ผ๋ถ€ ๋‚ด์šฉ์„ ๋‹ด๋‹นํ•˜๋„๋ก ๋งŒ๋“œ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.


์ข‹์€ ์˜ˆ:

import abc


class DataCarrier(metaclass=abc.ABCMeta):
     """Carries a data payload"""

     @property
     def data(self):
          ...


class Loadable(DataCarrier):
     """Can load data from storage by name"""

     @classmethod
     @abc.abstractmethod
     def load(cls, name: str):
          ...


class Saveable(DataCarrier):
     """Can save data to storage"""

     @abc.abstractmethod
     def save(self) -> None:
          ...


class PDFDocument(Loadable):
     """A PDF document"""

     @property
     def data(self) -> bytes:
          """The raw bytes of the PDF document"""
          ...  # Code goes here - omitted for brevity

     @classmethod
     def load(cls, name: str) -> None:
          """Load the file from the local filesystem"""
          ...  # Code goes here - omitted for brevity


def view(request):
     """A web view that handles a GET request for a document"""
     requested_name = request.qs['name']  # We want to validate this!
     return PDFDocument.load(requested_name).data

โฌ† ๋ชฉ์ฐจ๋กœ ์ด๋™



์˜์กด์„ฑ ์—ญ์ „ ์›์น™ (Dependency Inversion Principle; DIP)


"๊ตฌ์ฒด์ ์ธ ์„ธ๋ถ€ ์‚ฌํ•ญ(details)๋ณด๋‹ค๋Š” ์ถ”์ƒ(abstractions)์— ์˜์กดํ•˜๋Š” ๊ฑด ์–ด๋•Œ?", Uncle Bob.


CSV ํŒŒ์ผ์˜ ํ–‰์„ ์ฆ‰์‹œ ์ŠคํŠธ๋ฆฌ๋ฐํ•˜๋Š” HTTP Response๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” web view๋ฅผ ์ž‘์„ฑํ•˜๊ณ  ์‹ถ๋‹ค๊ณ  ์ƒ๊ฐํ•ด๋ณด์„ธ์š”.

์šฐ๋ฆฌ๋Š” ํŒŒ์ด์ฌ ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ์ œ๊ณตํ•˜๋Š” CSV writer๋ฅผ ์‚ฌ์šฉํ•˜๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค.


Bad

import csv
from io import StringIO


class StreamingHttpResponse:
     """A streaming HTTP response"""
     ...  # implementation code goes here


def some_view(request):
     rows = (
          ['First row', 'Foo', 'Bar', 'Baz'],
          ['Second row', 'A', 'B', 'C', '"Testing"', "Here's a quote"]
     )

     # Define a generator to stream data directly to the client
     def stream():
          buffer_ = StringIO()
          writer = csv.writer(buffer_, delimiter=';', quotechar='"')
          for row in rows:
               writer.writerow(row)
               buffer_.seek(0)
               data = buffer_.read()
               buffer_.seek(0)
               buffer_.truncate()
               yield data

     # Create the streaming response  object with the appropriate CSV header.
     response = StreamingHttpResponse(stream(), content_type='text/csv')
     response[
          'Content-Disposition'] = 'attachment; filename="somefilename.csv"'

     return response

์ฒซ ๊ตฌํ˜„์€ CSV writer ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

์ผ๋ถ€ ํ•˜์œ„ ์ž‘์—…์€ ํŒŒ์ผ์ฒ˜๋Ÿผ String I/O ๊ฐ์ฒด๋ฅผ ์กฐ์ž‘ํ•˜์—ฌ writer์— ๋ฐ์ดํ„ฐ๋ฅผ ์ผ์Šต๋‹ˆ๋‹ค.

์ด ๋ฐฉ๋ฒ•์€ ๋ฒˆ์žกํ•˜๊ณ  ์šฐ์•„ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.


๋” ์ข‹์€ ๋ฐฉ๋ฒ•์€ writer๊ฐ€ .write() ๋ฉ”์†Œ๋“œ๋ฅผ ํฌํ•จํ•˜๋Š” ๊ฐ์ฒด๋งŒ ํ•„์š”๋กœ ํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์ดํ•ดํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

StreamingHttpResponse ํด๋ž˜์Šค๊ฐ€ ์ฆ‰์‹œ ํด๋ผ์ด์–ธํŠธ๋กœ ๋‹ค์‹œ ์ŠคํŠธ๋ฆฌ๋ฐํ•  ์ˆ˜ ์žˆ๋„๋ก ์ƒˆ๋กœ์šด ํ–‰ ๋ฐ์ดํ„ฐ๋ฅผ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ํ•˜๋Š” dummy ๊ฐ์ฒด๋ฅผ ์ „๋‹ฌํ•˜๋Š” ๊ฒƒ์€ ์–ด๋–ค๊ฐ€์š”?


Good

import csv


class Echo:
     """An object that implements just the write method of the file-like
     interface.
     """

     def write(self, value):
          """Write the value by returning it, instead of storing in a buffer."""
          return value


def some_streaming_csv_view(request):
     """A view that streams a large CSV file."""
     rows = (
          ['First row', 'Foo', 'Bar', 'Baz'],
          ['Second row', 'A', 'B', 'C', '"Testing"', "Here's a quote"]
     )
     writer = csv.writer(Echo(), delimiter=';', quotechar='"')
     return StreamingHttpResponse(
          (writer.writerow(row) for row in rows),
          content_type="text/csv",
          headers={
               'Content-Disposition': 'attachment; filename="somefilename.csv"'},
     )

์œ„์™€ ๊ฐ™์ด ๊ตฌํ˜„ํ•˜๋ฉด ์ด์ „์˜ ๊ฒƒ๋ณด๋‹ค ํ›จ์”ฌ ๋‚ซ๊ณ  ์šฐ์•„ํ•ด์ง‘๋‹ˆ๋‹ค.

๋” ์ ์€ ์ฝ”๋“œ๋กœ ๋™์ผํ•œ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ–ˆ๋‹ค๋Š” ๊ฒƒ์€ ์žฅ์ ์ด ๋ถ„๋ช…ํ•ฉ๋‹ˆ๋‹ค.

์šฐ๋ฆฌ๋Š” writer ํด๋ž˜์Šค์—์„œ .write()๋ผ๋Š” ์ถ”์ƒ์ ์ธ ๋ฐฉ๋ฒ•์—๋งŒ ๊ด€์‹ฌ์ด ์žˆ๊ณ  ๋‚ด๋ถ€ ์„ธ๋ถ€ ์‚ฌํ•ญ์—๋Š” ๊ด€์‹ฌ์ด ์—†๋‹ค๋Š” ๊ฒƒ์„ ํ™œ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด ์˜ˆ์ œ๋Š” a submission made to the Django document์—์„œ ๊ฐ€์ง€๊ณ  ์˜จ ๊ฒƒ์ž…๋‹ˆ๋‹ค.


โฌ† ๋ชฉ์ฐจ๋กœ ์ด๋™



๋ฐ˜๋ณต์€ ์ง€์–‘ํ•ฉ์‹œ๋‹ค. (Don't repeat yourself; DRY)

์œ„ํ‚คํ”ผ๋””์•„์˜ ์ค‘๋ณต ๋ฐฐ์ œ ์›์น™ ๋ฌธ์„œ๋ฅผ ์‚ดํŽด๋ณด๊ณ  ์˜ค์„ธ์š”.


์ค‘๋ณต ์ฝ”๋“œ๋Š” ์ฝ”๋“œ ๋กœ์ง์„ ์ˆ˜์ •ํ•  ๋•Œ ์ค‘๋ณต๋˜๋Š” ๋ถ€๋ถ„๋„ ๋™์‹œ์— ์ˆ˜์ •ํ•ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

์ค‘๋ณต ์ฝ”๋“œ๊ฐ€ ๋งŽ์œผ๋ฉด ๋งŽ์„ ์ˆ˜๋ก ์ˆ˜์ • ์ž‘์—…๋Ÿ‰์ด ๋งŽ์•„์งˆ ์ˆ˜ ๋ฐ–์— ์—†๊ณ  ์˜ค๋ฅ˜ ๋ฐœ์ƒ ๊ฐ€๋Šฅ์„ฑ ๋˜ํ•œ ๋†’์•„์ง€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.


์‹๋‹น์„ ์šด์˜ํ•˜๊ณ  ํ† ๋งˆํ† , ์–‘ํŒŒ, ๋งˆ๋Š˜, ํ–ฅ์‹ ๋ฃŒ ๋“ฑ ์žฌ๊ณ ๋ฅผ ์กฐ์‚ฌํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•ด๋ด…์‹œ๋‹ค.

๋ฆฌ์ŠคํŠธ๊ฐ€ ์—ฌ๋Ÿฌ ๊ฐœ ์žˆ์œผ๋ฉด ํ† ๋งˆํ†  ํ•˜๋‚˜๋กœ ํ† ๋งˆํ† ๊ฐ€ ๋“ค์–ด๊ฐ„ ์š”๋ฆฌ๋ฅผ ๋งŒ๋“ค์—ˆ์„ ๋•Œ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋ฆฌ์ŠคํŠธ๋ฅผ ์ „๋ถ€ ์—…๋ฐ์ดํŠธ ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋ฐ˜๋Œ€๋กœ ๋ชฉ๋ก์ด ํ•˜๋‚˜๋งŒ ์žˆ์œผ๋ฉด ํ•˜๋‚˜๋งŒ ์—…๋ฐ์ดํŠธ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.


๊ณตํ†ต์ ์ด ๋งŽ์ง€๋งŒ ์ฝ”๋“œ ์ƒ์— ์•ฝ๊ฐ„ ๋‹ค๋ฅธ ๊ฒƒ์ด ์žˆ์–ด์„œ ์ค‘๋ณต ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์ข…์ข… ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ๊ทธ ์ฐจ์ด๋กœ ์ธํ•ด ๋™์ผํ•œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋‘ ๊ฐœ ์ด์ƒ์˜ ๊ฐœ๋ณ„ ํ•จ์ˆ˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

์ค‘๋ณต ์ฝ”๋“œ๋ฅผ ์ œ๊ฑฐํ•˜๋ ค๋ฉด ๋จผ์ € ๊ณตํ†ต ๋ถ€๋ถ„์„ ์ถ”์ƒํ™”ํ•œ ๋‹ค์Œ ํ•˜๋‚˜์˜ ํ•จ์ˆ˜/๋ชจ๋“ˆ/ํด๋ž˜์Šค๋กœ ๋‹ค๋ฅธ ๋ถ€๋ถ„์„ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


์ถ”์ƒ์  ์‚ฌ๊ณ ๋ฅผ ์ž˜ ํ•˜๋Š” ๊ฒƒ์€ ํ”„๋กœ๊ทธ๋ž˜๋จธ์—๊ฒŒ ์žˆ์–ด ๋งค์šฐ ์ค‘์š”ํ•œ ์Šคํ‚ฌ ์ค‘ ํ•˜๋‚˜์ž…๋‹ˆ๋‹ค.

๋‚˜์œ ์ถ”์ƒ์  ์‚ฌ๊ณ ๋กœ ์ธํ•œ ํ”ผํ•ด๋Š” ๋•Œ๋•Œ๋กœ ์ค‘๋ณต ์ฝ”๋“œ๋ณด๋‹ค ๋” ์‹ฌ๊ฐํ•œ ๋ฌธ์ œ์— ์ง๋ฉดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋งŒ์•ฝ ์ถ”์ƒ์  ์‚ฌ๊ณ ๋ฅผ ์ž˜ ํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด, ๊ทธ๋ ‡๊ฒŒ ํ•˜์…”์•ผ ํ•ฉ๋‹ˆ๋‹ค! ์ค‘๋ณต ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์ง€ ๋ง™์‹œ๋‹ค.

์ด๋ฅผ ์ง€ํ‚ค์ง€ ์•Š๋Š”๋‹ค๋ฉด ๋กœ์ง์„ ๋ณ€๊ฒฝํ•˜๊ณ ์ž ํ•  ๋•Œ ๋ณ€๊ฒฝํ•ด์•ผํ•  ๋ถ€๋ถ„์ด ๋งŽ๋‹ค๋Š” ๊ฒƒ์„ ๊ณง ์•Œ๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.


์ถ”์ƒ์  ์‚ฌ๊ณ ๋ฅผ ์ž˜ ํ•œ๋‹ค๋Š” ๊ฒƒ:
๋ฒˆ์—ญ์„ ํ•˜๋ฉฐ ์ œ์ผ ์ดํ•ดํ•˜๊ธฐ ์–ด๋ ค์› ๋˜ ๋ถ€๋ถ„์ด ๋ฐ”๋กœ abstraction์ด๋ผ๋Š” ๋ง์ธ๋ฐ,
๋งฅ๋ฝ์„ ๋ณด๋ฉด ๊ณตํ†ต์ ์ด๊ณ  ๋ณธ์งˆ์ ์ธ ๋ถ€๋ถ„๋งŒ ์ถ”์ถœํ•˜๊ณ  ๊ฐœ๋ณ„์ ์ธ ๋ถ€๋ถ„์€ ๋ฒ„๋ฆฐ๋‹ค๋Š” ์˜๋ฏธ ๊ฐ™์Šต๋‹ˆ๋‹ค.


๋‚˜์œ ์˜ˆ:

from typing import List, Dict
from dataclasses import dataclass


@dataclass
class Developer:
    def __init__(self, experience: float, github_link: str) -> None:
        self._experience = experience
        self._github_link = github_link

    @property
    def experience(self) -> float:
        return self._experience

    @property
    def github_link(self) -> str:
        return self._github_link


@dataclass
class Manager:
    def __init__(self, experience: float, github_link: str) -> None:
        self._experience = experience
        self._github_link = github_link

    @property
    def experience(self) -> float:
        return self._experience

    @property
    def github_link(self) -> str:
        return self._github_link


def get_developer_list(developers: List[Developer]) -> List[Dict]:
    developers_list = []
    for developer in developers:
        developers_list.append({
            'experience': developer.experience,
            'github_link': developer.github_link
        })
    return developers_list


def get_manager_list(managers: List[Manager]) -> List[Dict]:
    managers_list = []
    for manager in managers:
        managers_list.append({
            'experience': manager.experience,
            'github_link': manager.github_link
        })
    return managers_list


## create list objects of developers
company_developers = [
    Developer(experience=2.5, github_link='https://github.com/1'),
    Developer(experience=1.5, github_link='https://github.com/2')
]
company_developers_list = get_developer_list(developers=company_developers)

## create list objects of managers
company_managers = [
    Manager(experience=4.5, github_link='https://github.com/3'),
    Manager(experience=5.7, github_link='https://github.com/4')
]
company_managers_list = get_manager_list(managers=company_managers)

Good:

from typing import List, Dict
from dataclasses import dataclass


@dataclass
class Employee:
    def __init__(self, experience: float, github_link: str) -> None:
        self._experience = experience
        self._github_link = github_link

    @property
    def experience(self) -> float:
        return self._experience

    @property
    def github_link(self) -> str:
        return self._github_link


def get_employee_list(employees: List[Employee]) -> List[Dict]:
    employees_list = []
    for employee in employees:
        employees_list.append({
            'experience': employee.experience,
            'github_link': employee.github_link
        })
    return employees_list


## create list objects of developers
company_developers = [
    Employee(experience=2.5, github_link='https://github.com/1'),
    Employee(experience=1.5, github_link='https://github.com/2')
]
company_developers_list = get_employee_list(employees=company_developers)

## create list objects of managers
company_managers = [
    Employee(experience=4.5, github_link='https://github.com/3'),
    Employee(experience=5.7, github_link='https://github.com/4')
]
company_managers_list = get_employee_list(employees=company_managers)

โฌ† ๋ชฉ์ฐจ๋กœ ์ด๋™



Translations

์ด ๋ฌธ์„œ๋Š” ๋‹ค์–‘ํ•œ ์–ธ์–ด๋กœ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค:


โฌ† ๋ชฉ์ฐจ๋กœ ์ด๋™



About

๐Ÿ› Clean Code concepts adapted for Python

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Python 98.8%
  • Makefile 1.2%