TIL_30 : SOLID 원칙

JaHyeon Gu·2021년 8월 31일
0

OOP

목록 보기
5/5
post-thumbnail

🙄 SOLID의 각 원칙


  • Solid : 견고한, 단단한, 고체의
    👉 SOLID의 모든 원칙들을 지키면 유연하고 유지보수하기 쉬운 견고한 코드가 됨

1. 단일 책임 원칙 (Single Responsibility Principle)
2. 개방 폐쇄 원칙 (Open-Closed Principle)
3. 리스코프 치환 원칙 (Liskov Substitution Principle)
4. 인터페이스 분리 원칙 (Interface Segregation Principle)
5. 의존 관계 역전 원칙 (Dependency Inversion Principle)

➡ 객체 지향으로 만드는 프로그램이 커질수록 SOLID 원칙을 잘 지키는 것이 중요.
➡ 작고 간단한 프로그램을 만들 때 이 원칙을 모두 지키면 시간 낭비가 될 수도 있음.
➡ SOLID 원칙을 따르는 코드, 위반하는 코드를 알고 원칙을 적용했을 때 얻는 이점 파악.
➡ 시간 문제상 모든 원칙을 완벽하게 적용하지는 못하더라도 코드에서 개선해야할 점, 개선하지 않으면 생길 문제들을 파악하고는 있어야 함.



🙄 단일 책임 원칙 (Single Responsibility Principle)


  • 모든 클래스는 단 한가지의 책임만을 갖고, 클래스 안에 정의되어 있는 모든 기능은, 이 하나의 책임을 수행하는데 집중되어 있어야 한다.
    👉 한 가지 책임이란 사람들마다 생각이 다르고 상황에 따라서도 다름
  • 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 클래스의 인스턴스는 동작을 자신의 메소드로 직접 안함
👉 관련된 책임을 담당한 다른 클래스의 인스턴스를 통해 그 동작 실행
👉 전체 코드의 길이는 길어졌지만 클래스 하나의 길이는 짧아짐
👉 그래야 클래스의 의미를 쉽게 파악할 수 있고 유지보수에 용이
👉 어떤 프로그램인지, 개발자에 따라 생각이 다르기에 어려운 단일 책임 원칙
👉 클래스를 작성할 때 클래스가 너무 많은 책임을 갖고 있는건 아닌지 항상 의심해야 함
👉 단일 책임을 원칙을 지키지 않아도 처음에는 문제가 없을 수 있음
👉 하지만 프로그램의 크기가 커질 수록 코드 수정이 힘듦



🙄 개방 폐쇄 원칙 (Open-Closed Principle)


  • 클래스는 확장에 열려 있어야하며, 수정에는 닫혀 있어야한다
  • 어떤 클래스의 코드를 수정하지 않아도 기존 기능을 확장할 수 있어야 됨
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 클래스 수정할 필요 없음
👉 여러 개발자들이 추상 클래스를 기준으로 동시에 개발 가능
👉 더 쉽게 협력하고, 더 편하게 수정하기 위한 개방 폐쇄 원칙



🙄 리스코프 치환 원칙 (Liskov Substitution Principle)


  • 부모 클래스의 인스턴스를 사용하는 위치에 자식 클래스의 인스턴스를 대신 사용했을 때 코드가 원래 의도대로 작동해야 한다
  • 부모 클래스의 행동규약을 자식 클래스가 위반하지 말 것

부모 클래스의 행동규약을 자식클래스가 어긴다는 것은?

  • 자식 클래스가 부모 클래스의 변수와 메소드를 상속 받기만 하면 문제 없음
  • 자식 클래스가 부모 클래스의 변수와 메소드를 오버라이딩하면 문제 발생

자식 클래스가 오버라이딩을 잘못 하는 경우

  1. 자식 클래스가 부모 클래스의 변수의 타입을 바꾸거나 메소드의 파라미터 또는 리턴값의 타입 or 갯수를 바꾸는 경우

  2. 자식 클래스가 부모 클래스의 의도와 다르게 메소드를 오버라이딩 하는 경우


오버라이딩 잘못한 예시 (1번 관련)

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 값이 숫자
    👉 자식 클래스 Cashierreturn 값이 문자
    👉 wage 메소드는 숫자를 리턴한다 라는 행동 규약을 어김

오버라이딩 잘못한 예시 (2번 관련)

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_2Square의 인스턴스 이면서 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의 포함관계뿐만 아니라 행동규약을 지킬 수 있는지 확인 후 상속


Square 클래스 맞게 수정

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  

리스코프 치환 원칙은 개발자들끼리 협력할 때 중요 ‼️



🙄 인터페이스 분리 원칙 (Interface Segregation Principle)


  • 클래스가 사용하지 않을 메소드에 의존할 것을 강요하면 안 됨
    👉 클래스가 나중에 사용하지도 않을 메소드를 가지도록 강제하지 말라는 뜻

인터페이스란?

  • 추상 클래스 중에서 추상 메소드만 있고 일반 메소드는 없는 것
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시쯤 만나자

Memo 기능이 필요해져 새로운 클래스 생성

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 메소드를 사용하지 않으나 오버라딩하도록 강요 받음
  • 해결책은 더 작은 인터페이스로 분리하는 것
  • 뚱뚱한 인터페이스 : 너무 많은 메소드를 한번에 갖고 있는 인터페이스
  • 역할 인터페이스(role interface) : 뚱뚱한 인터페이스에서 분리된 작은 인터페이스

IMessage 인터페이스 분리, 코드 수정

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시까지 숙제 끝낼 것!

관련있는 기능끼리 한 인터페이스에 모으고 한 인터페이스가 지나치게 커지지 않도록 하겠다는 생각을 갖고 인터페이스를 설계



🙄 의존 관계 역전 원칙 (Dependency Inversion Principle)


  • 상위 모듈하위 모듈의 구현 내용에 의존하면 안 됨
  • 상위 모듈하위 모듈 모두 추상화된 내용에 의존해야 함
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이(가) 남았습니다.

➡ 의존 관계 역전 원칙은 개방-폐쇄 원칙을 지키는 하나의 방법
➡ 상위 모듈은 새 하위 모듈이 생겨도 기존 코드 수정없이 새 하위 모듈을 가져다 쓸 수 있음

profile
IWBAGDS

0개의 댓글

관련 채용 정보