Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

10장 유연한 설계 #743

Closed
Tracked by #585
jongfeel opened this issue May 4, 2024 · 2 comments
Closed
Tracked by #585

10장 유연한 설계 #743

jongfeel opened this issue May 4, 2024 · 2 comments
Assignees
Labels
2024 Domain-Driven Design 도메인 주도 설계 - 소프트웨어의 복잡성을 다루는 지혜

Comments

@jongfeel
Copy link
Owner

jongfeel commented May 4, 2024

10 유연한 설계

image

소프트웨어의 궁극적인 목적은 사용자를 만족시키는 것이다.
그 소프트웨어는 개발자를 만족시켜야 한다.
특히 리팩터링을 강조하는 프로세스에서는 이 점이 더 중요하다.

복잡하게 동작하는 소프트웨어에 좋은 설계가 결여돼 있다면
요소들을 리팩터링하거나 결합하기가 어려워진다.
소프트웨어가 깔끔하게 설계돼 있지 않다면 개발자들은 엉망진창으로 꼬여버린 코드를 보는 것 조차 두려워하며,
혼란을 악화시키거나 예측하지 못한 의존성 탓에 망가질지 모르는 변경을 덜 하게 될 것이다.
취약성(fragility)은 리팩터링과 반복적인 정제를 방해한다.

개발이 진행될 때 레거시 코드로 인해 중압감에 시달리지 않고 프로젝트 진행을 촉진하려면
변경을 수용하고 즐겁게 작업할 수 있는 설계가 필요한데, 그게 유연한 설계(supple design)이다.

유연한 설계는 심층 모델링을 보완한다.
암시적인 개념을 찾아내서 이를 명확하게 표현했다면 심층 모델을 만들 준비는 된 것이다.
설계와 코드를 작성하는 과정에서 모델에 포함된 개념을 개선할 수 있는 통찰력을 얻게 된다.

과도한 엔지니어링이 유연성이라는 이유로 정당화되어 왔다.
너무 과도한 추상 계층과 간접 계층이 존재하면 유연성에 방해가 된다.
단순한 것이 쉽다는 것을 의미하지는 않는다.
정교한 시스템을 만들 목적으로 조립 가능하고 그럼에도 이해하기가 어렵지 않은 요소를 만들어내려면 MODEL-DRIVEN DESIGN을 적당한 수준의 엄밀한 설계 형식과 접목하고자 노력해야 한다.
단순한 모델을 만들고 사용하려면 상대적으로 복잡한 설계 솜씨가 필요할 수도 있다.

개발자는 두 가지 역할을 수행하며, 각 역할은 설계에 의해 뒷받침되어야 한다.

하나는 클라이언트 개발자 역할이다.
느슨하게 결합된 개념들의 최소 집합을 유연하게 사용해서 도메인 내의 일련의 시나리오를 표현할 수 있다.
각 설계 요소는 자연스럽게 조화를 이루며, 그 결과 예상 가능하고 명확하게 특성을 파악할 수 있으며, 견고해진다.

다른 하나는 설계 자체를 변경하는 개발자도 뒷받침해야 한다.
변경에 열려 있으려면 설계가 클라이언트 개발자가 사용하는 모델과 동일한 저변의 모델을 드러내어 쉽게 이해할 수 있어야 한다.

초기에 작성된 설계는 유연하지 못하다.
프로젝트의 시간이나 예산 한도 내에서 유연성을 얻지 못하기 때문이다.
하지만 복잡성 때문에 진행이 지연될 경우 가장 중요하고 난해한 부분을 잘 다듬어 유연한 설계로 이끄는 작업이
레거시 코드 유지보수하면서 허우적 댈 것인지, 복잡도의 한계를 뚫고 전진할 것인지의 차이를 결정한다.

Figure 10.1. Some patterns that contribute to supple design
image

  • INTENTION-REVEALING INTERFACE(의도를 드러내는 인터페이스)
  • SIDE-EFFECT-FREE FUNCTION(부수효과가 없는 함수)
  • ASSERTION(단언)
  • CONCEPTUAL CONTOUR(개념적 윤곽)
  • STANDALONE CLASS(독립형 클래스)
  • CLOSURE OF OPERATION(연산의 닫힘)

INTENTION-REVEALING INTERFACE(의도를 드러내는 인터페이스)

클라이언트 개발자가 알아야 할 정보를 인터페이스로부터 얻지 못하면 객체 내부를 깊이 파고들 수 밖에 없다.
그러면 캡슐화로 얻을 수 있는 가치를 잃어버리게 된다.
클라이언트 개발자의 머리속이 컴포넌트 작동 방식과 같은 세부적인 내용으로 넘쳐나면
클라이언트 설계의 복잡함을 풀어나갈 정신적인 여유를 확보하지 못할 것이다.

개발자가 컴포넌트 구현 세부사항을 고려해야 한다면 캡슐화의 가치는 사라진다.
객체나 연산의 목적을 추측해야 한다면 우연에 맡겨야 할 수 있다.
추측한 바가 원래의 취지에 어긋난다면 코드는 정상 동작을 해도
설계의 개념적 기반은 무너지고 의도가 어긋난 상태로 일하게 된다.

도메인 내에 존재하는 개념을 클래스나 메서드의 형태로 명확하게 모델링해서 가치를 얻으려면
해당 도메인 개념을 반영하도록 클래스와 메서드의 이름을 지어야 한다.
클래스와 메서드의 이름은 개발자 간의 의사소통을 개선하고 시스템 추상화를 향상시킬 좋은 기회이다.

켄트 벡(Kent Beck)은 메서드의 목적을 효과적으로 전달하기 위해 INTENTION-REVEALING SELECTOR(Beck 1997)를 사용해 메서드의 이름을 짓는 것에 관해 글을 썼다.
인터페이스를 구성하는 각 요소의 이름을 토대로 설계 의도를 드러내고
타입 이름, 메서드 이름, 인자 이름이 모두 결합되서 INTENTION-REVEALING INTERFACE를 형성한다.

결과와 목적만을 표현하도록 클래스와 연산의 이름을 부여해야 한다.
그러면 클라이언트 개발자가 내부를 이해해야 할 필요성이 줄어든다.
이름은 UBUQUITOUS LANGUAGE에 포함된 용어를 따른다.
클래스와 연산을 추가하기 전에 행위에 대한 테스트를 작성하면 클라이언트 개발자 관점으로 생각할 수 있다.

방법이 아닌 의도를 표현하는 추상 인터페이스 뒤로 까다로운 매커니즘을 캡슐화해야 한다.

