해리의 데브로그

Clean code - SOLID 05 - Dependency Inversion Principle (DIP)

|

백명석님의 클린코드 강의 를 듣고 정리한 포스팅 입니다.

1. Dependency Inversion Principle

상위 레벨의 정책은 하위 레벨의 정책에 의존하면 안됨. 둘은 Abstract Interface에 의존해야 한다.

2. 객체지향의 핵심

Inheritance, Encapsulation, Polymorphism는 객체지향의 핵심이 아니라 객체지향의 매커니즘임. 객체지향의 핵심은 IoC를 통해 상위 레벨의 모듈을 하위 레벨의 모듈로 부터 보호하는 것

  • 제어의 흐름을 역전시켜 (IoC), 상위레벨의 로직이 하위 레벨의 로직에 의해 변경이 되지 않도록 하는 것 (하위 레벨의 변경은 빈번하게 일어 날 수 있음)
  • OCP를 통해 새로운 요구 사항을 반영할 수 있음. (SOLID의 개념은 상호 보완 적임. DIP는 OCP와 밀접한 연관이 있음)
  • 객체지향 설계의 핵심은 의존성 관리(depedency management)

3. Structured Design

Top-down 방법론

  • 구조적 설계를 하게 되면 Top-down 방법으로 설계하게 됨.
  • 소스코드 의존성 방향 === 런타임 의존성 방향
  • 소스코드 의존성 방향이 역전되어야함!

4. Dependency Inversion

A와 B 사이에 polymorphic interface를 삽입

A는 인터페이스를 사용하며, B는 인터페이스를 구현함. 이를 통해 A(상위 레벨 로직)는 인터페이스가 변경되지 않는 이상, B(구체적인 로직. 하위 레벨 로직)의 변경에 자유로워 짐.

5. Plugins

어떤 시스템이 있을 때, 시스템을 사용하는 변경가능한 부분을 플러그인이라고 함. 시스템이라고 하는 호출자는 어떤 플러그인이 호출될줄 모름. 다만 플러그 포인트라는 인터페이스를 준수하는 하위레벨이 호출됨.

  • Boundary를 Plugin Interface로 다룸
  • 의존성 역전이 SW 모듈간의 경계를 만드는 수단
  • 아래 예시에서 위의 부분은 Application layer, 아랫 부분은 web layer/database layer등이 될 수 있음
  • Boundary를 교차하는 의존성의 방향은 반드시 하나여야만 함.

Boundary는 우리가 플러그인을 만드는 방법과 같다. 경계를 만들고자 할 때마다 어떤 의존성을 역전시킬 것인지를 고심해야함.

  • 경계를 교차하는 의존성의 방향이 같도록
  • 경계로 시스템을 나누고 경계를 교차하는 의존성을 역전

플러그인 아키텍처를 설계하면 아래와 같은 구성이 됨(메인은 시스템 어플리케이션에 플러그인 된다)

  • App Partition: 일반적인 자바 코드가 존재하는 곳
  • Main Partition: 하위 레벨

6. Architectural Implications

DIP가 아키텍처 측면에서 의미하는 바는 아래와 같음. Use Case, DB, Web 등을 분리하는 boundary 간의 의존성을 역전시키기 위해 DIP가 사용됨

  • 젤 왼쪽에 controller가 있음. 사용자 요청을 받으면 컨트롤러가 해석 (ex. 웹: HTTP 요청 또는 쿠키 정보 해석)한 후 필요한 정보를 뽑아 냄. 이후 Request Model을 create 함
  • Request Model은 인터페이스임. 컨트롤러는 실제로는 Request Model을 create하는 것이 아니라 Request Model를 구현하는 클래스 중 하나의 개념
  • Boundary(그림에서 Request Model 2칸 아래에 위치)를 통해 Application에 request를 던짐.
  • Boundary를 구현하고있는 Interactor (Use Case를 담당하는 객체)가 요청을 받음.
  • Repository를 통해 데이터베이스에서 어떠한 entity들을 읽음.
  • entity들에게 “ㅇㅇ해라” 라고 일을 시킴(entity들의 상태가 변경됨)
  • Repository에게 entity의 변경된 상태를 저장해라고 명령을 내림
  • Response Model(인터페이스)을 만든 후, Response Model을 구현하고 있는 data structure에 화면에 뿌릴 때 필요한 부분을 다 채워서 넘김
  • 컨트롤러가 받아서 본인이 Presenter를 호출해도 되며, 혹은 컨트롤러가 Boundary를 통해 use case를 호출할 때, Presenter에 대한 레퍼런스를 넘겨도 됨.
  • Response Model을 Interactor가 호출한다는 것은 화면에 그림을 그리는 친구(Web의 경우 HTML)를 호출한다는 것을 의미
  • Use Case는 HTML을 호출할 수 없음. 따라서 Presenter(화면에 뿌릴 정보를 만드는 역할)를 통해 화면에 뿌림

