SimDuck 예제

  • SimDuck 응용 프로그램 시작
sim_duck1
그림1
  • 오리들이 날아 다닐 수 있도록 변경
sim_duck2
그림2
  • 그런데 심각한 문제가 생겼습니다.
sim_duck3
그림3

Duck 수퍼 클래스에 fly가 추가 되면서 모든 서브 클래스들이 날 수 있게 되는 문제가 발생

프로그램 한 부분의 변경으로 프로그램 전체에 부작용이 발생(날아 다니는 고무 오리)

  • 상속에 대해 생각해 봅시다 소리를 내지 않거나 날 수 없을 경우 quack과 fly 메소드를 override하면 될까? Duck의 행동을 제공하는 데 있어 상속을 사용할 경우 아래와 같은 단점이 발생 한다
    • 서브 클래스에서 코드가 중복 된다.
      • 실행시 특징을 바꾸기 어렵다
      • 모든 오리의 행동을 알기 어렵다.
      • 코드를 변경 했을 때 다른 오리들 한테 원치 않는 영향을 끼칠 수 있다.
sim_duck4
그림4
  • 인터페이스는 어떨까?
sim_duck5
그림5

모든 서브 클래스에 날거나 꽥꽥거리는 기능이 있어야 하는 것은 아니므로 상속을 사용하는 것이 올바른 해결책이 아니라는 것은 알 수 있다.
서브 클래스에서 Flyable, Quackable을 구현하도록 함으로써 일부 문제점은 해결할 수 있지만, 그렇게 하면 그러한 행동에 대한 코드 재사용을 전혀 기대할 수 없기 때문에 코드 관리 면에서 문제점을 야기한다.
날아가는 동작을 조금 변경하기 위해 날아 다닐 수 있는 서브 클래스 전체를 고쳐야 할 수 있다.

  • 문제를 명확하게 파악하기

디자인 원칙

애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분으로 부터 분리 시킨다. 바뀌는 부분은 따로 뽑아서 캡슐화 시킨다. 그렇게 하면 나중에 바뀌지 않는 부분에는 영향을 주지 않은 채로 그 부분만 고치거나 확장할 수 있다.

  • 바뀌는 부분과 그렇지 않은 부분 분리하기

  • fly()와 quack()은 Duck 클래스에서 오리 마다 달라지는 부분입니다.
  • 이러한 행동을 Duck 클래스에서 분리하여 각각의 행동을 나타내는 클래스 집합을 만듭니다.

  • 오리의 행동 디자인

디자인 원칙

구현이 아닌 인터페이스에 맞춰서 프로그래밍 한다.

각 행동은 인터페이스로 표현하고 행동을 구현할 때 이런 인터페이스를 구현하도록 한다.

인터페이스에 맞춰서 프로그래밍한다는 것은 사실 상위 형식에 맞춰서 프로그램밍한다는 것을 뜻합니다.

  • Duck의 행동을 구현하는 방법

FlyBehavior와 QuackBehavior라는 두 인터페이스를 사용합니다.

sim_duck6
그림6
  • Duck 행동 통합하기

가장 중요한 점은 나는 행동과 꽥꽥 소리를 내는 행동을 Duck 클래스에서 정의한 메소드를 써서 구현하지 않고 다른 클래스에 위임한다는 것이다.

sim_duck7
그림7
  • “A는 B이다” 보다 “A에는 B가 있다”가 나을 수 있다.

디자인 원칙

상속 보다는 구성을 활용 한다.

“A에는 B가 있다” 관계에 대해 생각해 봅시다.
각 오리에는 FlyBehavior와 QuackBehavior가 있으며, 나는 행동과 꽥꽥 거리는 행동을 위임 받습니다.
두 클새스를 이런 식으로 합치는 것을 구성을 이용하는 것이라고 부릅니다.
오리 클래스에서는 행동을 상속 받는 대신, 올바른 행동 객체로 구성됨으로써 행동을 부여 받게 됩니다.
이 테크닉은 매우 중요한 테크닉입니다.
구성을 이용하여 시스템을 만들면 유연성을 크게 향상시킬 수 있습니다.
단순히 알고리즘군을 별도의 클래스의 집합으로 캡슐화할 수 있도록 만들어 주는 것 뿐 아니라
구성요소로 사용하는 객체에서 올바른 행동 인터페이스를 구현하기만 하면 실행시에 행동을 바꿀 수도 있게 해 줍니다.

  • 스트레티지 패턴

