[OOP] Flaw: Digging into Collaborators

Digging into Collaborators는 협력 객체의 내부 구조를 너무 깊게 파고드는 것을 의미합니다. 즉, 객체가 다른 객체와 협력할 때, 그 객체의 내부 상태나 구현 세부사항에 지나치게 의존하는 경우입니다. 이런 접근은 디미터 법칙을 위반하는 방식으로 간주됩니다.

협력 객체(Collaborator)의 이해

  • 협력 객체는 객체 지향 프로그래밍에서 특정 객체가 자신의 역할을 수행하기 위해 상호작용하는 다른 객체를 의미

  • 객체는 모든 작업을 직접 처리하지 않고 협력 객체에 위임하여 책임을 분산시킵니다.

안티 패턴

Digging into Collaborators는 협력 객체의 내부에 과도하게 접근하는 안티패턴으로, 다음과 같은 문제를 야기합니다:

  • 캡슐화 위반

  • 객체 간 결합도 증가

  • 단일 책임 원칙 위반

아래는 Flutter 코드 예제를 통해 "Digging into Collaborators" 문제를 설명하고 이를 개선하는 방법입니다.

Digging into Collaborators 사례 1

🛑 문제 코드

class Address {
  final String street; // Collaborator
  final String city; // Collaborator

  Address(this.street, this.city);
}

class Order {
  final Address address; // Collaborator

  Order(this.address);
}

class ShippingService {
  void shipOrder(Order order) {
    // ❌ 문제: Service가 Order 객체 내부로 파고들어 값 객체(Address)에 직접 접근함
    String destination = "${order.address.street}, ${order.address.city}";
    print("Shipping to: $destination");
  }
}

void main() {
  Order order = Order(Address("123 Main St", "Flutter City"));
  ShippingService().shipOrder(order);
}

⚠️ 문제점 분석

  • ShippingServiceOrder 객체 내부의 Address 필드에 직접 접근해 데이터를 조합하는 방식은 캡슐화 원칙을 위반합니다.

  • 이는 Order가 자신의 데이터를 관리하지 않고 외부 객체가 이를 조작하도록 만드는 결과를 초래합니다.


✅ 개선된 코드 (Tell, Don't Ask 원칙 적용)


🚀 개선 사항 요약

  • Order 클래스가 자신에 대한 책임을 가지도록 getShippingAddress 메서드를 추가했습니다.

  • 이제 ShippingService는 객체에 데이터를 요청하기만 하고 내부 구현에 관여하지 않습니다.

Digging into Collaborators 사례 2

🛑 Before: 테스트하기 어려운 설계

테스트 코드 (Before)

🚩 테스트 코드 (Before) 문제점

  1. 객체 그래프 초기화 복잡성:

    • User, Address, Invoice 객체를 모두 생성해야 하는 불필요한 초기화 과정이 테스트 코드에 포함되어 있어 복잡도가 증가합니다.

  2. 강한 결합 (Tight Coupling):

    • SalesTaxCalculatorUserInvoice 객체에 직접 의존하면서 불필요한 객체 참조가 발생합니다.

  3. 테스트 유지보수 어려움:

    • 요구사항이 변경되면 객체 생성 로직이 수정되어야 하므로 테스트 코드의 유지보수가 어려워집니다.

  4. SRP 위반:

    • SalesTaxCalculator가 본래 책임인 세금 계산 외에 데이터 추출 로직도 수행하여 역할이 복잡해집니다.


✅ After: 테스트하기 쉬운 설계

테스트 코드 (After)


✨ 개선 요약

  • 문제 해결: SalesTaxCalculator가 더 이상 User, Invoice 같은 불필요한 객체에 의존하지 않고 필요한 값(Address, double)만 직접 받습니다.

  • 테스트 코드 단순화: 객체 그래프 초기화 작업을 제거하여 테스트가 단순해지고 명확해졌습니다.

  • 유연성 증가: 새로운 클래스 구조에도 쉽게 대응할 수 있는 테스트 가능한 설계입니다.

플러터 - Domain-Specific Language (DSL)의 경우에는 디미터 법칙 위반 에서 예

  • DSL은 특정 도메인(예: 설정, 컴파일, 데이터 매핑 등)에 최적화된 언어입니다.

  • 때로는 이러한 DSL이 디미터 법칙을 어기는 방식으로 설계될 수 있습니다.

예시 코드

왜 디미터 법칙을 위반해도 괜찮은가?

  • AnimationController의 설정을 위해 repeat(), addListener(), forward()와 같은 메서드를 체이닝 방식으로 호출하고 있습니다.

  • 디미터 법칙은 객체의 내부 속성에 직접 접근하지 않도록 권장하지만, 이 경우 메서드 체이닝을 사용하여 설정을 직관적이고 읽기 쉽게 만듭니다.

  • 이 방식은 값 객체쉽게 이해할 수 있도록 구성하는 방식으로, 디미터 법칙을 위반해도 설정의 가독성과 직관성을 우선시하는 허용 가능한 예외입니다.

참고

https://github.com/mhevery/guide-to-testable-code/blob/main/flaw-digging-into-colaborators.md#problem-law-of-demeter-violated-to-inappropriately-make-a-service-locator

Last updated