소스 코드 의존성의 방향을 살펴보면 어플리케이션에서 WEB 이나 Database로 가는 경우는 없음. 모두 어플리케이션을 향하고 있음. 이것이 바로 DIP가 아키텍쳐에서 갖는 의미라고 할 수 있음.

7. A Reusable Framework

객체지향이 과거 지향했던 재사용성이 통하지 않는 이유?

2인이 1년간 개발한 10만 라인 중 88%의 코드가 reusable framework에 있었음. 고객은 만족했고 추후 3개의 어플리케이션을 더 계약하였음. 하지만 resuable framework가 다른 3개의 어플리케이션에 적합하지 않다는 것을 알게되었음.

그래서 재사용 못하고 바닥부터 다시 만듦. 고객에게는 7만 7천 라인의 코드가 reusable하지 않다고 진실을 말할 수 밖에 없었음.하지만 시간과 비용을 고려했을 때 reusable한 코드가 있어야 만 했다.

그래서 고객은 첫 어플리케이션에서 만든 reusable framework를 버리고 바닥부터 만드는 대신 다시 한번 재사용 가능성을 검토하길 원했다. 그래서 다시 검토하였고 이번엔 3개의 어플리케이션을 병렬로 개발하였다. 그 결과 2 man years 동안 다시 7만 7천 라인의 reusable framework를 만들었다.

3개의 어플리케이션에서 이 framework는 재사용되었다. 이후에는 계속해서 시간과 비용을 줄이면서 개발 할 수 있었다. reusable framework를 만드는 일은 매우 어렵다. 2개 이상의 어플리케이션을 병렬로 개발하지 않는다면 거의 실패할 것이다.

재사용 가능한 코드

  • 만드는 노력: 3배
  • 유지하는 노력: 10배

8. The Inversion

어플리케이션이 프레임워크에 의존성을 가짐. 하지만 프레임워크는 어플리케이션에 의존성을 갖지 않음.

  • 프레임워크의 High Level Policy는 어플리케이션의 Low Level Detail에 대한 의존성이 없음.
  • 일반적인 Structured Design(High level Module이 Low Level Module에 의존성을 갖는)과는 반대

9. The Furnace Example

벽에 있는 Thermostat(온도조절장치)을 제어하는 소프트웨어를 개발해야한다고 가정

2개의 input과 2개의 output을 갖는 장치

  • 2개의 input: 현재 온도/희망 온도를 반환
  • 2개의 output: heater/cooler를 켜고/끄기 위한 boolean 값을 수신

특정 메모리주소를 바탕으로 온도 조절을 할 경우, 우측예시처럼 무한루프를 돌면서 조절이 될 것임. 이건 DIP에 위반됨.

  • regulate가 알고리즘이며, low level detail에 의존
  • High level control algorithm은 다른 디바이스와는 사용될 수 없음

DIP에 순응하도록 변경

  • HVAC이라는 인터페이스를 생성
  • 컨트롤러(Thermostat Controller)가 하드웨어 컨트롤러(Hardware Controller)에 직접적인 의존성을 갖지 않게 됨

10. OCP vs DIP

위의 완성된 구조는 OCP때와 다를바가 없어보임. OCP와 DIP는 얼핏 비슷해보이나 목적/의도가 다름. 현재 하드웨어 컨트롤러에는 히터와 쿨러가 들어가 있음. 이때 히터와 쿨러 외에도 다른 것들이 들어갈 수 도 있음 (OCP 확장의 개념).

  • OCP에서는 확장에 필요한 행위(영역)를 Abstraction함. (메모리를 read, write해야되는 행위를 abstraction)
  • DIP에서는 로우 레벨에 의존성을 갖는 부분을 Abstraction 함.

