Solid
: 견고한, 단단한, 고체의SOLID
의 모든 원칙들을 지키면 유연하고 유지보수하기 쉬운 견고한 코드가 됨1. 단일 책임 원칙 (Single Responsibility Principle)
2. 개방 폐쇄 원칙 (Open-Closed Principle)
3. 리스코프 치환 원칙 (Liskov Substitution Principle)
4. 인터페이스 분리 원칙 (Interface Segregation Principle)
5. 의존 관계 역전 원칙 (Dependency Inversion Principle)
➡ 객체 지향으로 만드는 프로그램이 커질수록 SOLID 원칙을 잘 지키는 것이 중요.
➡ 작고 간단한 프로그램을 만들 때 이 원칙을 모두 지키면 시간 낭비가 될 수도 있음.
➡ SOLID 원칙을 따르는 코드, 위반하는 코드를 알고 원칙을 적용했을 때 얻는 이점 파악.
➡ 시간 문제상 모든 원칙을 완벽하게 적용하지는 못하더라도 코드에서 개선해야할 점, 개선하지 않으면 생길 문제들을 파악하고는 있어야 함.
God object
: 여러 개의 책임을 자신의 속성과 행동으로 직접 수행하는 객체class Ship:
"""배 클래스"""
def __init__(self, fuel, fuel_per_hour, supplies, num_crew):
"""연료량, 시간당 연료 소비량, 물자량, 선원 수를 인스턴스 변수로 갖는다"""
self.fuel = fuel
self.fuel_per_hour = fuel_per_hour
self.supplies = supplies
self.num_crew = num_crew
def report_fuel(self):
"""연료량 보고 메소드"""
print("현재 연료는 {}l 남아 있습니다".format(self.fuel))
def load_fuel(self, amount):
"""연료 충전 메소드"""
self.fuel += amount
def report_supplies(self):
"""물자량 보고 메소드"""
print("현재 물자는 {}명분이 남아 있습니다".format(self.supplies))
def load_supplies(self, amount):
"""물자 보급 메소드"""
self.supplies += amount
def distribute_supplies_to_crew(self):
"""물자 배분 메소드"""
if self.supplies >= self.num_crew:
self.supplies -= self.num_crew
return True
print("물자가 부족하기 때문에 배분할 수 없습니다")
return False
def report_crew(self):
"""선원 수 보고 메소드"""
print("현재 선원 {}명이 있습니다".format(self.num_crew))
def load_crew(self, number):
"""선원 승선 메소드"""
self.num_crew += number
def run_engine_for_hours(self, hours):
"""엔진 작동 메소드"""
if self.fuel > self.fuel_per_hour * hours:
self.fuel -= self.fuel_per_hour * hours
print("엔진을 {}시간 동안 돌립니다!".format(hours))
else:
print("연료가 부족하기 때문에 엔진 작동을 시작할 수 없습니다")
ship = Ship(400, 10, 1000, 50)
ship.load_fuel(10)
ship.load_supplies(10)
ship.load_crew(10)
ship.distribute_supplies_to_crew()
ship.run_engine_for_hours(4)
ship.report_fuel()
ship.report_supplies()
ship.report_crew()
# 엔진을 4시간 동안 돌립니다!
# 현재 연료는 370l 남아 있습니다
# 현재 물자는 950명분이 남아 있습니다
# 현재 선원 60명이 있습니다
Ship
클래스는 연료, 물자, 선원, 엔진관련 책임을 가짐 = God object
class Ship:
"""배 클래스"""
def __init__(self, fuel, fuel_per_hour, supplies, num_crew):
self.fuel_tank = FuelTank(fuel)
self.crew_manager = CrewManager(num_crew)
self.supply_hold = SupplyHold(supplies, self.crew_manager)
self.engine = Engine(self.fuel_tank, fuel_per_hour)
class FuelTank:
"""연료 탱크 클래스"""
def __init__(self, fuel):
"""연료 탱크에 저장된 연료량을 인스턴스 변수로 갖는다"""
self.fuel = fuel
def load_fuel(self, amount):
"""연료 충전 메소드"""
self.fuel += amount
def use_fuel(self, amount):
"""연료 사용 메소드"""
if self.fuel - amount >= 0:
self.fuel -= amount
return True
print("연료가 부족합니다!")
return False
def report_fuel(self):
"""연료량 보고 메소드"""
print("현재 연료는 {}l 남아 있습니다".format(self.fuel))
class Engine:
"""엔진 클래스"""
def __init__(self, fuel_tank, fuel_per_hour):
"""연료 탱크 인스턴스와 시간당 연료 소비량을 인스턴스 변수로 갖는다"""
self.fuel_tank = fuel_tank
self.fuel_per_hour = fuel_per_hour
def run_for_hours(self, hours):
"""엔진 작동 메소드, 연료 탱크 인스턴스를 사용한다"""
if self.fuel_tank.use_fuel(self.fuel_per_hour * hours):
print("엔진을 {}시간 동안 돌립니다!".format(hours))
return True
print("연료가 부족하기 때문에 엔진 작동을 시작할 수 없습니다")
return False
class CrewManager:
"""선원 관리 클래스"""
def __init__(self, num_crew):
"""승선한 선원 수를 인스턴스 변수로 갖는다"""
self.num_crew = num_crew
def load_crew(self, number):
"""선원 승선 메소드"""
self.num_crew += number
def report_crew(self):
"""선원 수 보고 메소드"""
print("현재 선원 {}명이 있습니다".format(self.num_crew))
class SupplyHold:
"""물자 창고 클래스"""
def __init__(self, supplies, crew_manager):
"""물자량과 선원 관리 인스턴스를 인스턴스 변수로 갖는다"""
self.supplies = supplies
self.crew_manager = crew_manager
def load_supplies(self, amount):
"""물자 충전 메소드"""
self.supplies += amount
def distribute_supplies_to_crew(self):
"""물자 배분 메소드, 각 선원들에게 동일한 양의 물자를 배분한다"""
if self.supplies >= self.crew_manager.num_crew:
self.supplies -= self.crew_manager.num_crew
return True
print("물자가 부족하기 때문에 배분할 수 없습니다")
return False
def report_supplies(self):
"""물자량 보고 메소드"""
print("현재 물자는 {}명분이 남아 있습니다".format(self.supplies))
ship = Ship(400, 10, 1000, 50)
ship.fuel_tank.load_fuel(10)
ship.supply_hold.load_supplies(10)
ship.crew_manager.load_crew(10)
ship.supply_hold.distribute_supplies_to_crew()
ship.engine.run_for_hours(4)
ship.fuel_tank.report_fuel()
ship.supply_hold.report_supplies()
ship.crew_manager.report_crew()
# 엔진을 4시간 동안 돌립니다!
# 현재 연료는 370l 남아 있습니다
# 현재 물자는 950명분이 남아 있습니다
# 현재 선원 60명이 있습니다
👉 단일 책임 적용 후
Ship
클래스의 인스턴스는 동작을 자신의 메소드로 직접 안함
👉 관련된 책임을 담당한 다른 클래스의 인스턴스를 통해 그 동작 실행
👉 전체 코드의 길이는 길어졌지만 클래스 하나의 길이는 짧아짐
👉 그래야 클래스의 의미를 쉽게 파악할 수 있고 유지보수에 용이
👉 어떤 프로그램인지, 개발자에 따라 생각이 다르기에 어려운 단일 책임 원칙
👉 클래스를 작성할 때 클래스가 너무 많은 책임을 갖고 있는건 아닌지 항상 의심해야 함
👉 단일 책임을 원칙을 지키지 않아도 처음에는 문제가 없을 수 있음
👉 하지만 프로그램의 크기가 커질 수록 코드 수정이 힘듦
class AppleKeyboard:
"""애플 키보드 클래스"""
def __init__(self):
"""키보드 인풋과 터치바 인풋"""
self.keyboard_input = ""
def set_keyboard_input(self, input):
"""키보드 인풋 저장 메소드"""
self.keyboard_input = input
def send_keyboard_input(self):
"""키보드 인풋 전송 메소드"""
return self.keyboard_input
class KeyboardManager:
def __init__(self):
"""키보드 관리 클래스"""
self.keyboard = None
def connect_to_keyboard(self, keyboard):
"""키보드 교체 메소드"""
self.keyboard = keyboard
def get_keyboard_input(self):
"""유저가 키보드로 입력한 내용을 받아오는 메소드"""
return self.keyboard.send_keyboard_input()
keyboard_manager = KeyboardManager()
apple_keyboard = AppleKeyboard()
keyboard_manager.connect_to_keyboard(apple_keyboard)
apple_keyboard.set_keyboard_input("안녕하세요")
print(keyboard_manager.get_keyboard_input())
# 안녕하세요
class SamsungKeyboard:
"""삼성 키보드 클래스"""
def __init__(self):
"""키보드 인풋"""
self.user_input = ""
def save_user_input(self, input):
"""키보드 인풋 저장 메소드"""
self.user_input = input
def give_user_input(self):
"""키보드 인풋 전송 메소드"""
return self.user_input
class KeyboardManager:
def __init__(self):
self.keyboard = None
def connect_to_keyboard(self, keyboard):
self.keyboard = keyboard
def get_keyboard_input(self):
return self.keyboard.send_keyboard_input()
keyboard_manager = KeyboardManager()
samsung_keyboard = SamsungKeyboard()
keyboard_manager.connect_to_keyboard(samsung_keyboard)
samsung_keyboard.save_user_input("안녕하세요")
print(keyboard_manager.get_keyboard_input())
# AttributeError: 'SamsungKeyboard' object has no attribute 'send_keyboard_input'
class KeyboardManager:
def __init__(self):
self.keyboard = None
def connect_to_keyboard(self, keyboard):
self.keyboard = keyboard
def get_keyboard_input(self):
if isinstance(self.keyboard, AppleKeyboard):
return self.keyboard.send_keyboard_input()
elif isinstance(self.keyboard, SamsungKeyboard):
return self.keyboard.give_user_input()
새로운 키보드가 등장할 때마다 코드를 수정하면 개방 폐쇄 원칙 위반
키보드 매니저 코드를 그대로 두면서 새로운 키보드 코드를 추가할 수 있어야 함
from abc import ABC, abstractmethod
class Keyboard(ABC):
"""키보드 클래스"""
@abstractmethod
def save_input(self, content: str) -> None:
"""키보드 인풋 저장 메소드"""
pass
@abstractmethod
def send_input(self) -> str:
"""키보드 인풋 전송 메소드"""
pass
class AppleKeyboard(Keyboard):
def __init__(self):
self.keyboard_input = ""
def save_input(self, input):
self.keyboard_input = input
def send_input(self):
return self.keyboard_input
class SamsungKeyboard(Keyboard):
def __init__(self):
self.user_input = ""
def save_input(self, input):
self.user_input = input
def send_input(self):
return self.user_input
class KeyboardManager:
def __init__(self):
self.keyboard = None
def connect_to_keyboard(self, keyboard):
self.keyboard = keyboard
def get_keyboard_input(self):
return self.keyboard.send_input()
keyboard_manager = KeyboardManager()
apple_keyboard = AppleKeyboard()
samsung_keyboard = SamsungKeyboard()
keyboard_manager.connect_to_keyboard(apple_keyboard)
apple_keyboard.save_input("안녕하세요")
print(keyboard_manager.get_keyboard_input())
keyboard_manager.connect_to_keyboard(samsung_keyboard)
samsung_keyboard.save_input("안녕하세요")
print(keyboard_manager.get_keyboard_input())
# 안녕하세요
# 안녕하세요
Keyboard
클래스를 상속 받고 두 메소드를 오버라이딩 하면 됨확장 ⭕ : 추상 클래스를 상속받는 키보드기만 하면 언제든지 새로운 키보드 연결 가능
수정 ❌ : 추상 클래스를 상속받기만 하면KeyboardManager
클래스 수정할 필요 없음
👉 여러 개발자들이 추상 클래스를 기준으로 동시에 개발 가능
👉 더 쉽게 협력하고, 더 편하게 수정하기 위한 개방 폐쇄 원칙
자식 클래스가 부모 클래스의 변수의 타입을 바꾸거나 메소드의 파라미터 또는 리턴값의 타입 or 갯수를 바꾸는 경우
자식 클래스가 부모 클래스의 의도와 다르게 메소드를 오버라이딩 하는 경우
class Employee:
"""직원 클래스"""
company_name = "코드잇 버거"
raise_percentage = 1.03
def __init__(self, name, wage):
self.name = name
self._wage = wage
def raise_pay(self):
"""직원 시급을 인상하는 메소드"""
self._wage *= self.raise_percentage
@property
def wage(self):
return self._wage
def __str__(self):
"""직원 정보를 문자열로 리턴하는 메소드"""
return Employee.company_name + " 직원: " + self.name
class Cashier(Employee):
"""리스코프 치환 원칙을 지키지 않는 계산대 직원 클래스"""
burger_price = 4000
def __init__(self, name, wage, number_sold=0):
super().__init__(name, wage)
self.number_sold = number_sold
def raise_pay(self, raise_amount):
"""직원 시급을 인상하는 메소드"""
self.wage += self.raise_amount
@property
def wage(self):
return "시급 정보를 알려줄 수 없습니다"
Employee
클래스 raise_pay
메소드는 self
외에는 파라미터를 받지 않음Cashier
가 규약을 어김Employee
클래스 wage
메소드는 return
값이 숫자Cashier
는 return
값이 문자wage
메소드는 숫자를 리턴한다 라는 행동 규약을 어김class Rectangle:
"""직사각형 클래스"""
def __init__(self, width, height):
"""세로와 가로"""
self.width = width
self.height = height
def area(self):
"""넓이 계산 메소드"""
return self.width * self.height
@property
def width(self):
"""가로 변수 getter 메소드"""
return self._width
@width.setter
def width(self, value):
"""가로 변수 setter 메소드"""
self._width = value if value > 0 else 1
@property
def height(self):
"""세로 변수 getter 메소드"""
return self._height
@height.setter
def height(self, value):
"""세로 변수 setter 메소드"""
self._height = value if value > 0 else 1
class Square(Rectangle):
def __init__(self, side):
super().__init__(side, side)
@property
def width(self):
"""가로 변수 getter 메소드"""
return self._width
@width.setter
def width(self, value):
"""가로 변수 setter 메소드"""
self._width = value if value > 0 else 1
self._height = value if value > 0 else 1
@property
def height(self):
"""세로 변수 getter 메소드"""
return self._height
@height.setter
def height(self, value):
"""세로 변수 setter 메소드"""
self._width = value if value > 0 else 1
self._height = value if value > 0 else 1
rectangle_1 = Rectangle(4, 6)
rectangle_2 = Square(2)
rectangle_1.width = 3
rectangle_1.height = 7
print(rectangle_1.area()) # 21
rectangle_2.width = 3
rectangle_2.height = 7
print(rectangle_2.area()) # 49
rectangle_2
는 Square
의 인스턴스 이면서 Rectangle
의 인스턴스 이기도 함class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
@property
def width(self):
return self._width
@width.setter
def width(self, value):
self._width = value if value > 0 else 1
@property
def height(self):
return self._height
@height.setter
def height(self, value):
self._height = value if value > 0 else 1
class Square(Rectangle):
def __init__(self, side):
super().__init__(side, side)
width
, height
를 그대로 상속받게 되어 원칙 위반하지 않음정사각형은 직사각형 행동규약을 지키기 어려운 객체, 상속을 해선 안됨
A와 B의 포함관계뿐만 아니라 행동규약을 지킬 수 있는지 확인 후 상속
class Square:
def __init__(self, side):
self.side = side
def area(self):
return self.side * self.side
@property
def side(self):
return self._side
@side.setter
def side(self, value):
self._side = value if value > 0 else 1
리스코프 치환 원칙은 개발자들끼리 협력할 때 중요 ‼️
from abc import ABC, abstractmethod
class IMessage(ABC): # 인터페이스
@property
@abstractmethod
def content(self):
"""추상 getter 메소드"""
pass
@abstractmethod
def edit_content(self, new_content: str) -> None:
"""작성한 메시지를 수정하는 메소드"""
pass
@abstractmethod
def send(self, destination: str) -> bool:
"""작성한 메시지를 전송하는 메소드"""
pass
class Email(IMessage):
def __init__(self, content, owner_email):
"""이메일은 그 내용과 보낸 사람의 이메일 주소를 인스턴스 변수로 가짐"""
self._content = content
self.owner_email = owner_email
@property
def content(self):
"""_content 변수 getter 메소드"""
return self._content
def edit_content(self, new_content):
"""이메일 내용 수정 메소드"""
self._content = self.owner_email + "님의 메일\n" + new_content
def send(self, destination):
"""이메일 전송 메소드"""
print("{}에서 {}로 이메일 전송!\n내용: {}").format(self.owner_email, destination, self._content)
return True
class TextMessage(IMessage):
def __init__(self, content):
"""문자 메시지는 그 내용을 인스턴스 변수로 가짐"""
self._content = content
@property
def content(self):
"""_content 변수 getter 메소드"""
return self._content
def edit_content(self, new_content):
"""문자 메시지 내용 수정 메소드"""
self._content = new_content
def send(self, destination):
"""문자 메시지 전송 메소드"""
print("{}로 문자 메시지 전송!\n내용: {}").format(destination, self._content)
class TextReader:
"""인스턴스의 텍스트 내용을 읽어주는 클래스"""
def __init__(self):
self.texts = []
def add_text(self, text: IMessage):
# 메세지는 IMessage 인터페이스를 상속받은 클래스의 인스턴스여야 함
"""인스턴스 추가 메소드, 파라미터는 IMessage 인터페이스를 상속받을 것"""
self.texts.append(text)
def read_all_texts(self):
"""인스턴스 안에 있는 모든 텍스트 내용 출력"""
for text in self.texts:
print(text.content)
email = Email("안녕 잘 지내니? 오랜만이다!", "jahyeongu@gmail.com")
text_message = TextMessage("내일 시간 가능? 한 1시쯤 만나자")
text_reader = TextReader()
text_reader.add_text(email)
text_reader.add_text(text_message)
text_reader.read_all_texts()
# 안녕 잘 지내니? 오랜만이다!
# 내일 시간 가능? 한 1시쯤 만나자
class Memo(IMessage):
def __init__(self, content):
"""메모는 그 내용을 인스턴스 변수로 가짐"""
self._content = content
@property
def content(self):
"""_content 변수 getter 메소드"""
return self._content
def edit_content(self, new_content):
"""메모 내용 수정 메소드"""
self._content = new_content
def send(self, destination):
"""메모는 전송할 수 없음"""
print("메모는 아무 곳도 보낼 수 없습니다!")
return False
Memo
클래스는 send
메소드를 사용하지 않으나 오버라딩하도록 강요 받음from abc import ABC, abstractmethod
class IText(ABC):
@property
@abstractmethod
def content(self):
pass
@abstractmethod
def edit_content(self, new_content: str) -> None:
pass
class ISendable(ABC):
@abstractmethod
def send(self, destination: str) -> bool:
pass
class Email(IText, ISendable):
def __init__(self, content, owner_email):
self._content = content
self.owner_email = owner_email
@property
def content(self):
return self._content
def edit_content(self, new_content):
self._content = self.owner_email + "님의 메일\n" + new_content
def send(self, destination):
print("{}에서 {}로 이메일 전송!\n내용: {}").format(self.owner_email, destination, self._content)
return True
class TextMessage(IText, ISendable):
def __init__(self, content):
self._content = content
@property
def content(self):
return self._content
def edit_content(self, new_content):
self._content = new_content
def send(self, destination):
print("{}로 문자 메시지 전송!\n내용: {}").format(destination, self._content)
class TextReader:
def __init__(self):
self.texts = []
def add_text(self, text: IText):
self.texts.append(text)
def read_all_texts(self):
for text in self.texts:
print(text.content)
class Memo(IText):
def __init__(self, content):
self._content = content
@property
def content(self):
return self._content
def edit_content(self, new_content):
self._content = new_content
email = Email("안녕 잘 지내니? 오랜만이다!", "jahyeongu@gmail.com")
text_message = TextMessage("내일 시간 가능? 한 1시쯤 만나자")
memo = Memo("내일 2시까지 숙제 끝낼 것!")
text_reader = TextReader()
text_reader.add_text(email)
text_reader.add_text(text_message)
text_reader.add_text(memo)
text_reader.read_all_texts()
# 안녕 잘 지내니? 오랜만이다!
# 내일 시간 가능? 한 1시쯤 만나자
# 내일 2시까지 숙제 끝낼 것!
관련있는 기능끼리 한 인터페이스에 모으고 한 인터페이스가 지나치게 커지지 않도록 하겠다는 생각을 갖고 인터페이스를 설계
class Sword:
"""검 클래스"""
def __init__(self, damage):
self.damage = damage
def slash(self, other_character):
"""검 사용 메소드"""
other_character.get_damage(self.damage)
class GameCharacter:
"""게임 캐릭터 클래스"""
def __init__(self, name, hp, sword: Sword):
self.name = name
self.hp = hp
self.sword = sword
def attack(self, other_character):
"""다른 유저를 공격하는 메소드"""
if self.hp > 0:
self.sword.slash(other_character)
else:
print(self.name + "님은 사망해서 공격할 수 없습니다.")
def change_sword(self, new_sword):
"""검을 바꾸는 메소드"""
self.sword = new_sword
def get_damage(self, damage):
"""캐릭터가 공격받았을 때 자신의 체력을 깎는 메소드"""
if self.hp <= damage:
self.hp = 0
print(self.name + "님은 사망했습니다.")
else:
self.hp -= damage
def __str__(self):
"""남은 체력을 문자열로 리턴하는 메소드"""
return self.name + "님은 hp: {}이(가) 남았습니다.".format(self.hp)
game_character_1 = GameCharacter("박준규", 100, bad_sword)
game_character_2 = GameCharacter("구자현", 1000, good_sword)
game_character_1.attack(game_character_2)
game_character_1.attack(game_character_2)
game_character_1.attack(game_character_2)
game_character_2.attack(game_character_1)
print(game_character_1)
print(game_character_2)
# 박준규님은 사망했습니다.
# 박준규님은 hp: 0이(가) 남았습니다.
# 구자현님은 hp: 997이(가) 남았습니다.
GameCharacter
)Sword
)GameCharacter
클래스가 하위 모듈인 Sword
클래스의 구현 내용에 의존GameCharacter
클래스의 attack
메소드가 Sword
클래스의 slash
메소드에 의존attack
메소드가 잘 실행되려면 slash
메소드가 문제 없이 실행된다는 보장 있어야 함slash
메소드의 이름이 바뀌면 attack
메소드 수정 불가피from abc import ABC, abstractmethod
class IWeapon(ABC):
"""무기 클래스"""
@abstractmethod
def use_on(self, other_character):
pass
class Sword(IWeapon):
def __init__(self, damage):
self.damage = damage
def use_on(self, other_character):
other_character.get_damage(self.damage)
class Gun(IWeapon):
"""총 클래스"""
def __init__(self, damage, num_rounds):
self.damage = damage
self.num_rounds = num_rounds
def use_on(self, other_character):
"""총 사용 메소드"""
if self.num_rounds > 0:
other_character.get_damage(self.damage)
self.num_rounds -= 1
else:
print("총알이 없어 공격할 수 없습니다")
class GameCharacter:
def __init__(self, name, hp, weapon: IWeapon):
self.name = name
self.hp = hp
self.weapon = weapon
def attack(self, other_character):
if self.hp > 0:
self.weapon.use_on(other_character)
else:
print(self.name + "님은 사망해서 공격할 수 없습니다.")
def change_sword(self, new_sword):
self.sword = new_sword
def get_damage(self, damage):
if self.hp <= damage:
self.hp = 0
print(self.name + "님은 사망했습니다.")
else:
self.hp -= damage
def __str__(self):
return self.name + "님은 hp: {}이(가) 남았습니다.".format(self.hp)
bad_weapon = Sword(1)
good_weapon = Gun(100, 10)
game_character_1 = GameCharacter("박준규", 100, bad_weapon)
game_character_2 = GameCharacter("구자현", 1000, good_weapon)
game_character_1.attack(game_character_2)
game_character_1.attack(game_character_2)
game_character_1.attack(game_character_2)
game_character_2.attack(game_character_1)
print(game_character_1)
print(game_character_2)
# 박준규님은 사망했습니다.
# 박준규님은 hp: 0이(가) 남았습니다.
# 구자현님은 hp: 997이(가) 남았습니다.
➡ 의존 관계 역전 원칙은 개방-폐쇄 원칙을 지키는 하나의 방법
➡ 상위 모듈은 새 하위 모듈이 생겨도 기존 코드 수정없이 새 하위 모듈을 가져다 쓸 수 있음