소프트웨어의 궁극적인 목적은 사용자를 만족시키는 것이다. 하지만 우선 그 소프트웨어는 개발자를 만족시켜야 한다. 특히 리팩터링을 강조하는 프로세스에서는 이 점이 더 중요하다. … 개발자들은 이 소프트웨어를 사용해서 작업해야 한다. 하지만 개발자들이 정말 그러길 원할까?
유연한 설계는 심층 모델링을 보완한다. … 반복주기를 거쳐 핵심 관심사를 단순하고도 명확하게 표현하는 모델을 계발하고, 클라이언트 개발자가 모델을 실제 작동 가능한 코드로 만들어낼 수 있는 설계를 구성함으로써 심층 모델을 만들 재료로 유용한 형태를 만든다.
무수히 많은 과도한 엔지니어링이 유연성이라는 명목으로 정당화되어 왔다. 그러나 대개 너무 과도한 추상 계층과 간접 계층이 존재하면 오히려 유연성에 방해가 된다. … 정교한 시스템을 만들 목적으로 조립 가능하고 그럼에도 이해하기가 어렵지 안은 요소를 만들어내려면 MDD을 적당한 수준의 엄밀한 설계 형식과 접목하고자 노력해야 한다.
개발자는 두 가지 역할을 수행하며, 각 역할은 설계에 의해 뒷받침되어야 한다.
선언적인 형식의 설계를 사용해서 클라이언트 코드를 작성하는 것을 가능하게 한다. pp.292
p260
UL로 의미있는 이름의 캡슐화
이름은 팀원들이 그 의미를 쉽게 추측할 수 있게 UL에 포함된 용어를 따라야 한다.
클라이언트 개발자의 관점에서 생각하기 위해 클래스와 연산을 추가하기 전에 행위에 대한 테스트를 먼저 작성하라.
대부분의 소프트웨어 시스템에서 명령을 사용하지 않기란 불가능하지만 다음의 두 가지 방법으로 문제를 완화할 수는 있다.
명령(관찰 가능한 상태를 변경하는 메서드)을 도메인 정보를 반환하지 않는 아주 단순한 연산으로 엄격하게 분리하라.
한 걸음 나아가 책임에 적합한 어떤 개념이 나타난다면 복잡한 로직을 VO로 옮겨서 Side-effect를 통제하라.
심층 모델 탐구 → 도메인의 논리적인 일관성 발견 → 반복적인 리팩터링 → 개념적 윤곽의 발견
High cohesion, Loose coupling → 코드뿐 아니라 개념에 대해서도 동일하게 적용할 수 있다.
기계적인 관점에서 바라보는 함정을 피하려면 수시로 도메인에 관한 직관을 발휘해서 기술적인 방향으로 흐를 수 있는 사고의 흐름을 조절해야 한다. 결정을 내릴 때마다 다음과 같은 질문을 자문해보자.
모든 개발자가 늘 염두에 두고 있는 기본 개념(표준 라이브러리, 언어 문법, 패턴 등)외에는 정신적 과부하를 초래한다.
실수 Θ 실수 ⊂ 실수 (Θ : 연산자)
Side-effect, Boiler-plate가 발생할 때 고려해볼 방법
eg. DB 영속성과 객체 관계형 Mapping → Mapstruct, Flutter의 개발 방식
선언적 설계를 작성하는 방식은 다양하며
을 이용할 수도 있다.
선언적 설계에서 개발자는 선언을 보이는 모습 그대로 받아들일 수 있다. 그리고 개발자가 받아들인 선언은 절대적으로 보장된다.
특정 도메인을 위해 구축된 특정 모델에 맞게 조정된 프로그래밍 언어
Extending SPECIFICATIONS in a Declarative Style
SPECIFICATION은 확립된 정형화인 술어를 각색한 것이다. 술어에는 선택적으로 사용할 수 있는 갖가지 유용한 특성이 있다.
술어는 “AND”, “OR”, “NOT” 연산을 사용해 조합할 수 있다. 이러한 논리 연산은 술에 대해 닫혀 있어서 SPECIFCATION의 조합은 CLOSURE OF OPERATION을 의미한다.
public interface Specification<T> {
boolean isSatisfiedBy(T candidate);
Specification<T> and(Specification<T> other);
Specification<T> or(Specification<T> other);
Specification<T> not();
}
public abstract class AbstractSpecification<T> implements Specification<T> {
@Override
public Specification<T> and(Specification<T> other) {
return new AndSpecification<>(this, other);
}
@Override
public Specification<T> or(Specification<T> other) {
return new OrSpecification<>(this, other);
}
@Override
public Specification<T> not() {
return new NotSpecification<>(this);
}
}
@RequiredArgsConstructor
public class AndSpecification<T> extends AbstractSpecification<T> {
private final Specification<T> one;
private final Specification<T> other;
@Override
public boolean isSatisfiedBy(T candidate) {
return one.isSatisfiedBy(candidate) && other.isSatisfiedBy(candidate);
}
}
@RequiredArgsConstructor
public class OrSpecification<T> extends AbstractSpecification<T> {
private final Specification<T> one;
private final Specification<T> other;
@Override
public boolean isSatisfiedBy(T candidate) {
return one.isSatisfiedBy(candidate) || other.isSatisfiedBy(candidate);
}
}
@RequiredArgsConstructor
public class NotSpecification<T> extends AbstractSpecification<T> {
private final Specification<T> wrapped;
@Override
public boolean isSatisfiedBy(T candidate) {
return !wrapped.isSatisfiedBy(candidate);
}
}
and(not(armored), not(ventilated))
이 설계에는 다음과 같은 장담점이 있다.
Subsumption 1. 상대편을 자기편으로 감싸 끌어들임. 2. 철학 어떤 개념이 보다 일반적인 개념에 포괄되는 종속 관계. 포유류와 척추동물의 관계 따위이다.
자주 규칙(SPECIFICATION)이 변경 되는 사용. 일반적인 상황이라면 불필요하고 구현하기도 어렵지만, 가끔씩 정말 어려운 문제를 해결하는 데 요긴하게 쓸 수 있다.
더 엄격한 SPECIFICATION은 덜 엄격한 SPECIFICATION을 포함한다. 더 엄격한 SPECIFICATION은 이전의 어떤 요구사항도 간과큰 관심 없이 대강 보아 넘김하지 않은 채 추가될 수 있다.장점!
각 SPECIFICATION을 만족하는 임의의 대상은 기존의 SPECIFICATION 역시 만족시키며 이를 SPECIFICATION의 언어를 사용해서 표현하면 “새로운 SPECIFICATION은 기존의 SPECIFICATION을 포섭한다(subsume)“라고 한다.
각 SPECIFICATION을 술어로 간주할 경우 포섭은 논리적 함축(logical implication)과 동일하다. 이를 전통적인 표기법을 사용해서 A→B로 표현할 수 있으며, 이것은 문자 A가 문장 B를 함축하고 있음을 의미하고, 따라서 A가 참이면 B도 참이다.
@Getter
@RequiredArgsConstructor
public class MinimumAgeSpecification {
private final int threshold;
public boolean subsumes(MinimumAgeSpecification other) {
return threshold >= other.getThreshold
}
}
public interface Specification {
boolean isSatisfiedBy(Object candiate);
boolean and(Specification specification);
boolean subsumes(Specification other);
}
오직 AND 연산만을 포함하는 함축을 증명하는 것은 간단하다
A AND B → B A AND B AND C → A AND B
아쉽게도 OR와 NOT을 포함하면 증명이 훨씬 더 복잡해진다. 대부분의 경우 일부 연산을 무시하거나 포섭을 사용하지 않는 방식 중 하나를 택해서 이처럼 복잡한 상황을 피하는 것이 최선이다. 두 가지 모두 필요한 상황이라면 구현에 수반되는 어려움을 정당화할 정도로 돌아오는 이익이 큰지 신중하게 고려해봐야 한다.
// 1. 모든 인간은 죽는다.
Specification manSpec = new ManSpecification();
Specification mortalSpec = new MortalSpefication();
assert manSpec.subsumes(mortalSpec);
// 2. 아리스토텔레스는 인간이다.
Man aristole = new Man();
assert manspec.isSatisfiedBy(aristole);
// 3. 고로 아리스토텔레스는 죽는다.
assert mortalSpec.isSatisfiedBy(aristole);
영각(迎角), 받음각.1)
이 장에서 소개한 Coupling을 낮추기 위한 기법들을 어떻게 설계에 적합하게 적용할 것인가? 그에 대한 목표 선정은?
Carve Off Subdomains
전체 설계 영역을 피상적으로 수정하기는 어렵다.
작은 하나의 영역에 지붕 → 상태 변경에 제약을 가하는 복잡한 규칙 적용? → 별도의 모델이나 규칙을 선언적으로 표현해주는 간단한 프레임워크 내부로 옮김
Draw on Established Formalisms, When You Can
아무것도 없는 상태에서 빈틈없는 개념적 체계를 만들어낸다는 것은 매일 할 수 있는 간단한 작업이 아니다. 간혹 프로젝트 기간 동안 이런 체계를 발견하고 정제하기도 한다.
그러나 보통은 현재의 도메인이나 다른 도메인 영역에서 오랜 시간 동안 정립되어 온 개념적인 체계를 이용하거나 수정해서 적용할 수 있으며, 그 중 일부는 몇 세기에 걸쳐 정제되고 증류된 것들이다.
기본적인 산수만으로 놀랍도록 유용하게 해법을 이끌어낼 수 있는 방법 체계는 수학이다.
도메인에 적절히 특화된 수학은 깔끔한 동시에 명확한 규칙과 결합할 수 있어서 사람들이 이해하기도 쉽다.