도메인의 공개 인터페이스에는 관계와 규칙을 시행하는 방법이 아닌 관계와 그 규칙 자체만 명시한다.
이벤트와 액션도 수행 방법이 아닌 이벤트와 액션 그 자체를 기술한다.
방정식을 푸는 방법을 제시하지 말고 공식으로 표현한다.
문제를 내고 푸는 방법을 표현해서는 안 된다.

예제: 리팩터링, 페인트 혼합 애플리케이션

image

paint 메서드가 수행하는 작업을 알려면 코드를 읽는 것 뿐이다.

public void paint(Paint pain) {
    v = v + paint.getV(): //After mixing, volume is summed
    // Omitted many lines of complicated color mixing logic
    // ending with the assignment of new r, b, and y values.
}

메서드는 두 개의 Paint를 혼합하는 일을 하는 것이고, 그 결과로 용량은 더 늘어나고 색상은 혼합된다.

메서드에 대한 테스트를 작성하면 다음과 같다.

public void testPaint() {
    // Create a pure yellow paint with volume=100
    Paint yellow = new Paint(100.0, 0, 50, 0);
    // Create a pure blue paint with volume=100
    Paint blue = new Paint(100.0, 0, 0, 50);

    // Mix the blue into the yellow
    yellow.paint(blue);

    // Result should be volume of 200.0 of green paint
    assertEquals(200.0, yellow.getV(), 0.01);
    assertEquals(25, yellow.getB());
    assertEquals(25, yellow.getY());
    assertEquals(0, yellow.getR());
}

테스트 내의 코드가 무엇을 수행하고 있는지 잘 나타나고 있지 않다.
Paint 객체를 사용하는 클라이언트 애플리케이션을 작성해야 하므로
테스트 코드에서 어떤 식으로 사용하고 싶은지 반영해 다시 작성해 보면 다음과 같다.

public void testPaint() {
    // Start with a pure yellow paint with volume=100
    Paint ourPaint = new Paint(100.0, 0, 50, 0);
    // Take a pure blue paint with volume=100
    Paint blue = new Paint(100.0, 0, 0, 50);

    // Mix the blue into the yellow
    ourPaint.mixIn(blue);

    // Result should be volume of 200.0 of green paint
    assertEquals(200.0, ourPaint.getVolume(), 0.01);
    assertEquals(25, ourPaint.getBlue());
    assertEquals(25, ourPaint.getYellow());
    assertEquals(0, ourPaint.getRed());
}

시간을 들여 객체에게 이야기하고 싶은 방식을 반영하는 테스트를 작성한다.
그리고 테스트가 통과하는 Paint 클래스를 리팩터링 한다.

image

새로운 메서드 이름이 Paint를 "혼합한" 결과에 대해 세부적인 사항을 전달할 수 없다.
그러나 클래스를 사용하기에 충분할 정도의 의미는 줄 수 있으며, 테스트로 표현된 예제가 있다면 더 파악하기 쉽다.
클라이언트 코드를 읽는 사람은 새로운 메서드 이름을 통해 의도를 이해할 수 있다.

SIDE-EFFECT-FREE FUNCTION(부수효과가 없는 함수)

연산은 명령(command)과 질의(query)라는 두 가지 범주로 나눌 수 있다.
질의는 데이터를 기반으로 계산을 수행해 시스템으로부터 정보를 얻는 연산을 의미한다.
명령은 변수의 값을 변경해 시스템의 상태를 변경하는 연산을 의미한다.
부수효과(side effect)는 의도하지 않은 결과를 의미하지만,
컴퓨터 과학에서는 시스템의 상태에 대한 영향력을 의미한다.
따라서 부수효과에 대한 의미는 시스템 상태의 변경의 의미로 이해하면 좋다.

부수효과라는 용어를 사용하는 것은 의도하지 않은 영향력을 발생시키는 상호작용이 불가피하다는 점을 강조하는 것이다.

개발자가 구현과 관련된 세부사항을 함께 이해해야 한다면 인터페이스 추상화로 얻을 수 있는 유용성이 제한된다.
부수효과를 일으키지 않으면서 결과를 반환하는 연산을 함수(function)라고 한다.
함수는 여러 번 호출해도 무방하며 매번 동일한 값을 반환한다.

명령은 다음 두 가지 방법으로 문제를 완화할 수 있다.
첫째, 명령과 질의를 엄격하게 분리된 서로 다른 연산으로 유지한다. 질의와 계산은 관찰 가능한 부수효과를 발생시키지 않는 메서드 내에서 수행한다.(Meyer 1988)
둘째, 연산의 결과를 표현하는 새로운 VALUE OBJECT를 생성해서 반환한다.

VALUE OBJECT는 불변 객체이며, 초기화 연산을 제외하고 모든 연산은 함수이다.
상태 변경을 하는 로직과 계산이 혼합된 연산은 리팩터링을 통해 두 개의 연산으로 분리해야 한다. (Fowler 1999)

프로그램의 로직은 부수효과 없이 결과를 반환하는 함수 안에 작성해야 한다.
명령을 도메인 정보를 반환하지 않는 아주 단순한 연산으로 엄격하게 분리한다.
책임에 적합한 어떤 개념이 나타난다면 복잡한 로직을 VALUE OBJECT로 옮겨서 부수효과를 통제한다.

불변 VALUE OBJECT의 SIDE-EFFECT-FREE FUNCTION을 사용하면 연산을 안전하고 조합할 수 있다.
어떤 FUNCTION의 인터페이스가 INTENTION-REVEALING INTERFACE라면 개발자는 구현의 세부사항을 몰라도 FUNCTION 호출이 가능하다.

예제: 페인트 혼합 애플리케이션을 다시 리팩터링하기

FIgure 10-4
image

public void mixIn(Paint other) {
    volume = volume.plus(other.getVolume());
    // Many lines of complicated color-mixing logic
    // ending with the assignment of new red, blue,
    // and yellow values.
}

Figure 10.5. The side effects of the mixIn() method
image

이 설계는 질의(query)로 부터 변경(modification)을 분리해야 한다는 규칙을 준수한다.
문제는 mixIn() 메서드의 인자로 사용하는 paint2 객체의 용량이 불확실한 상태로 방치되고 있다.
용량이 변경되지 않는 건 개념 모델의 문맥상 완벽하게 논리적이라고 할 수 없다.
부수효과는 ASSERTION에서 다루고 지금은 색상에 대해 더 다룬다.

