2014-03-04 1 views
0

간단한 소프트웨어 디자인에 대한 질문이 있습니다. 데이터베이스에 유지되는 엔티티 (= 도메인 오브젝트)를 약간 불변으로 만들고 싶습니다. 수단 : 엔티티는 이어야하며은 서비스에서 생성되고 응용 프로그램의 모든 다른 부분은 게터 메소드 만 가진 interface으로 작동합니다.스프링 서비스, 저장소, 엔티티 디자인 패턴 어드바이스가 필요합니다.

예 : myService.getMyEntityById(5)

  • MyService가를 요청합니다 :

    1. MyController 개체를 얻기 위해 MyService를 요청하는 id=5

    2. MyControllerMyEntity를 검색 할

      root 
          |--- service 
          |   |--- MyService.java 
          |   |--- MyServiceImpl.java 
          |   | 
          |   |--- MyEntity.java 
          |   |--- MyEntityImpl.java 
          |   | 
          |   |--- MyEntityRepository.java 
          | 
          | 
          |------- web 
            |--- MyController.java 
      

      아이디어 : 10

    3. MyServiceMyEntityInterface

    MyController에 패키지 디자인을 반환 DB에서 개체를 가져 나의 첫번째 아이디어가 있었다

    단순히 백팩 사용 MyEntityImpl에서 kage로 보호되는 생성자를 사용할 수 있지만이 방법은 내가 사용하고있는 다른 라이브러리 (예 : Orika). 따라서 public이어야합니다.

    다음 아이디어는 MyEntity 인터페이스를 사용하는 것이 었습니다. 하지만 지금은 약간의 문제가 생겼어요 :

    문제 :MyService(Impl)

    라는 방법이 있습니다 updateMyEntityData(MyEntity e, Data data)합니다. 이제는 내 서비스 내에서이 MyEntity 개체가 실제로 MyEntityImpl의 인스턴스라는 것을 확신 할 수 없습니다. 물론 if(e instanceof MyEntityImpl) ...을 할 수는 있지만 정확히 이 아니며을 원한다.

    다음 문제는 :이 서비스 메서드는 MyEntityImpl 개체를 저장 및 검색 할 수 있지만 MyEntity 인터페이스를 처리 할 수없는 MyEntityRepository을 사용합니다. 해결 방법으로 나는 추가 DB 쿼리를 할 수 있지만, 다시는 내가 원하는하지 내용은 다음과 같습니다

    void updateMyEntityData(MyEntity e, Data data) { 
        MyEntityImpl impl = repo.findOne(e.getId()); 
        impl.setData(data); 
        repo.saveToDB(impl); 
    } 
    

    내가 MyEntityMyEntityImpl의 인스턴스 인 것을 알고 때문, 불필요한 DB 쿼리이며이었다 이 서비스로 작성된 는 DB의 개체이어야합니다.

    void updateMyEntityData(MyEntity e, Data data) { 
        MyEntityImpl impl = (MyEntityImpl) e; 
        impl.setData(data); 
        repo.saveToDB(impl); 
    } 
    

    요약 : 만 서비스가 MyEntityImpl

  • MyService(Impl) 나중에 MyEntityImpl의 필드를 수정 할 수 있어야합니다 구성 할 수있다

    • (의미 다른 가능성은 캐스트를 사용하는 것입니다 : 설정자가 있어야 함)
    • 불필요한 DB 쿼리를 피하십시오.

    미리 감사드립니다.

  • +0

    패키지 보호 설정? 상속보다 컴포지션을 사용할 수도 있습니다. 즉, MyEntityImpl 및 MyEntity를 래핑하는 클래스를 반환 할 수 있습니다. DAO가 유지하는 데 사용할 수있는 패키지 전용 'getMyEnitityImpl' 메소드를 제공해야합니다. [Lombok] (http://projectlombok.org/)을 사용하는 경우 [@Delegate] (http://projectlombok.org/features/Delegate.html) 코드의 3 줄에서이 작업을 수행 할 수 있습니다. –

    +1

    ' 이 문제를 과소 평가하고 이점을 거의 또는 전혀 얻지 않으면 서 복잡성을 도입해야합니다. 일단 개체가 생성되면 불변성의 다양한 이점이 발생합니다. 이러한 구성은이 작업과 관련하여 거의 중요하지 않습니다. 서비스를 강제로 도메인 객체를 생성하는 데 사용하면 응용 프로그램의 결합이 증가합니다. 왜냐하면 도메인 클래스에 의존하는 대신 서비스를 전달하게 될 것이기 때문입니다. 인터페이스와 impl 사이의 도메인 클래스를 비슷하게 분할하면 코드 기반의 복잡성이 추가되고 imho는 불필요합니다. –

    +0

    흠 ... 내 초기 생각은 : * 그것없이, 일부 컨트롤러 또는 다른 서비스는'MyEntity'의 인스턴스를 생성하고'myService.updateMyEntityData (...)'*를 호출 할 수 있습니다. 그렇다면 전달 된 객체가 실제로 DB의 객체인지 또는 다른 곳에서 만들어진 객체인지 확실하지 않습니다. –

    답변

    3

    나는 public 생성자를 극복해야한다고 생각한다. 저장소/DB에서 검색된 객체 만 유효한 ID을 할당 할 수 있으므로이 매개 변수를 사용하여 업데이트를 제어 할 수 있습니다.

    네, 당신은 아이덴티티를 추측 할 수 있습니다.하지만 당신이 생각하는 어떤 보호 장치를 사용하든 상관없이 모든 작업을 수행 할 수 있습니다. 최대한 활용할 수 있습니다. 인스턴스를 생성하고 필드를 지정할 수 있습니다. 고르다.

    불변성은 적어도 다중 스레드 환경에서보다 고귀한 목표입니다 (다중 스레드가 업데이트를 수행하는 환경에 있지 않은 경우 이점이 덜 명확하고 비용이 들지 않음).

    일반적으로 변이 될 도메인 엔티티와의 무한 경쟁은 문제입니다. 이 문제에 대한 일반적인 접근법은 마지막 변이가 커밋 된 시간을 나타내는 타임 스탬프를 포함하고 변이 된 사본을 사용하는 것입니다.

    public MyEntity 
    { 
        private Object identity; 
        private long mutated; 
        private Data data; 
    
        public MyEntity(Object identity, long mutated, Data data) 
        { 
        this.identity = identity; 
        this.mutated= mutated; 
        this.data = data;   
        } 
    
        public Object getIdentity() 
        { 
        return this.identity; 
        } 
    
        public Data getData() 
        { 
        return this.data; 
        } 
    
        public Builder copy() 
        { 
        return new Builder(); 
        } 
    
        public class Builder 
        { 
        private Data data = MyEntity.this.data; 
    
        public Builder setData(Data data) 
        { 
         this.data = data; 
        } 
    
        public MyEntity build() 
        { 
         return new MyEntity(MyEntity.this.identity, MyEntity.this.mutated, this.data); 
        } 
        } 
    } 
    

    호출 코드는 다음과 같습니다 :이 방법은 비교적 깨끗한 일을 계속하는 동안

    MyEntity mutatedMyEntity = myEntity.copy().setData(new Data()).build(); 
    

    을, 그것을 여기에 빌더 패턴을 사용하여 돌연변이 복사본을 만들 수있는 깨끗한 방법의 예 동시에 다수의 변이 된 사본을 생성하는 다중 스레드의 문제점을 야기합니다.

    정확한 요구 사항에 따라 변경 사항이 커밋 될 때 (saveToDB 메소드) 변경된 타임 스탬프와 최신 버전을 비교하여 충돌을 감지해야합니다 (데이터베이스 히트가 2 회 발생하지 않도록하려면 스토어드 프로 시저의 많은 부분이 있지만, 대안은 쓰기를 수행하는 클래스에서 변경된 시간 소인에 ID 캐시를 유지하는 것입니다. 동일한 엔티티의 다른 인스턴스에 대한 변경 사항을 전파하는 것처럼 충돌 해결은 사용자의 특정 요구 사항에 다시 적용됩니다.엔티티는 어떤 "최종"필드가있는 경우는지도를 할 수없는 경우 스프링 데이터 예외가 발생하기 때문에이 두 번째 생성자의 수,

    public class MyEntity { 
    
        MyEntity() { 
    
        } 
    
        @Id 
        private ObjectId id; 
        public ObjectId getId() { return id; } 
    
        private String someOtherField; 
        public String getSomeOtherField() { return someOtherField; } 
        setSomeOtherField(String someOtherField) { this.someOtherField = someOtherField; } 
    
    } 
    

    :

    +0

    고맙습니다. MongoDB를 사용하고 있으므로 DB의 모든 Object에 UUID가 있습니다. 불변성이 좋은 내 애플리케이션의 실제 문제는 다음과 같습니다. 사용자 비밀번호는 passwordRecoveryKey를 사용해서 만 설정할 수 있으므로 UserService만이 DB의 User 및 PasswordRecoveryKey에 액세스 할 수 있습니다. 이제 사용자가 변경할 수 있다면'u.setPassword (newPassword);를 호출 할 수 있습니다. userUpdateUser (User u, DataToUpdate d);'('updateUser' 메쏘드는'u'의 필드를 갱신 한 다음 전체 obj를 DB에 저장합니다). 이 시나리오에서 암호는 어디서나 토큰없이 업데이트 될 수 있습니다. –

    +0

    'setPassword' 메소드 만 있고'password' 키가 사용되지 않도록 충돌을 피하는 데 적용되는 경우'Builder'에 의해 해결되지 않습니다 타임스? –

    0

    은 이제 더 간단한 방법을 사용했습니다 필드 생성자 매개 변수 이름에 이름,이 방법은 항상 작동합니다

    root 
        |--- service 
        |   |--- MyService.java (public interface) 
        |   |--- MyServiceImpl.java (package protected class implements MyService) 
        |   | 
        |   |--- MyEntity.java (public class) 
        |   | 
        |   |--- MyEntityRepository.java (package protected) 
        | 
        | 
        |------- web 
          |--- MyController.java 
    
    ,691 :

    public class MyEntity { 
    
        protected MyEntity() {} // this one is for Spring Data, 
              // because it can't map 
        MyEntity(Integer i) { // this constructor param "i" 
        this.finalInt = i; // to a field named "i". (The 
        }      // field is called "finalInt") 
    
        @Id 
        private ObjectId id; 
        public ObjectId getId() { return id; } 
    
        private Integer finalInt; 
        public Integer getFinalInt() { return finalInt; } 
    
        private String someOtherField; 
        public String getSomeOtherField() { return someOtherField; } 
        setSomeOtherField(String someOtherField) { this.someOtherField = someOtherField; } 
    
    } 
    

    패키지 레이아웃이 같다

    이제 Controller은 자신의 Entity 개체를 만들 수 없습니다 (적어도 constructor를 사용하지 않는 동안에는) Service (스프링에 의해 ServiceImpl에 연결됨)을 사용해야합니다.

    Repository은 패키지가 보호되어 있기 때문에 Controller에서 액세스 할 수 없으므로 Service에서만 사용할 수 있습니다.

    모든 설정자가 패키지로 보호되어 있기 때문에 Service (및 Repository)은 Entity의 내용을 수정할 수만 있습니다.

    나는이 컨트롤러 코드 내부

    • 저장소 액세스와 같은 나쁜 코드를 많이 방지 할 수 있습니다 꽤 좋은 솔루션입니다 생각

    • 엔티티 컨트롤러에서 수정 및없이 DB에 저장 Service이 제어권을가집니다.

    • 예를 들어 ID가없는 응용 프로그램을 통해 잘못된 (자체 생성 된) 객체를 전달합니다.

    물론 리플렉션을 사용하여이를 우회 할 수는 있지만 요점은 아닙니다. 모든 것은 그것에 대해 깨끗한 코드데이터와 제어 흐름이 명확하게을 정의 된 잘 구조화 된 응용 프로그램의 약 보안 없습니다.

    관련 문제