행위

파이썬 클래스 설계방법

DB CAFE

Dbcafe (토론 | 기여)님의 2020년 9월 23일 (수) 10:28 판
thumb_up 추천메뉴 바로가기


1 클래스 설계 방법론[편집]

1.1 5가지 클래스 설계의 원칙 (S.O.L.I.D)[편집]

  1. S - SRP(Single responsibility principle) 단일 책임 원칙
  2. O - OCP(Open Closed Principle) 개방 - 폐쇄 원칙
  3. L - LSP(Liskov Substitusion Principle) 리스코프 치환 법칙
  4. I - ISP(Interface Segregation Principle) 인터페이스 분리 원칙
  5. D - DIP(Dependency Inversion Principle) 의존성 역전 법칙

출처) https://scotch.io/bar-talk/s-o-l-i-d-the-first-five-principles-of-object-oriented-design

1.1.1 SRP(Single responsibility principle) 단일 책임 원칙[편집]

  1. 클래스는 단 한개의 책임을 가져야 함 (클래스를 수정할 이유가 오직 하나)
    예: 계산기 기능 구현시, 계산을 하는 책임과 GUI를 나타낸다는 책임을 서로 분리하여, 각각 클래스로 설계
  2. 실제 애매한 부분이 많이 존재함, 가급적 설계시 고려하면 좋음.
    1. 나쁜 예
      1. 학생성적과 수강하는 코스를 한개의 class에서 다루는 예
      2. 한 클래스에서 두개의 책임을 갖기 때문에, 수정이 용이하지 않다.
class StudentScoreAndCourseManager(object):
    def __init__(self):
        scores = {}
        courses = {}
        
    def get_score(self, student_name, course):
        pass
    
    def get_courses(self, student_name):
        pass
    1. 변경 예
      1. 각각의 책임을 한개로 줄여서, 각각 수정이 다른 것에 영향을 미치지 않도록 함
class ScoreManager(object):
    def __init__(self):
        scores = {}
        
    def get_score(self, student_name, course):
        pass
    
    
class CourseManager(object):
    def __init__(self):
        courses = {}
    
    def get_courses(self, student_name):
        pass

1.1.2 OCP(Open Closed Principle) 개방-폐쇄 원칙[편집]

  1. 확장에는 열려있어야 하고, 변경에는 닫혀있어야 함
    예: 캐릭터 클래스를 만들 때, 캐릭터마다 행동이 다르다면, 행동 구현은 캐릭터 클래스의 자식 클래스에서 재정의(Method Override)한다.
    이 경우, 캐릭터 클래스는 수정할 필요 없고(변경에 닫혀 있음)
    자식 클래스에서 재정의하면 됨(확장에 대해 개방됨)
    1. 나쁜 예
class Rectangle(object):
    def __init__(self, width, height):
        self.width = width
        self.height = height
        
class Circle:
    def __init__(self, radius):
        self.radius = radius

class AreaCalculator(object):
    def __init__(self, shapes):
        self.shapes = shapes

    def total_area(self):
        total = 0
        for shape in self.shapes:
            total += shape.width * shape.height
        return total

shapes = [Rectangle(2, 3), Rectangle(1, 6)]
calculator = AreaCalculator(shapes)
print("The total area is: ", calculator.total_area())
    1. 좋은 예
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height
    
class Circle:
    def __init__(self, radius):
        self.radius = radius
        
    def area(self):
        return 3.14 * self.radius ** 2
    
    
'''다른 도형에 대해 확장하기 위해서,
AreaCalculator는 수정이 필요 없음 (변경에 닫혀 있음)
단지, Shape을 상속받은 다른 class를 정의하기만 하면 됨 (확장에 대해 개방됨)
'''
class AreaCalculator(object):
    def __init__(self, shapes):
        self.shapes = shapes

    def total_area(self):
        total = 0
        for shape in self.shapes:
            total += shape.area()
        return total


shapes = [Rectangle(1, 6), Rectangle(2, 3), Circle(5), Circle(7)]
calculator = AreaCalculator(shapes)

print("The total area is: ", calculator.total_area())