색상은 지식탐구 과정에서 색상 혼합(color mixing)과 페인트(paint)는 다른 것이고
RGB 광 디스플레이와 유사하다는 통찰력을 얻을 수 있다.
색상을 표현하는 새로운 객체는 이런 통찰력을 반영해야 한다.

Figure 10-6
image

Pigment Color(안료 색소)는 VALUE OBJECT이며 페인트를 혼합하면 Paint 객체 자체가 변경된다.
Pigment Color를 혼합하는 경우 상태를 변경하는 대신 새로운 색상을 표현하는 새로운 Pigment Color 객체를 생성한다.

Figure 10-7
image

public class PigmentColor {
    public PigmentColor mixedWith(PigmentColor other,
    double ratio) {
        // Many lines of complicated color-mixing logic
        // ending with the creation of a new PigmentColor object
        // with appropriate new red, blue, and yellow values.
    }
}
public class Paint {
    public void mixIn(Paint other) {
        volume = volume + other.getVolume();
        double ratio = other.getVolume() / volume;
        pigmentColor = pigmentColor.mixedWith(other.pigmentColor(), ratio);
    }
}

Figure 10-8
image

새로운 Pigment Color 클래스는 도메인 내의 지식을 표현하고 해당 지식을 명확하게 전달한다.
SIDE-EFFECT-FREE FUNCTION은 안전하기 때문에 색상을 혼합하는 로직을 올바르게 캡슐화할 수 있다.
또 구현에 관련된 세부사항을 이해하지 않아도 된다.

ASSERTION(단언)

부수효과를 초래하는 명령(command)은 ENTITY에 남아 있으므로 ENTITY를 사용하는 개발자는 명령의 영향력을 이해해야 한다.
ASSERTION을 사용하면 ENTITY를 사용하면 부수효과가 명확해지고 다루기 쉬워진다.

연산의 부수효과가 단지 구현에 의해서만 함축적으로 정의될 때 다수의 위임(delegation)을 포함하는 설계는 인과 관계로 혼란스러워진다.
프로그램을 이해하려면 분기 경로(branching path)를 따라 실행 경로를 추적하는 수밖에 없다.
이렇게 되면 캡슐화의 가치가 사라지고, 구체적인 실행 경로를 추적해야 한다는 필요성으로 추상화가 무의미해진다.

"계약에 의한 설계" 학파는 클래스와 메서드에 대해 개발자가 사실임을 보장하는 "단언"을 사용한다.
"계약에 의한 설계"는 (Meyer 1988)에서 자세히 논의하고 있다.
"사후조건"은 연산의 부수효과를 의미하며, 호출되는 연산에서 보장하는 결과를 기술한다.
"사전조건"은 계약에 명시된 단서 조항과 유사하며 사후조건이 유효하기 위해 충족돼야 하는 조건들을 기술한다.

연산의 사후조건과 클래스 및 AGGREGATE의 불변식을 명시한다.
코드에 직접 ASSERTION을 명시할 수 없다면 자동화된 단위 테스트를 작성해서 ASSERTION의 내용을 표현하라.
개발 프로세스 형식에 맞는 적절한 문서나 다이어그램으로 ASSERTION을 서술하라.
개발자들이 의도된 ASSERTION을 추측할 수 있게 하고
쉽게 배우고 모순된 코드를 작성하는 위험을 줄이는 응집도 높은 개념이 포함된 모델을 만들기 위해 노력해야 한다.

ASSERTION은 좋은 설계를 증진시키는 강력한 사고방식이다.
자동화된 단위 테스트를 작성하면 언어 차원에서 지원 부족을 부분적으로 보완할 수 있다.

사람은 모델에 포함된 개념을 추론하고 수정하기 때문에
애플리케이션의 요구사항과 사람이 이해할 수 있는 모델을 발견하는 것이 중요하다.

예제: 다시 페인트 혼합 애플리케이션으로

Paint 클래스의 mixIn(Paint) 연산에 인자로 전달된 객체의 상태 변화가 모호하게 표현되어 있는 부분부터 살펴본다.

Figure 10-9
image

mixIn(Paint) 메시지를 수신하는 객체의 용량은 인자로 전달된 객체의 용량만큼 증가한다.
실제로 페인트 혼합 프로세스의 경우 인자의 페인트의 용량을 줄여 0으로 만들거나 객체 자체를 삭제하는게 맞다.
지금은 인자를 변경하지 않지만, 인자를 변경하는 건 위험한 종류의 부수효과에 해당한다.
따라서 리팩터링은 mixIn() 메서드의 사후조건을 현재 상태 그대로 기술한다.

p1.mixIn(p2)을 실행하고 나면:
    p1.volume은 p2.volume만큼 증가함
    p2.volume은 변경되지 않음.

사후조건에 기술된 특성은 페인트 혼합에 관해 사고하는 개념과 일치하지 않아 실수가 유발된다.
문제를 해결하기 위해 인자로 전달된 Paint 객체의 용량을 0으로 변경한다.
인자를 변경하는 건 좋은 방법은 아니지만 일단 쉽고 직관적이다.
이제 불변식을 다음과 같이 작성할 수 있다.

페인트를 혼합하면 전체 용량은 변하지 않는다.

이 와중에 원래 설계자가 이런 방식으로 설계할 수 밖에 없던 이유가 밝혀졌는데
추가된 순수한 페인트 목록에 대한 보고서를 생성해야 하고
애플리케이션의 최종 목적은 사용자가 어떤 페인트를 혼합물에 넣어야 할지 판단하는 걸 돕는 데 있다.

따라서 논리적으로 일관성 있는 페인트의 용량 모델을 구축한다면
본래 애플리케이션의 요구사항을 만족시킬 수 없다.

이 경우는 부자연스러움의 원인이 놓쳐버린 개념에 있�어 새로운 모델을 찾아야 한다.

이제는 분명하게 알 수 있다

Paint에는 각기 다른 두 가지의 책임이 부여되어 있으므로 책임을 분리한다.

Figure 10-10
image

이제 명령(command)은 mixIn() 하나밖에 없다.
mixIn()은 객체를 컬렉션에 추가할 뿐, 그에 따른 결과는 모델을 직관적으로 이해한 경우 명확해진다.
그 외 연산은 모두 SIDE-EFFECT-FREE-FUNCTION이다.

ASSERTION 중 한 가지를 확인하는 테스트 메서드는 다음과 같다.

