다른 명령
클래스 설계 방법론
5가지 클래스 설계의 원칙 (S.O.L.I.D)
출처) https://scotch.io/bar-talk/s-o-l-i-d-the-first-five-principles-of-object-oriented-design
SRP(Single responsibility principle) 단일 책임 원칙
- 클래스는 단 한개의 책임을 가져야 함 (클래스를 수정할 이유가 오직 하나)
- 예: 계산기 기능 구현시, 계산을 하는 책임과 GUI를 나타낸다는 책임을 서로 분리하여, 각각 클래스로 설계
- 실제 애매한 부분이 많이 존재함, 가급적 설계시 고려하면 좋음.
- 나쁜 예
- 학생성적과 수강하는 코스를 한개의 class에서 다루는 예
- 한 클래스에서 두개의 책임을 갖기 때문에, 수정이 용이하지 않다.
- 나쁜 예
class StudentScoreAndCourseManager(object): def __init__(self): scores = {} courses = {} def get_score(self, student_name, course): pass def get_courses(self, student_name): pass
- 변경 예
- 각각의 책임을 한개로 줄여서, 각각 수정이 다른 것에 영향을 미치지 않도록 함
- 변경 예
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
OCP(Open Closed Principle) 개방-폐쇄 원칙
- 확장에는 열려있어야 하고, 변경에는 닫혀있어야 함
- 예: 캐릭터 클래스를 만들 때, 캐릭터마다 행동이 다르다면, 행동 구현은 캐릭터 클래스의 자식 클래스에서 재정의(Method Override)한다.
- 이 경우, 캐릭터 클래스는 수정할 필요 없고(변경에 닫혀 있음)
- 자식 클래스에서 재정의하면 됨(확장에 대해 개방됨)
- 나쁜 예
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())
- 좋은 예
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())
LSP(Liskov Substitusion Principle) 리스코프 치환 법칙
- 자식 클래스는 언제나 자신의 부모클래스와 교체할 수 있다는 원칙
- 갤럭시폰 is a kind of 스마트폰
- 스마트폰은 다른 사람과 전화와 메시지가 가능하다.
- 스마트폰은 데이터 또는 와이파이를 이용해 인터넷을 사용할 수 있다.
- 스마트폰은 앱 마켓을 통해 앱을 다운 받을 수 있다.
- 위 설명을 갤럭시 폰으로 대체하면 아래와 같다.
- 갤럭시 폰은 다른 사람과 전화와 메시지가 가능하다.
- 갤럭시 폰은 데이터 또는 와이파이를 이용해 인터넷을 사용할 수 있다.
- 스마트폰은 앱 마켓을 통해 앱을 다운 받을 수 있다.
- 연습3
- 다음 캐릭터의 메서드를 모두 담은 클래스를 만든다면?
- 어떻게 하면 OCP 원칙을 고려할 수 있을까요?
- Warrior
- - attack: 상대방 객체를 입력받아서, '칼로 찌르다' 출력하고, 상대방의 receive 메서드를 호출해서, striking_power만큼 상대방의 health_point를 낮춰준다.
- - receive: 상대방의 striking_point를 입력으로 받아서, 자신의 health_point를 그만큼 낮추기, health_point가 0 이하이면 '죽었음' 출력
- - use_shield: 1번 공격을 막는다.
- Elf
- - attack: 상대방 객체를 입력받아서, '마법을 쓰다' 출력하고, 상대방의 receive 메서드를 호출해서, striking_power만큼 상대방의 health_point를 낮춰준다.
- - receive: 상대방의 striking_point를 입력으로 받아서, 자신의 health_point를 그만큼 낮추기, health_point가 0 이하이면 '죽었음' 출력
- - wear_manteau: 1번 공격을 막는다.
- 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
ISP(Interface Segregation Principle) 인터페이스 분리 원칙
- 클래스에서 사용하지 않는(상관없는) 메서드는 분리해야 한다.
- 추상 클래스 선언하기
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)
- 클래스를 만들기 위해 파이썬에서는 기본 metaclass가 사용됨
class MyClass: pass <source lang=python> from abc import * class Character(metaclass=ABCMeta): @abstractmethod def attack(self): pass
- 나쁜 예
- 추상 클래스 상속하기
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
- 추상 클래스 선언하기
from abc import * class Character(metaclass=ABCMeta): @abstractmethod def attack(self): pass @abstractmethod def move(self): pass
- 추상 클래스 상속하기
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
- 두 번째 예: 이렇게 작성하는 것도 처음 코드보다는 더 좋음
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
- 한발짝 더 나가보기!(심화 문제)
- 게임 캐릭터 클래스 설계 예제 상기해봅니다. 다음 세 캐릭터의 다양한 메서드를 위 두번째 방법과 유사하게 작성해볼까요?
- 게임 캐릭터는 다음과 같이 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()
칼로 찌른다
DIP(Dependency Inversion Principle) 의존성 역전 법칙
- 부모 클래스는 자식 클래스의 구현에 의존해서는 안됨
- 자식 클래스 코드 변경 또는 자식 클래스 변경시, 부모 클래스 코드를 변경해야 하는 상황을 만들면 안됨
- 자식 클래스에서 부모 클래스 수준에서 정의한 추상 타입에 의존할 필요가 있음
- 실습 코드
class BubbleSort: def bubble_sort(self): # sorting algorithms pass
- 나쁜예
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'
의존성을 주입하고, 상위 클래스에서 하위 클래스 활용시 하위 클래스에 따라 변경되지 않도록, 일반화(추상화)된 설계를 하면 됨
- 좋은 예
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 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
- 초간단 연습2
- selection sort 를 출력하는 SelectionSort 클래스 만들고, SortManager로 begin_sort() 호출해서 출력해보기
- 좋은 예
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