Clean code - SOLID 04 - Interface Segregation Principle (ISP)

|

백명석님의 클린코드 강의 를 듣고 정리한 포스팅 입니다.

1. Interface Segregation Principle

Don’t depend on things that you don’t need.

사용하지 않지만 의존성을 가지고 있다면, 그 인터페이스가 변경되면 재컴파일/빌드/배포됨. (독립적인 개발/배포가 불가하다는 것을 의미) 또한, SRP와도 연관이 있음.

  • SRP - 요구사항 변경의 원인은 클라이언트임. 내가 가지고 있는 서브 클래스가 인터페이스의 30%만을 사용한다는 것은 그것 자체가 별개의 기능/책임임.
  • 한 기능에 변경이 발생하면 다른 기능을 사용하는 클라이언트들에도 영향을 미침.

사용하는 기능만 제공하도록 인터페이스를 분리함으로써 한 기능에 대한 변경의 여파를 최소화. 클라이언트 입장에서 인터페이스를 분리하라는 원칙

2. Switch 예제

switch가 activate되면 light를 turn on하는 SW를 설계하라

2-1. 문제점

아래의 코드는 여러가지의 문제점을 가지고 있음

  • Switch가 Light에 의존적임. Switch가 Light뿐만 아니라 Fan, Motor 등에도 turnOn할 수 있음
  • Switch는 Light에 대해서 알면 안됨. (의존성이 뒤집어져야함)
public class Switch {
    private Light light;
    
    public Switch(Light light){
      this.light = light;
    }
  
  	public void activate() {
      light.turnOn();
    }    
}

2-2. 해결책

Switch는 Light에 대한 의존성을 갖지 않아야 함. Switch와 Switchable이 같은 패키지/배포 단위이어야 함.

추상 Interface(Switchable)는

  • 클라이언트(Switch)에 속해야 한다.
  • 구현체(Light)와는 관련이 없어야 함.
  • 그러므로 인터페이스의 이름은 클라이언트와 연관된 것이어야 한다. (Not Lightable, but Switchable)

3. Fat class 예제

Job 클래스가 많은 일을 하고 있어서 많은 Fan In. 각 서브 시스템은 서로 다른 이유로 Job에 의존하고 있는 상황임. 이 경우, rebuild에 많은 시간이 소요되며, 독립적인 배포/개발이 불가능하다는 문제점이 야기됨.

3-1. 해결책

One Interface for a sub system. Job이라는 굉장히 큰 인터페이스를 분리(Segregation)하여 각각의 클라이언트들을 대변하는 인터페이스를 다 뽑아냄.

  • 어떤 인터페이스에 변경이 발생하면 Job 클래스와 해당 인터페이스를 사용하는 서브 시스템만 rebuild하면 됨.

4. Fat class

Fat class(거대한 클래스)를 만나면 인터페이스를 생성하여 Fat Class를 클라이언트로부터 독립시켜야함. (Fat Class에서 다수의 인터페이스를 구현)

인터페이스는 구현체보다는 클라이언트와 논리적으로 결합되므로 클라이언트가 호출하는 메소드만 인터페이스에 정의되어있다는 것을 확신할 수 있음(ISP 준수)

특정 인터페이스의 변경으로 인한 다른 클라이언트 영향을 없애서

  • 재컴파일/재배포를 없앰
  • 클라이언트들을 다른 독립된 컴포넌트에 배치(클라이언트 + 인터페이스 = 배치단위)할 수 있고 독립적으로 배포(개발) 가능하게 됨

Clean code - SOLID 03 - Liskov Substitution Principle (LSP)

|

백명석님의 클린코드 강의 를 듣고 정리한 포스팅 입니다.

1. Liskov Substitution Principle

서브 타입(자식 클래스) 은 언제나 자신의 기반타입(부모 클래스) 으로 교체 할 수 있어야 한다.

객체지향에서 많이들 이야기하는 것이 슈퍼 클래스에 많은 기능을 넣어 넣은 후, 상속받아 서브 클래스에서 사용하는 것을 재 사용성이라고 한다. 하지만 이는 down casting으로 LSP에 위배됨. 타입에 대한 의존성은 아주 강한 의존성으로 반드시 지양 해야함.

2. OCP vs LSP