알고리즘군을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다.
스크래티지를 활용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.

개방 폐쇄 원칙 (OCP)

소프트웨어 개체(클래스, 모듈, 함수)는 확장에 열려 있어야하고, 수정에 닫혀 있어야 한다.

OCP가 잘 적용된다면, 이미 제대로 동작하는 원래 코드를 변경하는 것이 아니라 새로운 코드를 덧붙임으로써 나중에 그런 변경을 할 수 있게 된다.

해결책은 추상화다

모듈은 추상화를 조작할 수 있다. 이런 모듈은 고정된 추상화에 의존하기 때문에 수정에 대해 닫혀 있을 수 있다. 그 모듈의 행위는 추상화의 새 파생 클래스들을 만듦으로써 확장이 가능하다.

  1. OCP를 따르지 않는 예제
OCP 위배
그림1
  1. OCP를 따르는 예제
OCP 준수
그림2. 스트레티지 패턴, Client는 개방 폐쇄 원칙을 따른다

AbstractServer가 아닌 ClientInterface로 이름을 지은 이유:
추상 클래스는 자신을 구현하는 클래스 보다도 클라이언트에 더 밀접하게 관련되어 있기 때문이다.

OCP 준수
그림3. 탬플릿 메소드 패턴, 기반 크래스는 개방 폐쇄 원칙을 따른다

그림3은 대안이 되는 구조를 보여 준다. Policy 클래스는 어떤 종류의 정책을 구현하는 구체적인 공용 함수들을 갖는데, 그림2에서 본 client의 함수 들과 비슷한 것이다. 이 정책 함수들은 어떤 추상 인터페이스의 형식을 통해 작업하려는 일을 설명한다. 그러나 이번에는 추상 인터페이스가 Policy클래스 자체의 한 부분이다. 자바의 경우 추상 메소드일 것이다. 이런 함수들은 Policy 서브 타입에서 구현된다. 따라서 Policy 내부에 명시된 행위는 Policy클래스의 새로운 파생클래스를 생성함으로써 확장되거나 수정될 수 있다.

단일 책임 원칙 (SRP)

한 클래스는 단 한 가지의 변경 이유만을 가져야 한다.

SRP 위배 예제

그림1의 설계를 보자. 각기 다른 두 어플리케이션이 Rectangle 클래스를 사용한다. 하나는 계산 기하학을 위한 어플리케이션으로, Rectangle을 사용하여 기하학 도형의 수학적 계산을 돕지만 화면에 사각형을 그리지는 않는다. 다른 하나는 본질적으로 그래픽을 위한 어플리케이션이다. 이 어플리케이션도 계산 기하학 과련 동작을 조금 하긴 하지만, 화면에 직사각형을 그리는 것이 주된 기능이다. 따라서 이 설계는 Rectangle 클래스가 두 가지 책임을 지고 있으므로 단일 책임 원칙을 위반한다.

SRP위배
그림1

SRP 준수 예제

좀 더 나은 설계는 그림2와 같이 2개의 완전히 다른 클래스로 분리해 넣는 것이다.

SRP준수
그림2

Factory

The aim of a creational design pattern is to provide better alternatives for situations where a direct object creation (which in Python happens by the init() function [j.mp/divefunc], [Lott14, page 26]) is not convenient.

In the Factory design pattern, a client asks for an object without knowing where the object is coming from(that is, which class is used to generate it).

class Point3:
    def __init__(self, x, y):
        self.x = x
        self.y = y 

    def __str__(self):
        return f"x: {self.x}, y: {self.y}"

SRP

Single Responsibility Principle

Class Should have its primary responsibility and should not take on other responsibilities.

Anti Pattern 예제
class Journal:
    def __init__(self):
        self.entries = []
        self.count = 0
    def add_entry(self, text):
        self.entries.append(f"{self.count}: {text}")
        self.count += 1
    def remove_entry(self, pos):
        del self.entries[pos]
    def __str__(self):
        return "\n".join(self.entries)
    # break SRP
    def save(self, filename):
        file = open(filename, "w")
        file.write(str(self))
        file.close()
    def load(self, filename):
        pass
    def load_from_web(self, uri):
        pass
개선하기 위해 아래 save, load 를 PersistenceManager Class로 구현