DDD가 OOP : 객체 지향 저장소를 구현하는 방법을 충족합니까?


12

DDD 저장소의 일반적인 구현은 예를 들어 save()메소드와 같이 OO처럼 보이지 않습니다 .

package com.example.domain;

public class Product {  /* public attributes for brevity */
    public String name;
    public Double price;
}

public interface ProductRepo {
    void save(Product product);
} 

인프라 부분 :

package com.example.infrastructure;
// imports...

public class JdbcProductRepo implements ProductRepo {
    private JdbcTemplate = ...

    public void save(Product product) {
        JdbcTemplate.update("INSERT INTO product (name, price) VALUES (?, ?)", 
            product.name, product.price);
    }
} 

그러한 인터페이스는 Product적어도 게터와 함께 a anemic 모델이 될 것으로 기대합니다 .

반면에 OOP는 Product객체가 자신을 저장하는 방법을 알아야 한다고 말합니다 .

package com.example.domain;

public class Product {
    private String name;
    private Double price;

    void save() {
        // save the product
        // ???
    }
}

문제는 Product자체 저장 방법을 알고 있으면 인프라 코드가 도메인 코드와 분리되지 않았 음을 의미합니다.

저축을 다른 개체에 위임 할 수도 있습니다.

package com.example.domain;

public class Product {
    private String name;
    private Double price;

    void save(Storage storage) {
        storage
            .with("name", this.name)
            .with("price", this.price)
            .save();
    }
}

public interface Storage {
    Storage with(String name, Object value);
    void save();
}

인프라 부분 :

package com.example.infrastructure;
// imports...

public class JdbcProductRepo implements ProductRepo {        
    public void save(Product product) {
        product.save(new JdbcStorage());
    }
}

class JdbcStorage implements Storage {
    private final JdbcTemplate = ...
    private final Map<String, Object> attrs = new HashMap<>();

    private final String tableName;

    public JdbcStorage(String tableName) {
        this.tableName = tableName;
    }

    public Storage with(String name, Object value) {
        attrs.put(name, value);
    }
    public void save() {
        JdbcTemplate.update("INSERT INTO " + tableName + " (name, price) VALUES (?, ?)", 
            attrs.get("name"), attrs.get("price"));
    }
}

이것을 달성하는 가장 좋은 방법은 무엇입니까? 객체 지향 저장소를 구현할 수 있습니까?


6
OOP는 제품 객체 자체를 저장하는 방법을 알고 있어야합니다 말한다 - 나는 확실하지의 정말 ... 정말 그렇게 지시하지 않는 자체 OOP, 그것은 더 많은 디자인 / 패턴 문제 (의 맞 해요 당신이 어떤-DDD를 / 인 –use에 들어온다)
jleach

1
OOP와 관련하여 객체에 대해 이야기하고 있음을 기억하십시오. 데이터 지속성이 아닌 객체 만. 귀하의 진술에 따르면 객체의 상태는 외부에서 관리해서는 안되며 이는 내가 동의합니다. 저장소는 일부 지속성 계층 (OOP 영역 외부에 있음)에서로드 / 저장을 담당합니다. 클래스 속성과 메서드는 자체 무결성을 유지해야합니다. 그러나 이것이 다른 개체가 상태를 유지할 책임이 없다는 것을 의미하지는 않습니다. 그리고 게터와 세터는 객체의 들어오고 나가는 데이터의 무결성을 보장해야합니다.
jleach

1
"이것은 다른 객체가 상태를 유지할 책임이 없다는 의미는 아닙니다." -말 안했어. 중요한 진술은 객체가 활성화 되어야한다는 것 입니다. 그것은 객체 (그리고 다른 누구도) 가이 작업을 다른 객체에 위임 할 수는 있지만 다른 방법으로는 할 수 없다는 것을 의미합니다. . 위의 스 니펫 에서이 접근법을 구현하려고했습니다.
ttulka

1
@jleach 당신 말이 맞아, OOP에 대한 우리의 이해가 다르다. 나에게는 게터 + 세터가 전혀 OOP가 아니고, 그렇지 않으면 내 질문은 의미가 없었다. 어쨌든 감사합니다! :-)
ttulka

1
내 요점에 대한 기사는 다음과 같습니다. martinfowler.com/bliki/AnemicDomainModel.html 모든 경우에 빈혈 모델이 아닙니다. 예를 들어, 함수형 프로그래밍을위한 좋은 전략입니다. 그냥 OOP가 아닙니다.
ttulka

답변:


7

당신은 썼다

반면에 OOP는 Product 객체가 자신을 저장하는 방법을 알아야한다고 말합니다.