OCP의 경우

  • abstraction, polymorphism(inheritance)을 적용하여 구현
  • 어떤 인터페이스나 추상 클래스가 있을 때, 실제 구현체가 무엇인지 모름. 어떠한 형태가 담겨있는지 모른 채 수행 됨.
  • 여러 디테일한 부분은 추상화하여 하나의 인터페이스로 표현

LSP의 경우

  • OCP를 받쳐주는 polymorphism에 관한 원칙을 제공함. (서브타입은 언제나 자신의 슈퍼타입으로 교체될 수 있어야 한다.)
  • instanceof / downcasting을 사용하는 것은 전형적인 LSP 위반의 징조 (instanceof / downcasting를 통해 정확한 타입을 알아야 하는 일이 있어서는 안된다)
  • (백명석님 사견으로는) 서브클래스에서는 슈퍼클래스를 사용해서는 안됨. (지저분하고 난잡해짐)
  • LSP가 위반되면 OCP도 위반됨. LSP를 위반하면 서브타입이 추가될때마다 클라이언트들이 수정되어야 함.

3. Rectangle 예제

예) Rectangle은 시스템의 여러곳에 퍼져있음. 이러한 상황에서 정사각형(Square)을 서브타입으로 추가하려고 한다.

  • Square IS-A Rectangle (정사각형은 사각형이다. IS-A 관계 성립 ➔ 상속 관계로 표현이 가능함)
  • setWidth와 setHeight에서 난항을 겪음
    • setWidth는 가로와 세로를 똑같이 width로 셋팅을 해야하나?
    • setHeight는 가로세로를 똑같이 height로 셋팅을 해야하나?

Failed RectangleTest

  • instanceof를 사용하게되어 LSP위반이 발생함

4. The Representative Rule

대리인은 자신이 대리하는 어떤 것들에 대한 관계까지 대리(공유)하지는 않는다.

  • 이혼 소송 변호사들(대리인)이 이혼하는 부부들의 관계(부부)를 대리(공유)하지 않음
  • 따라서 기하학적에 따르면 Square IS-A Rectangle이지만, 이들을 표현/대리(represent)하는 S/W는 꼭 그들의 관계(IS-A)를 공유하지 않음

코드에 downcast, instanceof, 서브클래스에서 슈퍼클래스를 호출하는 경우가 발생한다면 상속관계를 끊고 composition 관계로 옮길 수 있는지를 고민해야함.

Clean code - SOLID 02 - Open Closed Principle (OCP)

|

백명석님의 클린코드 강의 를 듣고 정리한 포스팅 입니다.

0. Open Closed Principle

확장에 대해선 열려 있고, 변경에 대해선 닫혀있다.

1. Copy Example

Copy Module을 컴파일도 안하고 Low Level Details를 변경할 수 있다(예. 장치 추가)

  • Abstraction and Inversion
    • 추상화 인터페이스(reader, writer)를 copy 와 device 사이에 넣어서 의존성을 역전시킴

  • 기존 코드

  • 잘못된 변경 코드 예시

확장이 필요한 행위를 Abstraction시켜야 함

  • Open for extension: 디바이스가 추가되면 해당 디바이스를 담당하는 클래스를 추가
  • Closed for modification: 그러나 copy 로직의 수정은 발생하지 않음.

2. Is This Possible?

OCP를 준수하면 Modification을 완벽히 제거할 수 있나? 이론적으로는 가능하나, 실용적이지 못함

  • 2가지의 문제점 발생

2-1. Main Partition

main에서 의존성 주입을 해줘야하는 경우가 있음. (그곳에는 if/else 존재) main은 OCP를 준수할 수 없음.

2-2. Crystal ball problem

확장을 위해 미리 인터페이스를 준비해놓는다는 것이 현실적으로 불가능 함.

  • 아무리 잘 찾고 예측하더라도 고객은 반드시 당신이 준비하지 못한 것에 대한 기능 추가/변경을 요구 한다 - Unknown Unknowns. 미래의 변경으로부터 보호 받도록 Abstraction을 적용하여 설계하는 것은 쉽다. (만일 미래에 어떤 변경이 있을지 알 수 있다면 말이다.) 하지만 우린 그런 미래를 알 수 있는 Crystal ball이 없음.

  • 이게 사람들이 말하기 꺼리는 OCP, OOD에 대한 하나의 더러운 비밀이다. (OCP, OOD는 당신이 미래를 예측할 수 있을 때만 해당 기능을 보호할 수 있다.)
  • 미래를 예측하지 못한다면 어떻게 해야하는가? 완벽한 선견력이 필요하다면 객체지향의 장점은 무엇인가? 지난 30년간 SW 산업은 이 문제와 투쟁을 해왔음. 이러한 노력의 일환으로 Crystal ball의 필요성을 제거하기 위한 2가지 주요한 접근법을 식별했음.