public void testMixingVolume {
    PigmentColor yellow = new PigmentColor(0, 50, 0);
    PigmentColor blue = new PigmentColor(0, 0, 50);
    
    StockPaint paint1 = new StockPaint(1.0, yellow);
    StockPaint paint2 = new StockPaint(1.5, blue);
    MixedPaint mix = new MixedPaint();

    mix.mixIn(paint1);
    mix.mixIn(paint2);
    assertEquals(2.5, mix.getVolume(), 0.01);
}

불변식과 사후조건이 의미하는 게 일치하며
결과적으로 모델을 유지보수하고 사용하기 쉬워질 것이다.

CONCEPTUAL CONTOUR(개념적 윤곽)

모델 또는 설계를 구성하는 요소가 모놀리식 구조에 묻혀 있다면 각 요소의 기능이 중복된다.
클라이언트는 외부 인터페이스로부터 일부만 파악할 수 있을 뿐이다.
서로 다른 개념이 섞여 있으므로 의미를 파악하기 어렵다.

클래스와 메서드를 잘게 나누면 클라이언트 객체가 무의미하게 복잡해진다.
이는 클라이언트 객체가 작은 부분들의 협력 방식을 이해하고 있어야 하기 때문이다.
절반의 우라늄 원자는 우라늄이 아니다.
중요한 것은 입자의 크기가 아니라 입자가 어디에서 움직이고 있느냐다.

도메인에는 잠재적인 일관성이 존재해 도메인의 일부 영역에서 적절한 모델을 발견하면
이 모델이 나중에 발견되는 다른 영역과도 일관성을 유지할 가능성이 높다.

이것이 반복적인 리팩터링을 통해 유연한 설계를 얻게 되는 이유 중 하나다.
새로 알게 된 개념이나 요구사항을 코드에 적용하다 보면 CONCEPTUAL CONTOUR가 나타난다.

두 가지 기본 원리인 높은 응집도와 낮은 결합도는 개별 메서드부터 클래스와 MODULE
그리고 대규모 구조에 이르기까지 모든 규모의 설계에 중요한 역할을 한다.
코드 뿐 아니라 개념에 대해서도 동일하게 적용할 수 있다.
기계적인 관점에서 개념을 바라보는 함정을 피하려면 수시로 도메인에 관한 직관을 발휘해서
기술적인 방향으로 흐를 수 있는 사고의 흐름을 조절해야 한다.

도메인을 중요 영역을 나누는 것과 관련한 직관을 감안해서 설계 요소(연산, 인터페이스, 클래스, AGGREGATE)를 응집력 있는 단위로 분해해야 한다.
변경을 분리하기 위한 패턴을 명확하게 표현하는 CONCEPTUAL CONTOUR를 찾는다.

합리적으로 표현하기 위해 논리적으로 결합할 수 있고 관계없는 선택사항으로 인한 혼란과
유지보수의 부담이 없는 단순한 인터페이스 집합을 얻는다.
이건 리팩터링을 통해 달성할 수 있으며, 사전 설계로는 달성하기 어렵다.
단순한 인터페이스 집합은 심층적인 통찰력을 향한 리팩터링을 거쳐 드러난다.

CONCEPTUAL CONTOUR에 맞춰 설계할 때도 수정과 리팩터링은 필요하다.
리팩터링이 한정된 범위에서만 이뤄지고 넓은 범위의 개념을 흔들지 않는다면 모델이 현재 도메인에 적합해졌다는 표시다.
객체와 메서드를 와해시킬 정도로 광범위한 변경을 야기하는 요구사항이 나타났다는 것은 도메인에 관해 알고 있는 지식을 개선해야 한다는 메시지다.
이 기회를 활용해 모델을 깊이 있게 만들고 설계를 좀더 유연하게 만드는 기회로 활용할 수 있다.

예제: 발생(Accrual)의 윤곽

9장의 대출 관리 시스템을 회계 개념으로 리팩터링했다.
새로운 모델은 기존 모델에 비해 객체를 하나 더 포함하고 있지만 책임 분할은 크게 바뀌었다.

Figure 10-11
image

새롭게 명시적으로 드러난 개념이 미치는 영향과 Accrual Schedule(발생 기록표) 계층구조의 응집력 때문에
CONCEPTUAL CONTOUR를 좀더 잘 반영하고 있다고 생각했다.

Figure 10.12. This model accommodates adding new kinds of Accrual Schedules.
image

설계가 예상치 못한 변경을 어떻게 처리할지에 관해서는 아무것도 보장할 수 없지만
적어도 개발자는 그렇게 할 수 있는 가능성은 높였다고 생각했다.

예상치 못한 변화

프로젝트가 진행되면서 조기 상환 및 연체 상환 처리에 대한 세분화된 규칙과 관련된 요구사항들이 나타났다.
새로운 모델 요소들 역시 본질적으로 하나의 Payment(상환) 클래스에 연결된다는 것을 의미했다.

Figure 10-13
image

이 같은 확장의 용이함은 개발자가 변화를 예상하거나 변경에 대해 설계를 수용할 수 있게 만들어서 나타난 건 아니다.
이전에 수행했던 리팩터링에서 기반 도메인의 개념이 설계와 조화됐기에 일어난 것이다.

STANDALONE CLASS(독립형 클래스)

MODULE 내에서 의존성이 증가한다면 설계를 파악하는데 어려움이 가파르게 높아진다.
이는 개발자에게 정신적 과부하(mental overload)를 줘서 개발자가 다룰 수 있는 설계의 복잡도를 제한한다.
아울러 명시적인 참조에 비해 암시적인 개념이 훨씬 더 많은 정신적 과부하를 초래한다.

암시적 개념은 명시적인 참조만큼이나 중요하다.
일반적으로 정수형이나 문자열과 같은 기본 타입에 대한 의존성은 무시해도, 그것이 표현하는 바를 무시할 수는 없다.

객체 개념을 구성하는 데 필수적이라는 사실이 증명되기 전까지는 모든 의존성을 검토해야 한다.
이런 과정은 모델 개념 자체를 분해하는 것에서 출발한다.
그런 다음 개별 연관관계와 연산에 주목해야 한다.

낮은 결합도는 객체 설계의 기본 원리다.
가능한 한 늘 결합도를 낮추고자 노력하라.
현재 상황과 무관한 모든 개념을 제거하라.
그러면 클래스가 완전히 독립적(self-contained)으로 바뀌고 단독으로 검토하고 이해할 수 있을 것이다.
그러한 독립적인 클래스는 MODULE을 이해하는 데 따르는 부담을 상당히 덜어준다.

