이 글은 책 '파이썬 클린코드'를 읽고 일부를 정리한 내용입니다.
Python 코드를 작성할때 도움이 되기를 바랍니다.
단일 책임 원칙은 하나의 클래스는 하나의 책임을 져야한다는 원칙이다. 하나의 클래스를 수정 할 이유는 하나뿐이며, 다른 이유로 클래스를 수정해야 한다면 추상화가 잘못되어 하나의 클래스에 너무 많은 책임이 있다는 것을 뜻한다.
class StudentScoreAndCourseManager:
def __init__(self):
scores = {}
courses = {}
def get_score(self, student_name, course):
pass
def get_courses(self, student_name):
pass
위 코드를 보면 하나의 클래스에서 학생의 성적과 과목을 모두 관리하고 있다. 이는 코드의 응집력을 저하시키며, 유지보수에도 용이하지 않다.
하나의 클래스에 너무 많은 역할을 주지 말고 각각의 클래스로 책임을 분산시켜보자.
class ScoreManager:
def __init__(self):
scores = {}
def get_score(self, student_name, course):
pass
class CourseManager:
def __init__(self):
courses = {}
def get_courses(self, student_name):
pass
다음과 같이 코드를 수정하면 하나의 클래스가 하나의 책임을 가지며, 각 클래스는 서로 어떠한 영향도 주지 않는다.
개방/폐쇄 원칙은 모듈이 개방되어 있으면서도 폐쇄되어야 한다는 원칙이다. 다시 말해, 확장 가능하여 새로운 기능을 추가하기 좋게 개방되어 있어야 하며, 새로 추가한 기능 때문에 기존 코드가 수정되지는 않도록 폐쇄적이어야 한다는 의미이다.
# Bad Case
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
class Circle:
def __init__(self, radius):
self.radius = radius
class AreaCalculator:
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)
처음에는 Ractangle 클래스와 넓이의 합을 구해주는 AreaCalculator 클래스 두 가지 클래스만 있었다고 가정해보자. 언뜻 보면 문제가 없어보이지만 Circle이라는 클래스가 추가되면 AreaCalculator 클래스도 같이 수정해야 올바른 결과를 얻을 수 있다. 만약 다른 도형 클래스가 더 추가된다면 AreaCalculator 클래스는 점점 누더기가 될 것이다.
그렇다면 다음 코드를 살펴보자.
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
class AreaCalculator:
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)
위 코드는 각 도형 클래스에서 넓이를 구하는 메서드를 구현 함으로서 코드의 확장성이 증가하고 AreaCalculator 클래스가 모든 도형의 넓이의 합을 구할 수 있게 되었다.
위와 같이 새로운 코드가 추가되어도 기존의 코드가 수정되지 않도록 유의하며 코드를 작성해야 한다.
리스코프 치환 원칙은 어떤 하위 타입을 사용해도 실행에 따른 결과는 같아야 한다는 원칙이다. 조금 더 설명하면 S가 T의 하위 타입이라면 프로그램을 변경하지 않고 T타입의 객체를 S타입의 객체로 치환 가능해야 한다는 의미다.
** 리스코프 치환 원칙에 관한 오류의 대부분은 Mypy 혹은 Pylint와 같은 툴로 문제를 검사할 수 있다.
# Bad Case
class Event:
def meets_condition(self, event_data: dict) -> bool:
return False
class LoginEvent(Event):
def meets_condition(self, event_data: list) -> bool:
return bool(event_data)
위 코드에 대해 Mypy를 실행하면 오류 메시지가 표시된다. Event 클래스와 LoginEvent 클래스의 인자 타입이 다르기 때문이다. LoginEvent 클래스는 Event 클래스를 상속 받았기 때문에 다른 곳에서 사용된 Event 클래스는 LoginEvent 클래스로 치환될 수 있어야 하며 오류 또한 발생해서는 안된다.
하지만 Mypy과 같은 툴은 함수의 사전조건을 검사해주지만, 인자(dict, list) 내부의 변수의 타입에 대한 오류는 검사해주지 못하니 이점은 사용자가 유의해서 코드를 작성해야 한다.
인터페이스 분리 원칙은 여러개의 메서드를 가진 인터페이스가 있다면 매우 정확하고 구체적인 구분에 따라 더 적은 수의 메서드를 가진 여러개의 메서드로 분할하는 것이 좋다는 원칙이다.
하나의 인터페이스에서 서로 관련이 없는 메서드가 두 개 있다면, 각각의 인터페이스로 분리하는 것이 좋다.
하지만 무조건 작은 단위의 인터페이스로 분리해야 한다는 것은 아니다. 컨텍스트 관리자는 enter와 exit메서드 두 가지가 함께 있을때 유효하기 때문에 이러한 인터페이스는 같이 묶어두어도 좋다.
의존성 역전 원칙은 상위 클래스는 하위 클래스 구현에 의존해서는 안되며, 하위 클래스는 상위 클래스의 인터페이스에 의존하도록 해야한다는 원칙이다.
예를 들어보자, A와 B 두 객체가 있을때, A는 B의 인스턴스를 사용한다. 만약 코드가 B에 크게 의존한다면 B 코드가 변경되면 원래의 코드는 쉽게 깨지게 될 것이다. 이럴때 의존성을 역전시켜 B를 A에 적응하도록 코드를 작성해야 한다.
# Bad Case
class MainClass:
def __init__(self):
subInstance = SubClass()
subInstance.method_a()
class SubClass:
def __init__(self):
pass
def method_a(self):
pass
위의 코드를 살펴보면 MainClass는 SubClass를 의존하고 있다. 하지만 위의 경우 SubClass가 변경 될 경우 MainClass의 코드도 수정되어야 한다. SubClass의 method_a가 method_b로 수정되었다고 가정하면, MainClass에서도 동일하게 코드를 수정해야 한다는 의미다. 이러한 경우 코드의 오류가 발생하기 쉽고, 파급 효과를 증가하게 만든다.
class MainClass:
def __init__(self, subInstance):
self.setSubMethod(subInstance)
def setSubMethod(self, subInstance):
self.subMethod = subInstance
def excuteMethod(self):
self.subMethod.method()
class SubClassA:
def __init__(self):
pass
def method(self):
pass
class SubClassB:
def __init__(self):
pass
def method(self):
pass
반면 위와 같이 메소드명을 일반화 하여 코드를 작성하면 SubClassA와 SubClassB의 method의 내용이 변경되어도 MainClass는 수정하지 않아도 된다.