3. Big Design Up Front(BDUF)

조심스럽게 고객과 문제 영역을 고찰하며, 고객의 요구사항을 치밀하고 과도하게 예측하여 도메일 모델을 만든다. 이를 통해 OCP가 가능하도록 도메인 모델에 추상화를 적용시킨다. 그리고 변경될 가능성이 있는 모든 것들에 대한 청사진을 얻을 때 까지 헛된 짓을 계속한다.

  • 대부분의 경우 필요치 않는 추상화로 도배된, 매우 크고 무겁고 복잡한 쓰레기 설계를 만드는 문제가 발생한다.
  • 추상화는 유용하고 강력한 만큼 비용도 크다.

4. Agile Design

변화에 대한 가장 좋은 예측은 변화를 경험하는 것이다. 발생할 것 같은 변화를 발견한다면 향후 해당 변화와 같은 종류의 변화로부터 코드를 보호할 수 있다.

고객이 요구할 모든 종류의 변경을 완벽하게 예측하고 이에 대한 변경에 대응하기 위해 Abstraction을 적용하는 대신, 고객이 변경을 요구할 때까지 기다리고 Abstraction을 만들어서 향후 추가적으로 재발하는 변화로부터 보호될 수 있도록 하라.

  • 점진적으로 추상화를 추가(필요할 때마다)
  • 고객이 변경을 요구하면 Agile Designer는 코드를 리팩토링해서 그런 종류의 변경이 쉽게 일어날 수 있도록 추상화를 추가한다(OCP를 준수하도록)

5. Agile Design in Practice

물론 우리는 실제로 BDUF와 Agile 두 극단 사이에 살고 있다. (BDUF를 피해야 하지만 No DUF도 피해야 한다)

시스템에 대해서 사고하고 Decoupled 모델을 사전 설계하는 것은 가치있는 일이다. 하지만 간단하고 적은 면에 있다. 우리의 목적은 시스템의 기본 모양을 수립하는 것이지 모든 작은 상세까지 수립하는 것이 아니다.

문제에 대해 과하게 생각하면 유지보수비용이 높은 많은 불필요한 추상화를 만들게 된다.

빨리 자주 배포하고 고객의 요구사항 변화에 기반하여 리팩토링하는 것이 매우 가치 있다. 이럴 때 OCP가 진가를 발휘한다. 하지만 간단한 도메인 모델없이 이렇게 진행하면 방향성 없는 혼란한 구조를 유발한다.

6. Reprise

마술이 아니라 공학이다. OCP를 완벽하게 준수하는 것은 불가능하다. 모든 것을 생각해 낼 수는 없다.

당신의 목적은 변경의 고통을 완전히 제거하는 것이 아니다. 이것은 불가능하다. 당신의 목적은 변경을 최소화하는 것이다. 이게 OCP를 준수하는 디자인이 당신에게 주는 잇점이다.

OCP는 시스템 아키텍처의 핵심이다. 완벽한 보호(변경으로부터의)를 얻을 수는 없지만, 얻기 위해 투쟁할 필요는 있다.

Clean code - SOLID 01 - Single Responsibility Principle (SRP)

|

백명석님의 클린코드 강의 를 듣고 정리한 포스팅 입니다.

1. Single Responsibility Principle

클래스는 하나의 책임을 가져야 한다. 책임은 무엇인가?

class Employeelmpl {
  public function calculatePay(){}
  public function save(){}
  public function describeEmployee(){}
  public function findById($id){}
}
  • Save/findById - 같은 부류 (DB와 연관된 기능)
  • 같은 책임을 갖는 기능 - calculatePay
    • calculateDeduction, calculateSalary
    • 메소드가 추가되어도 책임의 수는 변하지 않음
  • 같은 부류로 나뉘어 책임이 몇개인가를 결정
  • 부류
    • 메소드의 client에 의해 결정(저 메소드를 누가 사용하는가?)
    • 누가 해당 메소드의 변경을 유발하는 사용자인가?
      • (save라는 메소드의 로직 변경이 필요해. 누구때문에 save 로직의 변경이 필요한가?)