1.1.3 LSP(Liskov Substitusion Principle) 리스코프 치환 법칙[편집]

  1. 자식 클래스는 언제나 자신의 부모클래스와 교체할 수 있다는 원칙
  • 갤럭시폰 is a kind of 스마트폰
    • 스마트폰은 다른 사람과 전화와 메시지가 가능하다.
    • 스마트폰은 데이터 또는 와이파이를 이용해 인터넷을 사용할 수 있다.
    • 스마트폰은 앱 마켓을 통해 앱을 다운 받을 수 있다.
  • 위 설명을 갤럭시 폰으로 대체하면 아래와 같다.
    • 갤럭시 폰은 다른 사람과 전화와 메시지가 가능하다.
    • 갤럭시 폰은 데이터 또는 와이파이를 이용해 인터넷을 사용할 수 있다.
    • 스마트폰은 앱 마켓을 통해 앱을 다운 받을 수 있다.
  1. 연습3
    다음 캐릭터의 메서드를 모두 담은 클래스를 만든다면?
    어떻게 하면 OCP 원칙을 고려할 수 있을까요?
    1. Warrior
      - attack: 상대방 객체를 입력받아서, '칼로 찌르다' 출력하고, 상대방의 receive 메서드를 호출해서, striking_power만큼 상대방의 health_point를 낮춰준다.
      - receive: 상대방의 striking_point를 입력으로 받아서, 자신의 health_point를 그만큼 낮추기, health_point가 0 이하이면 '죽었음' 출력
      - use_shield: 1번 공격을 막는다.
    2. Elf
      - attack: 상대방 객체를 입력받아서, '마법을 쓰다' 출력하고, 상대방의 receive 메서드를 호출해서, striking_power만큼 상대방의 health_point를 낮춰준다.
      - receive: 상대방의 striking_point를 입력으로 받아서, 자신의 health_point를 그만큼 낮추기, health_point가 0 이하이면 '죽었음' 출력
      - wear_manteau: 1번 공격을 막는다.
    3. Wizard
      - attack: 상대방 객체를 입력받아서, '마법을 쓰다' 출력하고, 상대방의 receive 메서드를 호출해서, striking_power만큼 상대방의 health_point를 낮춰준다.
      - receive: 상대방의 striking_point를 입력으로 받아서, 자신의 health_point를 그만큼 낮추기, health_point가 0 이하이면 '죽었음' 출력
      - use_wizard: 자신의 health_point를 3씩 올려준다.
# 추상 클래스 선언하기
from abc import *

class Character(metaclass=ABCMeta):
    def __init__(self, name='yourname', health_point=100, striking_power=3, defensive_power=3):
        self.name = name
        self.health_point = health_point
        self.striking_power = striking_power
        self.defensive_power = defensive_power        
    
    def get_info(self):
        print (self.name, self.health_point, self.striking_power, self.defensive_power)
    
    @abstractmethod
    def attack(self, second):
        pass

    @abstractmethod
    def receive(self):
        pass

    @abstractmethod
    def special(self):
        pass

1.1.4 ISP(Interface Segregation Principle) 인터페이스 분리 원칙[편집]

  1. 클래스에서 사용하지 않는(상관없는) 메서드는 분리해야 한다.
  1. 추상 클래스 선언하기
from abc import *

class Character(metaclass=ABCMeta):
    @abstractmethod
    def attack(self):
        pass
    
    @abstractmethod
    def move(self):
        pass

    @abstractmethod
    def eat(self):
        pass
  • metaclass 란?
    • 클래스를 만들기 위해 파이썬에서는 기본 metaclass가 사용됨
      • 즉, 클래스를 만들기 위해서 메타클래스 라는 것이 필요했던 것임
      • class 생성시, () 아무 것도 넣지 않으면, 기본 파이썬에서 클래스를 만들기 위한 메타클래스가 쓰인다고 보면 됨
      • 추상 클래스 만들시에는 기본 메타클래스로는 생성이 어려우니, 다음과 같이 작성
        • class Character(metaclass=ABCMeta)
      • 싱글톤을 위해 기본 메타클래스를 바꾸는 것임 (싱글톤은 다음에 나오는 디자인 패턴에서 설명)
        • class PrintObject(metaclass=Singleton)