그리고 의견에.

... 그것으로 수행 된 모든 작업에 대한 책임이 있어야합니다

이것은 일반적인 오해입니다. Product가에 대한 책임을 져야하므로 도메인 객체이며, 도메인 포함하는 작업 하나 때문에 확실히를 위해 - 더 이상, 더 적은 제품 객체를, 모든 작업. 일반적으로 지속성은 도메인 작업으로 보이지 않습니다. 이와 반대로 엔터프라이즈 응용 프로그램에서는 도메인 모델에서 지속성 무지를 달성하려고 시도하는 경우가 드물지 않으며 (적어도 어느 정도까지) 지속성 메커니즘을 별도의 리포지토리 클래스에 유지하는 것이 널리 사용되는 솔루션입니다. "DDD"는 이러한 종류의 응용을 목표로하는 기술입니다.

그렇다면 합리적인 도메인 운영은 Product무엇입니까? 이것은 실제로 응용 프로그램 시스템의 도메인 컨텍스트에 따라 다릅니다. 시스템이 작은 시스템이고 CRUD 작업 만 독점적으로 지원하는 Product경우 실제로 는 예와 같이 상당히 "애매한"상태를 유지할 수 있습니다. 이러한 종류의 응용 프로그램의 경우 데이터베이스 작업을 별도의 리포지토리 클래스에 넣거나 DDD를 사용하는 것이 번거로운 일이라면 논쟁의 여지가 있습니다.