2. It’s About Users

SRP는 사용자에 관한 것. 책임?

  • SW의 변경을 요청하는 특정 사용자들에 대해 클래스/함수가 갖는 것
  • 변경의 근원 이라고 볼 수 있음. 변경의 원인. 무엇 때문에 변경이 일어나는가? 변경의 원인이 같은 것은 같은 책임이라고 볼 수 있음
  • User들은 그들이 수행하는 Role에 따라 나눠야한다. User가 특정 Role을 수행할 때 Actor라고 부른다.
    • 책임은 개인이 아니라 Actor와 연결
    • Actor: 서로 다른 Needs, Expectation을 가짐, Employee 클래스의 변경을 요구하는 사용자들
  • Employee 클래스에는 3개의 Actor가 있다.
    • Policy, Architect, Operations

3. Reprise

  • Responsibility - 특정 Actor의 요구사항을 만족시키기 위한 일련의 함수의 집합
  • Actor의 요구사항 변경이 일련의 함수들의 변경의 근원이 됨

4. Two Values of S/W

  • Secondary value of SW: 현재의 SW가 현재 사용자의 현재 요구사항을 만족하는가?
  • Primary value of S/W: 지속적으로 변화하는 요구사항을 수용(tolerate, facilitate) 하는 것. 대부분의 S/W는 현재의 요구사항을 잘 만족하지만 변경하긴 어렵다.

5. Collision

Employee 클래스 처럼 3가지의 책임을 한 클래스에 몰아 넣을 경우, primary value가 저하됨.

  • Policy Actors: Business rule의 변경을 필요로 함
  • Architector Actors: DB Schema의 변경을 필요로 함
  • Operations Actors: Business rule의 변경을 원함
  • 동일 모듈 변경. Merge 충돌. Source Repo 충돌

6. Fan Out

  • Employee 클래스는 너무 많은 것을 알고 있음(비즈니스 룰, 데이터베이스, 리포팅, 포맷팅)
  • 많은 책임을 갖고 있음.
  • 각각의 책임은 Employee가 다른 클래스들을 사용하도록 한다.
  • Employee 클래스에 거대한 Fan Out이 존재함.
  • 변경에 민감. Employee User는 더 민감함. 따라서 Fan Out을 제한하는 것이 좋음.
    • 좋은 방법은 책임을 최소화 하는 것

7. Collocation is Coupling

Operations Actor가 새로운 리포트 기능을 필요하다고 가정할 경우 새로운 리포트 기능도 Employee 클래스에 추가함. 기존 책임(Policy, Architecture)에는 변경이 없음에도 새로운 리포트가 추가되어 Employee 클래스가 변경.

  • 새로운 리포트 기능이 Employee 클래스에 추가되면 이 기능을 필요로 하지 않는 Employee 클래스를 사용하는 모든 클래스들이 다시 컴파일/배포되어야 함
  • 모든 액터들이 영향을 받게 된다(Collocation of responsibilities couples the actors)

8. SRP(Single Responsibility Principle)

하나의 모듈은 반드시 하나의 변경 사유를 가져야 한다 (One and only one responsibility)

  • 동일한 이유(같은 사용자, Actor)로 변경(호출)되어야 하는 것들은 동일 모듈에 있어야 한다.
  • 다른 이유로 변경되어야 하는 것들은 다른 모듈에 있어야 한다.
  • 변경 사유 = Actor = 클래스를 사용하는 사람들.
  • 하나의 클래스는 한 클라이언트들만 서빙을 해야한다는 것을 의미.

9. 시스템 설계

SRP는 시스템을 설계할 때 가장 적용하기 좋음.

  • Actor 파악에 주의해야 함. Actor들을 serve하는 책임들을 식별할 것
  • 책임을 모듈에 할당 (각 모듈이 반드시 하나의 책임을 갖도록 유지)
  • 유즈 케이스 분리의 이유
    • 다른 이유(다른 Actor)로 인해 변경되고, 다른 때에 변경되기 때문

10. Solutions

Employee 클래스 내 3개의 Actor, 3개의 Responsibility가 존재함. 어떻게 Responsibility를 분리하나? 정답은 없음