class MyClass:
    pass

<source lang=python>
from abc import *

class Character(metaclass=ABCMeta):
    @abstractmethod
    def attack(self):
        pass
  1. 나쁜 예
    추상 클래스 상속하기
class Elf(Character):
    def attack(self):
        print ("practice the black art")
    
    def move(self):
        print ("fly")

    def eat(self):
        print ("no eat")  # <--- 요정은 밥을 안먹지 않을까요? 그래도 선언해줘야 함(상관없는 기능)

        
class Human(Character):
    def attack(self):
        print ("plunge a knife")
    
    def move(self):
        print ("run")

    def eat(self):
        print ("eat foods")


  1. 첫 번째 예: 이렇게 작성하는 것이 우선 위 코드보다는 더 좋음1
    추상 클래스 선언하기
from abc import *

class Character(metaclass=ABCMeta):
    @abstractmethod
    def attack(self):
        pass
    
    @abstractmethod
    def move(self):
        pass
  1. 추상 클래스 상속하기
class Elf(Character):
    def attack(self):
        print ("practice the black art")
    
    def move(self):
        print ("fly")
        
class Human(Character):
    def attack(self):
        print ("plunge a knife")
    
    def move(self):
        print ("run")

    def eat(self):  # <--- 메서드 확장
        print ("eat foods")
elf1 = Elf()
human1 = Human()

elf1.attack()
elf1.move()
human1.attack()
human1.move()
human1.eat()
practice the black art
fly
plunge a knife
run
eat foods


  1. 두 번째 예: 이렇게 작성하는 것도 처음 코드보다는 더 좋음
from abc import *

class AttackingWay(metaclass=ABCMeta):
    @abstractmethod
    def attack(self):
        pass

class MovingWay(metaclass=ABCMeta):
    @abstractmethod
    def move(self):
        pass

class EatingWay(metaclass=ABCMeta):
    @abstractmethod
    def eat(self):
        pass

class AbstractHumanCharacter(AttackingWay, MovingWay, EatingWay):
    pass
# 추상 클래스 상속하기
class Elf(AttackingWay, MovingWay):
    def attack(self):
        print ("practice the black art")
    
    def move(self):
        print ("fly")
        
class Human(AttackingWay, MovingWay, EatingWay):
    def attack(self):
        print ("plunge a knife")
    
    def move(self):
        print ("run")

    def eat(self):
        print ("eat foods")
elf1 = Elf()
human1 = Human()

elf1.attack()
elf1.move()
human1.attack()
human1.move()
human1.eat()
practice the black art
fly
plunge a knife
run
eat foods
  1. 한발짝 더 나가보기!(심화 문제)
    게임 캐릭터 클래스 설계 예제 상기해봅니다. 다음 세 캐릭터의 다양한 메서드를 위 두번째 방법과 유사하게 작성해볼까요?
    게임 캐릭터는 다음과 같이 3명이 존재하고, 각각의 메서드는 다음과 같음
  • Warrior
    • - 공격하면 칼로 찌른다를 출력
  • Elf
    • - 공격하면 마법을 쓴다를 출력
  • Wizard
    • - 공격하면 마법을 쓴다를 출력
from abc import *

class UsingKnife(metaclass=ABCMeta):
    @abstractmethod
    def use_knife(self):
        pass

class UsingWizard(metaclass=ABCMeta):
    @abstractmethod
    def use_wizard(self):
        pass

class Warrior(UsingKnife):
    def use_knife(self):
        print ('칼로 찌른다')

class Elf(UsingWizard):
    def use_wizard(self):
        print ('마법을 쓰다')

class Wizard(UsingWizard):
    def use_wizard(self):
        print ('마법을 쓰다')

warrior1 = Warrior()
elf1 = Elf()
wizard1 = Wizard()

warrior1.use_knife()

칼로 찌른다

1.1.5 DIP(Dependency Inversion Principle) 의존성 역전 법칙[편집]

  1. 부모 클래스는 자식 클래스의 구현에 의존해서는 안됨
  2. 자식 클래스 코드 변경 또는 자식 클래스 변경시, 부모 클래스 코드를 변경해야 하는 상황을 만들면 안됨
  3. 자식 클래스에서 부모 클래스 수준에서 정의한 추상 타입에 의존할 필요가 있음
    1. 실습 코드