모든 의존성을 제거하는 것이 아니라 모든 비본질적인 의존성을 제거하는 것이 목표다.
모든 의존성을 제거할 수는 없더라도 제기된 의존성으로 인해 남아 있는 개념적인 의존성에 집중할 수 있다.

가장 복잡다단한 계산을 STANDALONE CLASS로 도출하려고 노력하라.
이때 VALUE OBJECT로 모델링하고 관계가 밀접한 클래스에서 해당 VALUE OBJECT를 참조한다.

CLOSURE OF OPERATION(연산의 닫힘)

적절한 위치에 반환 타입과 인자 타입이 동일한 연산을 정의하라.
구현자(implementer)가 연산에 사용되는 상태를 포함하고 있다면 연산의 인자로 구현자를 사용하는 것이 효과적이므로
인자의 타입과 반환 타입을 구현자의 타입과 동일하게 정의한다.
이런 방식으로 정의된 연산은 해당 타입의 인스턴스 집합에 닫혀 있다.
닫힌 연산은 부차적인 개념을 사용하지 않고도 고수준의 인터페이스를 제공한다.

이 패턴은 VALUE OBJECT의 연산을 정의하는데 주로 사용된다.
ENTITY도 정의할 수는 있지만 ENTITY는 어떤 계산의 수행 결과를 표현하는 개념이 아니다.

인자는 구현자와 동일하지만 반환 타입이 다르거나,
반환 타입은 같지만 구현자와 인자의 타입이 다를 수 있다.
이런 연산은 특정 타입에 닫혀 있지 않지만 어느 정도는 CLOSURE의 혜택을 받을 수 있다.

예제: 컬렉션에 포함된 요소의 선택

자바
Collection에 포함된 일부 요소를 선택하기 위해 Iterator에 요청을 보낸다.
요소를 순회하면서 조건을 만족하는 지 검사하고, 만족하는 요소를 새로운 Collection에 추가한다.

Set employees = (some Set of Employee objects);
Set lowPaidEmployees = new HashSet();
Iterator it = employees.iterator();
while (it.hasNext()) {
    Employee anEmployee = it.next();
    if (anEmployee.salary() < 40000)
        lowPaidEmployees.add(anEmployee);
}

스몰토크
Collection의 "select" 연산을 호출하면서 조건을 검사하는 로직을 인자로 전달한다.
"select" 연산은 조건을 만족하는 요소를 담은 새로운 Collection을 반환한다.

employees := (some Set of Employee objects).
lowPaidEmployees := employees select:
    [:anEmployee | anEmployee salary < 40000].

반환값은 구현자와 일치하므로 필터처럼 연결해서 나열할 수 있는데,
이런 코드는 읽기 쉽고 작성하기도 편하다.


여태까지 제시한 패턴들은 일반적인 설계 형식과 함께 설계에 대해 생각하는 방법을 보여준다.
소프트웨어를 분명하고, 예측 가능하며, 전달력 있게 만든다면
추상화와 캡슐화의 목표를 효과적으로 달성할 수 있다.

모델링과 설계 기술을 계바할 의지가 있는 팀의 경우
여기서 소개한 패턴과 사고 방식이 복잡한 소프트웨어를 창조하기 위해
지속적으로 다루고 개정할 수 있는 소프트웨어를 만들어낸다.

선언적 설계

선언적 설계(declarative design)의 배경에는 몇 가지 동기가 있다.
선언적 설계는 대상에 따라 다양한 의미를 지닐 수 있지만 일반적으로 일종의 실행 가능한 명세(executable specification)로서 프로그램 전체 혹은 프로그램의 일부를 작성하는 방식을 의미한다.
특성(properties)을 매우 정확하게 기술함으로써 소프트웨어를 제어하는 것이다.

모델의 특성을 선언해서 동작하는 프로그램을 생성하는 방법이 MODEL-DRIVEN DESIGN에서는 일종의 성배에 해당하지만
실제로 적용하는 데는 몇 가지 위험이 있다.
코드를 생성할 경우 발생하는 두 가지 중요한 문제는 다음과 같다.

  • 필요한 모든 것을 충분하게 표현할 수 없는 선언 언어(declaration language)와 자동화로 감당할 수 있는 범위를 벗어나면 소프트웨어를 확장하기가 어려운 프레임워크
  • 자동으로 생성된 코드와 직접 작성한 코드를 통합한 후 코드를 다시 생성할 경우 통합된 부분이 없어져서 반복적인 주기를 무력하게 만드는 코드 생성 기법

선언적 설계의 의도하지 않은 결과로 모델과 애플리케이션의 수준이 낮아지고,
개발자들은 프레임워크의 한계에 갇힌 채 뭔가를 인도하기 위해 설계에서 우선적으로 처리해야 할 문제를 위주로 프로세스를 재편하게 된다.

여러 선언적인 접근법은 개발자가 의도적이든 의도적이지 않든 이를 우회할 경우 변질될 가능성이 있다.
시스템이 사용하기 어렵거나 과도하게 제한적일 경우 이런 문제가 발생하기 쉽다.
선언적인 프로그램의 이점을 누리려면 모든 개발자가 프레임워크의 규칙을 준수해야 한다.

도메인 특화 언어

선언적인 형식을 취하는 흥미로운 접근법으로 도메인 특화 언어(domain-specific language)가 있다.
특정 도메인을 위해 구축되었고 특정 모델에 맞게 조정된 프로그래밍 언어를 사용해 클라이언트 코드를 작성한다.

해운 시스템의 도메인 특화 언어에는 화물(cargo), 항로(route)와 같은 용어와 화물과 항로를 연관시키는 문법이 포함될 것이다.
프로그램을 컴파일하고, 컴파일된 프로그램은 객체지향 언어로 변환되며,
클래스 라이브러리는 해당 언어에 포함된 용어의 구현을 제공한다.

이렇게 만든 언어를 쓰면 프로그램의 표현력을 향상시킬 수 있고
UBIQUITOUS LANGUAGE와도 가장 높은 일관성을 유지할 수 있다.

단점은 모델 개선을 위해 개발자가 언어를 수정할 수 있어야 하는데
언어를 수정하려면 기반 클래스 라이브러리뿐 아니라 문법을 선언하는 방식과 언어를 번역할 때의 특징까지도 변경해야 할 수 있다.

근본적인 대책

객체보다 훌륭하게 도메인 특화 언어를 다루는 패러다임도 있다.
함수형 프로그래밍 패러다임을 대표하는 스킨(Scheme) 언어에서는
도메인 특화 언어와 유사한 요소를 표준 프로그래밍 형식으로 포함하고 있어서
시스템을 서로 다른 언어를 사용하는 부분으로 나누지 않고도
도메인 특화 언어의 표현력을 얻을 수 있다.