그러나 애플리케이션이 제품 구매 또는 판매, 재고 유지 및 관리, 세금 계산과 같은 실제 비즈니스 운영을 지원하는 즉시 Product수업 에 현명하게 배치 될 수있는 운영을 발견하기 시작하는 것이 일반적 입니다. 예를 들어, CalcTotalPrice(int noOfItems)대량 할인을 고려할 때 특정 제품의`n 품목에 대한 가격을 계산 하는 조작 이 있을 수 있습니다 .

요컨대, 클래스를 디자인 할 때 Joel Spolsky의 5 개 세계 중 어느 상황에 있는지 , 그리고 시스템에 충분한 도메인 논리가 포함되어 있으면 DDD가 유리한 상황에 대해 생각해야합니다 . 대답이 '예'인 경우 지속성 메커니즘을 도메인 클래스에서 제외하기 때문에 빈혈 모델로 끝날 가능성은 거의 없습니다.


당신의 요점은 나에게 매우 현명하게 들립니다. 따라서 제품은 빈혈 데이터 구조 (데이터베이스)의 컨텍스트 경계를 넘을 때 빈혈 데이터 구조가되고 저장소는 게이트웨이입니다. 그러나 이것은 여전히 ​​getter와 setter를 통해 객체의 내부 구조에 대한 액세스를 제공해야한다는 것을 의미합니다. 이는 API의 일부가되어 지속성과 관련이없는 다른 코드에서 쉽게 오용 될 수 있습니다. 이것을 피하는 좋은 방법이 있습니까? 감사합니다!
ttulka

"그러나 이것은 여전히 ​​getter와 setter를 통해 객체의 내부 구조에 대한 액세스를 제공해야 함을 의미합니다 . " 지속성 무지 도메인 오브젝트의 내부 상태는 일반적으로 도메인 관련 속성 세트에 의해 독점적으로 제공됩니다. 이러한 속성의 경우 getter 및 setter (또는 생성자 초기화)가 존재해야합니다. 그렇지 않으면 "흥미로운"도메인 작업이 불가능합니다. 여러 프레임 워크에는 개인 속성을 반영하여 유지할 수있는 지속성 기능도 있으므로 "기타 코드"가 아니라이 메커니즘에 대해서만 캡슐화가 중단됩니다.
Doc Brown

1
지속성은 일반적으로 도메인 작업의 일부가 아니지만 동의가 필요한 개체 내부의 "실제"도메인 작업의 일부 여야한다는 데 동의합니다. 예를 들어 Account.transfer(amount)전송이 지속되어야합니다. 그것이 어떻게 외부 개체가 아닌 개체의 책임입니다. 반면에 객체를 표시하는 것은 일반적으로 도메인 작업입니다! 요구 사항은 일반적으로 물건이 어떻게 보이는지 자세히 설명합니다. 프로젝트 멤버, 비즈니스 또는 그 밖의 언어 중 일부입니다.
Robert Bräutigam

@ RobertBräutigam : 클래식 Account.transfer은 일반적으로 두 개의 계정 개체와 작업 단위 개체를 포함합니다. 그런 다음 트랜잭션 지속 작업은 후자의 일부일 수 있으므로 (관련 리포지토리에 대한 호출과 함께) "transfer"메소드에서 유지됩니다. 그렇게하면 Account지속성을 무시할 수 있습니다. 나는 이것이 당신이 생각한 해결책보다 반드시 낫다고 말하지는 않지만, 당신은 또한 몇 가지 가능한 접근법 중 하나 일뿐입니다.
Doc Brown

1
@ RobertBräutigam 객체와 테이블의 관계에 대해 너무 많이 생각하고 있음을 확신하십시오. 객체가 메모리 자체의 상태를 갖는 것으로 생각하십시오. 계정 개체에서 전송을 수행하면 새로운 상태의 개체가 남게됩니다. 그것이 당신이 유지하고 싶은 것입니다. 다행히도 계정 개체는 상태에 대해 알 수있는 방법을 제공합니다. 그것은 그들의 상태가 데이터베이스의 테이블과 같아야한다는 것을 의미하지는 않습니다.
Steve Chamaillard

5

연습이 이론보다 우선합니다.

경험에 따르면 Product.Save ()는 많은 문제가 발생합니다. 이러한 문제를 해결하기 위해 우리는 저장소 패턴을 발명했습니다.

제품 데이터를 숨기는 OOP 규칙을 위반하는 것은 확실합니다. 그러나 잘 작동합니다.

예외가있는 일반적인 좋은 규칙을 만드는 것보다 모든 것을 포괄하는 일관된 규칙을 만드는 것이 훨씬 어렵습니다.


3

DDD와 OOP의 만남

가치 객체, 집계, 리포지토리는 OOP가 올바르게 수행되는 것으로 간주되는 패턴의 배열입니다.

반면 OOP는 Product 객체가 자신을 저장하는 방법을 알아야한다고 말합니다.

별로. 객체는 자체 데이터 구조를 캡슐화합니다. 귀하의 제품에 대한 귀하의 메모리 표현은 제품 행동을 보여 주어야합니다 (무엇이든). 그러나 영구 저장소는 저쪽에 있으며 (저장소 뒤에) 수행해야 할 자체 작업이 있습니다.

데이터베이스의 메모리 내 표현과 지속 된 메모 사이에 데이터 를 복사 할 수있는 방법이 필요합니다 . 경계 에서 상황은 매우 원시적 인 경향이 있습니다.

기본적으로 쓰기 전용 데이터베이스는 특별히 유용하지 않으며 메모리에 해당하는 데이터베이스는 "지속적"정렬보다 더 유용하지 않습니다. 정보를 Product절대 꺼내지 않을 경우 정보를 객체 에 넣을 필요는 없습니다. 반드시 "getter"를 사용할 필요는 없습니다. 제품 데이터 구조를 공유하려는 것은 아니며 제품의 내부 표현에 대한 변경 가능한 액세스를 공유해서는 안됩니다.

저축을 다른 개체에 위임 할 수도 있습니다.

영구 스토리지는 효과적으로 콜백이됩니다. 아마도 인터페이스를 더 간단하게 만들 것입니다.

interface ProductStorage {
    onProduct(String name, double price);
}

정보가 여기에서 거기로 (다시) 다시 도착해야하기 때문에 메모리 내 표현과 저장 메커니즘 사이에 연결 이 있을 것 입니다. 공유 할 정보를 변경하면 대화의 양쪽 끝에 영향을 미칩니다. 그래서 우리는 우리가 할 수있는 곳을 명시 적으로 만들 수도 있습니다.

콜백을 통해 데이터를 전달하는이 접근 방식 은 TDD에서 모의 개발에 중요한 역할을했습니다 .

콜백에 정보를 전달하면 쿼리에서 정보를 반환하는 것과 동일한 제한이 모두 적용됩니다. 데이터 구조의 변경 가능한 복사본을 전달해서는 안됩니다.

이 접근 방식은 쿼리를 통해 데이터를 반환하는 것이 일반적인 방법이며 도메인 개체가 "지속성 문제"에서 혼합되지 않도록 특별히 설계된 Blue Book에서 설명한 Evans와는 약간 상반됩니다.

나는 DDD를 OOP 기술로 이해하고 있기 때문에 그 모순처럼 보이는 것을 완전히 이해하고 싶습니다.

염두에 두어야 할 것-The Blue Book은 15 년 전에 Java 1.4가 지구를 돌아 다니면서 쓰여졌습니다. 특히,이 책은 Java 제네릭 보다 앞서 있습니다. 우리는 지금 Evans가 그의 아이디어를 개발할 때 더 많은 기술을 사용할 수 있습니다.


2
"저장 자체"는 항상 다른 객체 (파일 시스템 객체, 데이터베이스 또는 원격 웹 서비스)와의 상호 작용을 필요로하며,이 중 일부는 액세스 제어를 위해 세션을 설정해야 할 수도 있습니다. 따라서 그러한 대상은 자립적이고 독립적이지 않습니다. 따라서 OOP의 목적은 객체를 캡슐화하고 커플 링을 줄이기위한 것이므로 필요하지 않습니다.
Christophe

좋은 답변 감사합니다. 먼저, Storage당신이했던 것과 같은 방식으로 인터페이스를 디자인 한 다음, 높은 커플 링을 고려하고 변경했습니다. 그러나 당신은 맞습니다, 어쨌든 피할 수없는 결합이 있으므로 더 명시 적으로 만들지 마십시오.
ttulka

1
"이 접근 방식은 Evans가 Blue Book에 설명 된 내용과 약간 상반됩니다." -결국에는 약간의 긴장이 있습니다. 모순되는 것처럼 보입니다.
ttulka

1
내 경험상 이러한 것들 (일반적으로 ODD, DDD, TDD, 선택 약어)은 모두 훌륭하고 훌륭하게 들리지만 "실제"구현에 관해서는 항상 약간의 상충 관계가 있거나 그것이 이상적이지 않은 이상주의.
jleach

지속성 (및 표현)이 어떻게 든 "특별"하다는 개념에 동의하지 않습니다. 그들은 아닙니다. 요구 사항을 확장하기 위해 모델링의 일부 여야합니다. 반대로 실제 요구 사항이없는 한 응용 프로그램 내에 인공 (데이터 기반) 경계 가있을 필요는 없습니다.
Robert Bräutigam

1

아주 좋은 관찰, 나는 그들에 당신과 완전히 동의합니다. 이 주제에 대한 나의 이야기 (수정 : 슬라이드 만)가 있습니다 : 객체 지향 도메인 기반 디자인 .

짧은 대답 : 아닙니다. 응용 프로그램 에 순수한 기술적이며 도메인 관련성이없는 개체 가 없어야합니다. 이는 회계 애플리케이션에서 로깅 프레임 워크를 구현하는 것과 같습니다.

사용자 Storage인터페이스의 예는이 가정, 훌륭한 하나입니다 Storage당신이 그것을 쓰는 경우에도, 그 다음 일부 외부 프레임 워크를 간주됩니다.

또한 save()개체에서 도메인의 일부인 경우에만 허용되어야합니다 ( "언어"). 예를 들어, Account호출 한 후을 명시 적으로 "저장"할 필요는 없습니다 transfer(amount). 비즈니스 기능 transfer()이 이전을 지속 할 것으로 기대합니다 .

DDD의 아이디어는 좋은 아이디어라고 생각합니다. 그러나 유비쿼터스 언어를 사용하여 대화, 경계 컨텍스트 등을 사용하여 도메인을 연습 할 수 있습니다. 그러나 빌딩 블록 은 객체 지향과 호환 되려면 심각한 점검이 필요합니다. 자세한 내용은 연결된 데크를 참조하십시오.


당신의 대화는 어디 선가보고 있습니까? (링크 아래에는 슬라이드 만 있습니다). 감사!
ttulka 2019

: 나는 단지 여기에 이야기의 독일의 녹음이 javadevguy.wordpress.com/2018/11/26/...
로버트 Bräutigam

좋은 이야기! (다행스럽게도 나는 독일어를 구사합니다). 블로그 전체를 읽을 가치가 있다고 생각합니다. 작업 해 주셔서 감사합니다!
ttulka 2019

매우 통찰력있는 슬라이더 Robert. 필자는 설명이 매우 훌륭하지만 결국에는 캡슐화를 깨뜨리지 않도록 해결 된 많은 솔루션이 LoD가 도메인 개체에 많은 책임을 부여하는 데 기반을두고 있다는 느낌을 받았습니다. 인쇄, 직렬화, UI 형식화 등 도메인과 기술 (구현 세부 사항) 간의 연결을 증가 시키는가? 예를 들어 AccountNumber는 Apache Wicket API와 결합되었습니다. 또는 Json 객체가 무엇이든간에 계정입니까? 그것이 가치있는 커플이라고 생각하십니까?
Laiv

@Laiv 질문의 문법은 기술을 사용하여 비즈니스 기능을 구현하는 데 문제가 있음을 나타냅니다. 이런 식으로하자. 문제는 도메인과 기술 사이의 결합이 아니라 다른 추상화 레벨 사이의 결합이다. 예를 들어 AccountNumber 있어야 그것이로 표현 될 수 있다는 것을 알고 TextField. 다른 사람들 ( "보기"와 같은)이 이것을 알고 있다면, 그것은 존재하지 않아야하는 결합입니다. 왜냐하면 그 구성 요소는 무엇으로 AccountNumber구성되어 있는지 , 즉 내부 요소를 알아야하기 때문입니다 .
Robert Bräutigam

1

저축을 다른 개체에 위임 할 수도 있습니다

불필요하게 필드에 대한 지식을 전파하지 마십시오. 개별 필드에 대해 알고있는 것이 많을수록 필드를 추가하거나 제거하기가 더 어려워집니다.

public class Product {
    private String name;
    private Double price;

    void save(Storage storage) {
        storage.save( toString() );
    }
}

여기서 제품은 로그 파일 또는 데이터베이스 또는 둘 다에 저장하는지 여부를 모릅니다. 여기에 저장 방법은 4 개 또는 40 개의 필드가 있는지 전혀 모릅니다. 그것은 느슨하게 결합되어 있습니다. 좋은 일입니다.

물론 이것은이 목표를 달성 할 수있는 방법의 한 예일뿐입니다. DTO로 사용할 문자열을 만들고 파싱하는 것을 좋아하지 않으면 컬렉션을 사용할 수도 있습니다. LinkedHashMap순서를 유지하고 로그 파일에서 toString ()이 좋아 보이기 때문에 내가 가장 좋아하는 것입니다.

그러나 당신은 그것을합니다, 필드에 대한 지식을 퍼 뜨리지 마십시오. 이것은 늦게까지 사람들이 종종 무시하는 결합 형태입니다. 객체가 가능한 많은 필드를 정적으로 알고 싶은 것은 거의 없습니다. 이렇게하면 필드를 추가 할 때 여러 곳에서 많은 편집 작업이 필요하지 않습니다.


이것은 실제로 내 질문에 게시 한 코드입니다. 나는을 사용했고 Map, 당신은 a String또는 을 제안 했다 List. 그러나 @VoiceOfUnreason이 그의 답변에서 언급했듯이 커플 링은 여전히 ​​명시 적이 지 않습니다. 최소한 객체로 다시 읽을 때 데이터베이스 나 로그 파일에 모두 저장하기 위해 제품의 데이터 구조를 알 필요는 없습니다.
ttulka

저장 방법을 변경했지만 그렇지 않으면 예와 거의 같습니다. 차이점은 커플 링이 더 이상 정적이지 않아 스토리지 시스템에 코드를 변경하지 않고도 새 필드를 추가 할 수 있다는 것입니다. 따라서 스토리지 시스템을 다양한 제품에서 재사용 할 수 있습니다. 그것은 단지 더블을 끈으로 바꾸고 다시 더블로 바꾸는 것과 같이 약간 부 자연스러운 것을하도록 강요합니다. 그러나 실제로 문제가 있다면 해결할 수도 있습니다.
candied_orange


그러나 내가 말했듯이, 정적이 아닌 (명시 적) 컴파일러만으로는 오류가 발생하기 쉽다는 단점이 있지만 (구문 분석하여) 커플 링이 여전히 있음을 알 수 있습니다. 가 Storage도메인의 일부 (및 저장소 인터페이스이다)이고, 이러한 지속성 API를 만든다. 변경되면 클라이언트가 런타임에 깨지지 않도록 반응해야하기 때문에 컴파일 타임에 클라이언트에게 알리는 것이 좋습니다.
ttulka

그것은 오해입니다. 컴파일러는 로그 파일이나 DB를 확인할 수 없습니다. 하나의 코드 파일이 다른 코드 파일과 일치하는지, 로그 파일 또는 DB와 일치하지 않을 수도 있습니다.
candied_orange

0

이미 언급 된 패턴에 대한 대안이 있습니다. Memento 패턴은 도메인 객체의 내부 상태를 캡슐화하는 데 좋습니다. 메멘토 개체는 도메인 개체 공개 상태의 스냅 샷을 나타냅니다. 도메인 객체는 내부 상태에서이 공개 상태를 만드는 방법을 알고 있으며 그 반대도 마찬가지입니다. 그런 다음 리포지토리는 상태의 공개 표현에서만 작동합니다. 이를 통해 내부 구현은 모든 지속성 사양과 분리되며 공개 계약을 유지해야합니다. 또한 도메인 객체는 실제로 빈약하게 만드는 게터를 노출시키지 않아야합니다.

이 주제에 대한 자세한 내용은 Scott Millett와 Nick Tune의 "도메인 주도 디자인의 패턴, 원리 및 실습"이라는 훌륭한 책을 추천합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.