class BubbleSort:
    def bubble_sort(self):
        # sorting algorithms
        pass
    1. 나쁜예
class SortManager:
    def __init__(self):
        self.sort_method = BubbleSort() # <--- SortManager 는 BubbleSort에 의존적
        
    def begin_sort(self):
        self.sort_method.bubble_sort() # <--- BubbleSort의 bubble_sort 메서드에 의존적

이렇게 되면 어떤 문제가 생길까요? BubbleSort의 메서드 이름을 바꿔봤습니다.

# BubbleSort의 bubble_sort 메서드 변경
class BubbleSort:
    def sort(self):
        print('bubble sort')
        pass
sortmanager = SortManager()
sortmanager.begin_sort()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-15-7b6c218e4fe5> in <module>()
      1 sortmanager = SortManager()
----> 2 sortmanager.begin_sort()

<ipython-input-14-eb257b4cc514> in begin_sort(self)
      5 
      6     def begin_sort(self):
----> 7         self.sort_method.bubble_sort() # <--- BubbleSort의 bubble_sort 메서드에 의존적

AttributeError: 'BubbleSort' object has no attribute 'bubble_sort'

그러면, 위 예에서는 상위 클래스인 SortManager도 코드를 바꿔줘야 한다. 하부 클래스 코드를 수정하면 상위 클래스 코드도 바꿔줘야 하므로, 어색한 것은 분명함 이 부분을 의존성 역전 법칙에서 상위 클래스가 하부 클래스에 의존되는 역전현상을 막아야 한다라고 어렵게 써놓은 것임

sorting1 = SortManager()
sorting1.begin_sort()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-8-19868312ab3c> in <module>()
      1 sorting1 = SortManager()
----> 2 sorting1.begin_sort()

<ipython-input-6-eb257b4cc514> in begin_sort(self)
      5 
      6     def begin_sort(self):
----> 7         self.sort_method.bubble_sort() # <--- BubbleSort의 bubble_sort 메서드에 의존적

AttributeError: 'BubbleSort' object has no attribute 'bubble_sort'

의존성을 주입하고, 상위 클래스에서 하위 클래스 활용시 하위 클래스에 따라 변경되지 않도록, 일반화(추상화)된 설계를 하면 됨

  1. 좋은 예
class SortManager:
    def __init__(self, sort_method):    # <--- 의존성을 주입시킨다고 이야기함
        self.set_sort_method(sort_method)
        
    def set_sort_method(self, sort_method):
        self.sort_method = sort_method
        
    def begin_sort(self):
        self.sort_method.sort()         # <--- 하부 클래스가 바뀌더라도, 동일한 코드 활용 가능토록 인터페이스화
  1. 실습 코드
class BubbleSort:
    def sort(self):
        print('bubble sort')
        pass

class QuickSort:
    def sort(self):
        print('quick sort')
        pass
bubble_sort1 = BubbleSort()
quick_sort1 = QuickSort()

sorting1 = SortManager(bubble_sort1)
sorting1.begin_sort()

sorting2 = SortManager(quick_sort1)
sorting2.begin_sort()
bubble sort
quick sort
  1. 초간단 연습2
  • selection sort 를 출력하는 SelectionSort 클래스 만들고, SortManager로 begin_sort() 호출해서 출력해보기
  1. 좋은 예
class SortManager:
    def __init__(self, sort_method):    # <--- 의존성을 주입시킨다고 이야기함
        self.set_sort_method(sort_method)
        
    def set_sort_method(self, sort_method):
        self.sort_method = sort_method
        
    def begin_sort(self):
        self.sort_method.sort()         # <--- 하부 클래스가 바뀌더라도, 동일한 코드 활용 가능토록 인터페이스화

class SelectionSort:
    def sort(self):
        print('selection sort')
        pass

selection_sort = SelectionSort()
sorting3 = SortManager(selection_sort)
sorting3.begin_sort()

selection sort