선언적인 형식의 설계

설계에 INTENTION-REVEALING INTERFACE, SIDE-EFFECT-FREE FUNCTION ASSERTION을 적용했다면
서서히 선언적인 영역으로 나아가고 있는 것이다.
유연한 설계는 선언적인 형식의 설계를 사용해서 클라이언트 코드를 작성하는 것을 가능하게 한다.

SPECIFICATION을 선언적인 형식으로 확장하기

SPECIFICATION은 확립된 정형화인 술어를 각색한 것이다.
술어에는 선택적으로 사용할 수 있는 갖가지 유용한 특성이 있다.

논리 연산을 이용한 SPECIFICATION 조합

SPECIFICATION은 술어의 한 예이며 이는 "AND", "OR", "NOT" 연산을 사용해 조합할 수 있다.
이런 논리 연산은 술어에 닫혀 있으므로 CLOSURE OF OPERRATION을 의미한다.

다양한 종류의 SPECIFICATION에 사용할 수 있는 추상 클래스나 인터페이스로 만드는 것이 유용하다.
이것은 인자의 타입으로 고수준의 추상 클래스를 사용한다는 것을 의미한다.

public interface Sepcification {
    boolean isSatisfiedBy(Object candidate);
}

세 가지 새로운 연산을 추가해서 Specification 인터페이스를 확장하면 다음과 같다.

public interface Sepcification {
    boolean isSatisfiedBy(Object candidate);

    Specification and(Speicification other);
    Specification or(Specification other);
    Specification not();
}

예로, 통풍 컨테이너(ventilated Container), 일부는 강화 컨테이너(armored Container)를 요구하도록
Container Specification을 설정했던 것이 있는데
휘발성이면서 동시에 폭발성이 강한 화학 물질인 경우에 이 두 가지 SPECIFICATION이 모두 필요하다.
새로 정의한 메서드를 이용하면 처리할 수 있다.

Specification ventilated = new ContainerSpecification(VENTILATED);
Specification armored = new ContainerSpecification(ARMORED);

Specification both = ventilated.and(armored);

통풍 컨테이너의 종류가 한 가지 이상이라고 하면 어떤 통풍 컨테이너에 포장되는지는 중요하지 않을 수도 있다.

Specification ventilatedType1 = new ContainerSpecification(VENTILATED_TYPE_1);
Specification ventilatedType2 = new ContainerSpecification(VENTILATED_TYPE_2);

Specification either = ventilatedType1.or(ventilatedType2);

모래의 경우 특수 컨테이너에 저장하는 건 낭비이므로 저렴한 컨테이너에 관한 Container Specification을 명시해서 낭비를 방지한다.

Specification cheap = (ventilated.not()).and(armored.not());

이런 제약조건을 이용한다면 9장의 창고 포장기 프로토타입을 최적화된 형태로 설계할 수 있다.

단순한 요소를 사용해 복잡한 명세를 만들어 내는 능력은 코드의 표현력을 향상시킨다.
이러한 조합은 선언적인 형식으로 작성돼 있다.

Figure 10.14. COMPOSITE design of SPECIFICATION
image

도메인의 핵심 개념을 포착하는 모델과 해당 모델을 충실히 반영한 구현이 중요하다.
이런 모델과 구현은 성능 문제를 해결할 수 있는 여지를 많이 남긴다.

다른 연산에 비해 AND 연산을 자주 사용하는 경향이 있으므로 실제 구현할 때 늘어나는 복잡도 역시 낮다.
필요한 연산이 AND 뿐이라면 AND만 구현하는 방안을 주저하지 말자.

예제: COMPOSITE SPECIFICATION을 구현하는 다른 방법

다음은 논리 표현식을 나타내는 문자열 또는 배열로 복합적인 SPECIFICATION을 표현하고 이를 실행 시간에 해석하는 다른 구현 방식의 예이다.

SPECIFICATION Stack Content for "Cheap Container"
AndSpecificationOperator (FLY WEIGHT)
NotSpecificationOperator (FLY WEIGHT)
Armored
NotSpecificationOperator
Ventilated

어떤 대상이 SPECIFICATION을 만족하는지 검사하려면 스택에서 각 요소를 꺼낸 다음 요소 자체를 평가하거나
요소가 연산이라면 다음 요소를 꺼내서 이를 조합하는 방식으로 위 구조를 해석해야 한다.
최종적으로 아래와 같은 결과를 얻게 된다.

and(not(armored), not(ventilated))

이 설계에는 다음과 같은 장점(+)과 단점(-)이 있다.

  • 생성되는 객체의 수가 적음
  • 효율적인 메모리 사용
  • 좀더 숙련된 개발자가 필요

타협점을 토대로 직면한 상황에 적절한 구현 방법을 찾아야 한다.
동일한 패턴과 모델이라도 구현 방법은 다양할 수 있다.

포섭 관계

일반적인 상황에서는 불필요하고 구현도 어렵지만, 가끔 어려운 문제를 해결하는 데 요긴하게 쓸 수 있다.
또 SPECIFICATION의 의미를 명료하게 만든다.

화학 창고 포장기의 예에서 사용자가 원하는 건 더 엄격하게 취급해야 하는 화학물질의 목록이다.
조건에 맞는 화학물질을 회사에서 확보해 놓고 있지 않았거나
이미 더 엄격한 컨테이너를 사용해서 보관할 수도 있다.

두 SPECIFICATION을 직접 비교하는 새로운 연산을 추가해 본다.

boolean subsumes(Specification other);

더 엄격한 SPECIFICATION은 덜 엄격한 SPECIFICATION을 포함한다.
더 엄격한 SPECIFICATION은 이전의 어떠한 요구사항도 간과하지 않은 채 추가될 수 있다.

Figure 10.15. The SPECIFICATION for a gasoline container has been tightened.
image

새로운 SPECIFICATION을 만족하는 임의의 대상은 기존의 SPECIFICATION 역시 만족시키며
이를 SPECIFICATION의 언어를 사용해서 표현하면
'새로운 SPEFICATION은 기존의 SPECIFICATION을 포섭한다(subsume)'라고 한다.

각 SPECIFICATIOn을 술어로 간주하면 포섭은 논리적 함축(logical implication)과 동일하다.
이를 전통적인 표기법을 사용해서 A->B로 표현할 수 있으며,
문장 A가 문장 B를 함축하고 있음을 의미하고, 따라서 A가 참이면 B 또한 참이다.