10-1. Inverted Dependencies

의존성 역전. OOP에서 이런 의존성을 다루는 전략. 클래스를 인터페이스와 클래스로 분리시킴

  • Actor를 클래스에서 Decouple 그러나, 모든 Actor들이 하나의 인터페이스에 coupled 되어 있으며, 하나의 클래스에 구현되어 구현도 coupled

10-2. Extract Classes

3개의 책임을 분리하는 방법: 3개의 클래스로 분리

  • Actor 들은 분리된 3개의 클래스에 의존
  • 3개의 책임에 대한 구현은 분리
  • 하나의 책임이 변경되어도 다른 책임에 영향을 미치지 않음

문제점

  • transitive dependency (EmployeeGateway/EmployeeReporter -> Employee).
    • Employee에 변경이 있을 때, EmployeeGateway/EmployeeReporter에 영향을 미칠 수 있음.
  • Employee의 개념이 3개의 조각을 분리됨.

10-3. Inverted Dependencies + Extract Classes

위 두가지 방법을 적절히 배합하면 더 나은 결과를 도출 시킬 수 있음. 우선 인터페이스를 extract 한후, 책임에 따라 인터페이스를 잘게 쪼개어 3개의 인터페이스로 분리시킴.

이후, EmployeeImpl(Employee 클래스)가 3개의 인터페이스를 구현 (또는 EmployeeImpl를 3개로 나눠도 됨) . 이를 통해 특정 Actor에 의한 인터페이스 변경이 일어나더라도 다른 Actor들은 영향을 받지 않게 됨.

10-4. Facade

어디에 구현이 있는지 찾기 쉬워짐.

  • Put all 3 function families into a facade class.
  • And, Facade delegates to the 3 different implementations)

어디에 구현되어있는지 찾기 쉬운 장점이 있지만 여전히 Actor들은 Coupled 되어 있음.

10-5. Interface Segregation

각 책임에 대하여 3개의 인터페이스를 만든 후, 3개의 인터페이스를 하나의 클래스로 구현

Actor들은 완전히 decoupled되어 있지만, 어디에 구현되어있는지 찾기 어려우며, 하나의 클래스에 구현되어 여전히 구현은 coupled 되어 있음.

11. Case Study

패키지 다이어그램을 살펴보면 Customer Actor를 위한 책임이 어플리케이션 아키텍처의 중심인 것을 알 수 있음. 각 패키지는 각 액터들을 위한 책임을 구현하였음.

  • 패키지 간의 의존성 방향에 주의(모두 gamePlay 패키지로 향하고 있음)
  • 이 설계는 좋은 아키텍처 (어플리케이션이 중앙에, 다른 책임들은 어플리케이션에 plug into)

  • 각 클래스는 하나의 액터만을 위한 기능을 제공
    • 하나의 클래스는 반드시 하나의 책임만을 가짐
  • 패키지간의 의존성이 한 방향으로 향하고 있음.

12. Faking It

Waterfall 순서로 한 것 같은가? step by step으로 이 복잡한 것들을 모두 찾아 낸 것 같은가? (액터 → 패키지 다이어 그램 → 클래스 다이어그램 → 코드)

실제로는 그렇지 않음. 실제로 한 것은

  1. 제일 먼저 테스트를 작성하고 통과하도록 했다.
  2. 스코어를 계산하는 동작하는 함수를 하나 만들고, 추측하는 로직을 위한 동작하는 함수를 하나 만들고, 설계가 드러날 때까지 이 함수 저 함수를 리팩토링 했다.
  3. 동작하는 전체 게임을 얻을 때까지 모든 동작들이 테스트에 성공하도록 설계를 적용했다.
  4. 그리고 아키텍처를 살폈다. 테스트가 당신들에게 보여준 설계의 80%를 유도했다.
  5. 그리고 테스트가 3개의 책임을 식별하는 것을 도왔다.
  6. unit test가 확보된 후에 무차별적인 리팩토링을 수행했다(디자인을 향상시키기 위해서)
  7. 이런 후에만 3개의 액터들이 식별된다. 그러면 클래스들을 3개의 패키지로 분리시킨다.
  8. 마지막으로 이쁜 다이어그램을 그린다. 이쁜 다이어그램을 그리기 가장 좋은 때는 완료된 후이다.