이 로직을 화학물질에 적절한 컨테이너를 찾아야 하는 요구사항에 적용해 보면

새 명세 -> 기존 명세

로 표현할 수 있다.
포괄적인 방식의 논리적 함축을 증명하기는 어렵지만
특별한 경우로 한정한다면 쉽게 구현할 수 있다.

매개변수화된 SPECIFICATION은 자기 자신의 포섭 규칙을 정의할 수 있다.

public class MinimumAgeSpecification {
    int threshold;
    
    public boolean isSatisfiedBy(Person candidate) {
        return candidate.getAge() >= threshold;
    }
    
    public boolean subsumes(MinimumAgeSpecification other) {
        return threshold >= other.getThreshold();
    }
}

테스트 코드를 짠다면 다음과 같다.

drivingAge = new MinimumAgeSpecification(16);
votingAge = new MinimumAgeSpecification(18);
assertTrue(votingAge.subsumes(drivingAge));

Container Specification 문제를 해결하기에 적절한 또 다른 특별한 경우는
하나의 AND 논리 연산과 포섭을 결합해서 사용하는 SPECIFICATION 인터페이스다.

public interface Specification {
    boolean isSatisfiedBy(Object candidate);
    Specification and(Specification other);
    boolean subsumes(Specification other);
}

오직 AND 연산만을 포함하는 함축을 증명하면

A AND B -> A

또는 좀 더 복잡한 경우에는 다음과 같이 증명할 수 있다.

A AND B AND C -> A AND B

따라서 Composite Specification 내부로 AND 연산으로 결합된 모든 단말(leaf) SPECIFICATION을 모을 수만 있다면
포섭하는 SPECIFICATION이 포섭되는 SPECIFICATION에 포함된 모든 단말 SPECIFICATION과
일부 부가적인 SPECIFICATION을 포함하는지 여부만 확인하면 된다.

public boolean subsumes(Specification other) {
    if (other instanceof CompositeSpecification) {
        Collection otherLeaves = (CompositeSpecification) other.leafSpecifications();
        Iterator it = otherLeaves.iterator();
        while (it.hasNext()) {
            if (!leafSpecifications().contains(it.next()))
                return false;
        }
    } else {
        if (!leafSpecifications().contains(other))
            return false;
    }
    return true;
}

이런 상호작용은 신중하게 선택된 매개변수화된 단말 SPECIFICATION과 다른 복잡한 SPECIFICATION을 비교해 향상시킬 수 있다.
OR, NOT을 포함하면 증명이 복잡해지지만, 일부 연산을 무시하거나 포섭을 사용하지 않는 방식을 선택해서
복잡한 상황을 피하는 것이 최선이다.

Socrates on SPECIFICATIONS

All men are mortal.

Specification manSpec = new ManSpecification();
Specification mortalSpec = new MortalSpecification();
assert manSpec.subsumes(mortalSpec);

Socrates is a man.

Man socrates = new Man();
assert manSpec.isSatisfiedBy(socrates);

Therefore, Socrates is mortal.

assert mortalSpec.isSatisfiedBy(socrates);

받음각(Angles of Attack)

옮긴이 각주 설명 추가:
영각이라고도 하며, 항공기의 날개를 절단한 면의 기준선과 기류가 이루는 각도를 의미한다.
항공기는 받음각이 커지면 상승하고, 작아지면 하강한다.

거대한 시스템을 이제 유연한 시스템으로 만들어 보자 라고 할하는 것으로는 충분하지 않고 목표를 선정해야 한다.

하위 도메인으로 분할하라

전체 설계 영역을 동시에 다룰 수는 없다.
조금씩 뜯어내야 한다.
시스템의 일부 측면에는 어떤 접근 방식을 취해야 하는지에 관한 암시가 있으므로 그런 측면을 뽑아낸 후 개선할 수 있다.

전체 영역을 피상적으로 수정하기보다는 하나의 영역에 집중해서 그 부분의 설계가 매우 유연해지도록 개선하는 편이 더 유익하다.

가능하다면 정립된 정형화를 활용하라

보통 현재의 도메인이나 다른 도메인 영역에서 오랜 시간 동안 정립되어 온 개념적인 체계를 이용하거나 수정해서 적용하며, 그 중 일부는 몇 세기에 걸쳐 정제되고 증류된 것들이다. 업무용 어플리케이션의 경우 회계 개념과 관련이 있다.

정형화된 개념 체계에서 수학은 유용하다.
도메인의 어딘가에는 수학적인 개념이 존재하므로 찾아서 파헤쳐 본다.
도메인에 특화된 수학은 깔끔한 동시에 명확한 규칙과 결합할 수 있어서 사람들이 이해하기도 쉽다.

예제, 패턴 통합하기: 지분 계산(Shares Math)

8장의 신디케이트론 시스템에서 모델 도약 사례와 비교해도 손색이 없는 한 가지 설계상 특징을 살펴본다.

신디케이트론 시스템에는 차용인이 원금을 상환할 경우 기본적으로 대출사의 지분 비율에 따라 상환금을 비례 배분해야 한다는 요구사항이 있다.

상환액 배분에 대한 초기 설계

Figure 10.16
image

public class Loan {
    private Map shares;
    //Accessors, constructors, and very simple methods are excluded
    public Map distributePrincipalPayment(double paymentAmount) { ... }
    public double getAmount() { ... }
COMMAND와 SIDE_EFFECT_FREE FUNCTION의 분리

이 설계는 INTENTION-REVEALING INTERFACE에 따라 의도를 명확하게 표현하고 있다.
distributePrincipalPayment() 메서드는 각 대출사에 배분할 금액을 계산하면서 동시에 Loan의 상태를 변경한다.
리팩터링을 거쳐 변경자(modifier)와 질의(query)를 분리한다.

Figure 10.17
image

public void applyPrincipalPaymentShares(Map paymentShares) { ... }
public Map calculatePrincipalPaymentShares(double paymentAmount) { ... }

Loan을 사용해 상환액을 배분하는 클라이언트 코드는 다음과 같다.

Map distribution = aLoan.calculatePrincipalPaymentShares(paymentAmount);
aLoan.applyPrincipalPaymentShares(distribution);

FUNCTION은 복잡한 세부사항을 INTENTION-REVEALING INTERFACE 너머로 캡슐화한다.
applyDrawdown(), calculateFeePaymentShares() 등의 메서드를 추가하면 코드의 양이 증가하기 시작한다.
코드가 복잡해지는 이유는 계산 메서드가 수행하는 작업이 너무 많기 때문이다.
전통적인 해법으로 메서드를 더 작은 하위 루틴으로 분해하는 것인데
최종적으로 원하는 건 근본적인 개념상의 경계를 찾고 모델을 심층적으로 만드는 것이다.
CONCEPTUAL CONTOUR에 따라 개념의 적절한 윤곽을 포함하는 설계 방법을 적용하면 필요한 변경사항을 쉽게 추가할 수 있다.

암시적인 개념을 명확하게 만들기

Share(지분) 객체는 수동적인 존재이고 복잡한 저수준 방식으로 조작되고 있다.
지분에 대한 규칙과 계산 방식이 개별 지분이 아닌 전체 지분의 합을 대상으로 하기 때문이다.
여기서 누락된 개념은 지분이 전체를 구성하는 일부로서 서로 관련을 맺고 있다는 점인데
이런 개념을 명시적으로 드러내면 지분의 규칙과 계산 방식이 좀 더 간결해지게 다듬을 수 있다.

Figure 10.18
image

Share Pie(지분 총액)는 Loan에 투자한 전체 투자사들의 지분 비율을 표현한다.
Loan은 상환액을 투자사별로 배분하는 계산 작업을 직접 처리하지 않고 Share Pie에 위임할 수 있다.

Figure 10.19
image

public class Loan {
    private SharePie shares;

    //Accessors, constructors, and straightforward methods are omitted

    public Map calculatePrincipalPaymentDistribution(double paymentAmount) {
        return getShares().prorated(paymentAmount);
    }
    
    public void applyPrincipalPayment(Map paymentShares) {
        shares.decrease(paymentShares);
    }
}

Loan은 단순해졌고 Share를 배분하는 계산 절차는 계산 자체에 집중하는 VALUE OBJECT로 모아졌다.

Share Pie를 VALUE OBJECT로 만들기: 통찰력의 연쇄 반응

Loan과 Share Pie 간의 강한 결합도가 생기므로 관계가 모호해진다.
Share Pie를 VALUE OBJECT로 만든다면 객체의 상태를 변경할 수 없으므로
상태 변경을 수반하는 increase(Map)와 decrease(Map)은 사용할 수 없다.
Share Pie 값을 변경하려면 매번 새로운 Share Pie 값으로 교체해야 한다.
이럴 때 Share Pie를 새로 생성해서 반환할 수 있는 addShares(Map) 연산을 추가한다.

CLOSURE OF OPERATION 까지 적용해 보면
두 개의 Share Pie를 더하면 새로운 Share Pie가 반환되게 만들 수 있다.

부수효과가 없다는 점을 강조하기 위해 prorate()에서 protated()로 변경한다.

Figure 10.20
image

새로운 Shares Pie VALUE OBJECT에 관한 명확한 ASSERTION을 정의할 수 있다.

public class SharePie {
    private Map shares = new HashMap();

    //Accessors and other straightforward methods are omitted

    public double getAmount() { ... }
    public SharePie minus(SharePie otherShares) { ... }
    public SharePie plus(SharePie otherShares) { ... }
    public SharePie prorated(double amountToProrate) { ... }
}
새로운 설계의 유연함

Loan 클래스의 메서드는 다음과 같이 단순해진다.

public class Loan {
    private SharePie shares;
   
    //Accessors, constructors, and straightforward methods are omitted

    public SharePie calculatePrincipalPaymentDistribution(double paymentAmount) {
        return shares.prorated(paymentAmount);
    }
 
    public void applyPrincipalPayment(SharePie paymentShares) {
        setShares(shares.minus(paymentShares));
    }
}

위의 짧은 메서드는 각각 자신의 의미를 명확하게 나타낸다.
Share Pie의 설계는 Loan의 코드를 계산하는 과정보다는 업무 거래에 내포된 개념적인 정의와 유사한 방식으로 읽히도록 선언적인 형식으로 구현한다.

대출금 인출이 발생한 경우 인출금은 Facility 내의 지분 비율에 따라 각 대출사로 분배된다.
추가로 발생된 인출금은 융자 잔고(outstanding) Loan에 더해진다.

이 과정의 유형을 새로운 도메인 언어를 사용하면 다음과 같다.

public class Facility {
    private SharePie shares;
    . . .
    public SharePie calculateDrawdownDefaultDistribution(double drawdownAmount) {
        return shares.prorated(drawdownAmount);
    }
}

public class Loan {
    . . .
    public void applyDrawdown(SharePie drawdownShares) {
         setShares(shares.plus(drawdownShares));
    }
}

각 대출사가 계약 시의 분담금과 실제 금액 간의 차이를 확인하려면 융자 잔고 Loan의 금액을 가상으로 분배한 후 분배액을 Loan의 실제 배당금에서 빼면 된다.

SharePie originalAgreement = aFacility.getShares().prorated(aLoan.getAmount());
SharePie actual = aLoan.getShares();
SharePie deviation = actual.minus(originalAgreement);

Share Pie 설계에는 코드를 재결합하고 의사소통하는 과정을 용이하게 만들어주는 아래 특성들이 포함되어 있다.

  • 복잡한 로직을 SIDE-EFFECT-FREE FUNCTION이 포함된 특화된 VALUE OBJECT 내부로 캡슐화 했다.
  • 상태를 변경하는 연산은 단순하며 ASSERTION을 사용해서 부수효과를 기술했다.
  • 모델 개념 간의 결합도를 낮췄다(연산이 다른 타입과 최소한의 관계만 맺는다).
  • 이미 익숙한 정형화로 규약을 이해하기가 쉬워졌다.
@jongfeel
Copy link
Owner Author

INTENTION-REVEALING INTERFACE(의도를 드러내는 인터페이스) 에서

켄트 벡(Kent Beck)의 INTENTION-REVEALING SELECTOR(Beck 1997)을 언급함

@jongfeel
Copy link
Owner Author

INTENTION-REVEALING INTERFACE(의도를 드러내는 인터페이스) 에서

결과와 목적만을 표현하도록 클래스와 연산의 이름을 부여하라는 얘기는
객체지향 프로그래밍에서 중요한 개념에 속하는 부분이다.
재미있는 건 캡슐화에 대한 중요성을 언급할 뿐 객체지향의 이런 원칙, 요소, 원리 등에 대한 용어나 설명은 배제한 체
설명하고 있다는 점이다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2024 Domain-Driven Design 도메인 주도 설계 - 소프트웨어의 복잡성을 다루는 지혜
Projects
Status: Done
Development

No branches or pull requests

1 participant