2014-03-04 92 views
0

我有一個(希望)簡單的軟件設計問題:我想讓我的實體(=獲得持久化到DB的域對象)有點不可變。意思是:主體應由服務和應用程序的每個另一部分獲得創建與interface其中只有getter方法的工作原理。Spring服務,存儲庫,實體設計模式建議需要

例子:

  1. MyController要檢索MyEntityid=5

  2. MyController有權要求MyService爲了獲取對象:myService.getMyEntityById(5)

  3. MyService會問MyEntityRepository從數據庫獲取對象

  4. MyService返回MyEntityInterfaceMyController

包裝設計:

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

思路:

我的第一個想法是隻需使用一個pac卡格保護的構造函數MyEntityImpl,但這不工作我使用一些其他的庫(即Orika)。因此,它必須是public

接下來的想法是使用MyEntity接口。但現在我已經得到了一些問題:

問題:

MyService(Impl)有一個名爲方法:updateMyEntityData(MyEntity e, Data data)。現在,我不能肯定我的服務,這MyEntity對象是真正的MyEntityImpl一個實例內。當然,我可以做一個if(e instanceof MyEntityImpl) ...,但是這正是我想做的事情。

接下來的問題是:此服務方法使用MyEntityRepository,它可以保存和檢索MyEntityImpl對象,但無法處理接口MyEntity。作爲一種變通方法,我可以做一個額外的數據庫查詢,但同樣這我想:

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

這是不必要的數據庫查詢,因爲我知道MyEntityMyEntityImpl一個實例,它已經由此服務創建,所以它必須是來自DB的對象。另一種可能是使用強制:

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

摘要:

  • 只有服務允許構建MyEntityImpl
  • MyService(Impl)必須能夠修改MyEntityImpl事後領域(指:必須有setter)
  • 避免不必要的數據庫查詢

提前謝謝!

+0

封裝受保護的setters?你也可以使用組合而不是繼承 - 返回一個包裝'MyEntityImpl'和'implements MyEntity'的類。它應該提供一個DAO可以用來持久化的包私有'getMyEnitityImpl'方法。如果你使用[Lombok](http://projectlombok.org/),那麼[@Delegate](http://projectlombok.org/features/Delegate.html)可以在3行代碼中做到這一點... –

+1

You'重新考慮這個問題,並將複雜性引入一點或者幾乎沒有好處 - 一旦對象被構建,那麼不可變性的各種好處就來了,那裏的構造發生在這方面並不重要。強迫你的服務被用來構造域對象會增加你的應用程序的耦合性,因爲你最終會傳遞服務而不僅僅取決於域類。類似地將接口和impl之間的域類拆分只會增加代碼庫的複雜性,並且不必要。 –

+0

嗯...我最初的想法是:*沒有它,一些控制器或其他服務可以創建一個'MyEntity'實例並調用'myService.updateMyEntityData(...)'*。然後我不能確定傳遞的對象是否真的是來自數據庫的對象,或者它是否在其他地方創建。 –

回答

3

我認爲你需要克服公共構造函數。由於只有從存儲庫/數據庫中檢索到的對象可以分配有效的標識,因此可以使用它來控制更新。

是的,你可以猜測身份,但你可以做一些愚蠢的事情來處理任何保護,你認爲你放在適當的位置 - 我可以創建一個實例並重新分配字段,如果我選擇。至少在多線程環境下(如果你不在多線程執行更新的環境中,那麼好處不明顯,而且不值得花費),那麼不變性是更高貴的目標。

問題是不可變性與通常會發生變異的域實體衝突。解決這個問題的一種常見方法是在時間戳中加入一個時間戳,指示最後一次突變的提交時間和使用突變的拷貝。這裏有一個清潔的方式使用建設者格局產生突變的拷貝一個例子:

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方法),方法是檢查最新版本的變異時間戳(以避免兩次數據庫命中,最好做在存儲過程中很多,但替代方法是在執行寫操作的類中將身份緩存保留爲突變時間戳)。衝突解決方案將再次降低到您的具體要求,同時將對同一實體的其他實例進行更改。

+0

謝謝。我使用的是MongoDB,因此DB中的每個對象都有一個UUID。下面是我的應用程序中的一個真實世界的問題,其中不變性是很好的:用戶密碼只能使用passwordRecoveryKey設置,因此只有UserService可以從數據庫訪問User和PasswordRecoveryKey。現在,如果用戶是可變的,可以調用'u.setPassword(newPassword); userService.updateUser(User u,DataToUpdate d);'('updateUser'方法只更新'u'中的字段,然後將整個obj保存到DB中)。在這種情況下,密碼可以從任何地方進行更新,而無需使用令牌 –

+0

如果只有'setPassword'方法,則不能通過'Builder'來解決,並且避免衝突以避免使用密碼恢復密鑰次? –

0

我現在用一個更簡單的辦法:

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; } 

} 

如果實體有一些「最後」的字段,它獲得的第二構造,因爲春天的數據拋出一個異常,如果它不能映射字段名的構造函數的參數名稱,這樣總是工作:

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; } 

} 

封裝佈局是這樣的:

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

現在Controller不能構建自己的Entity對象(至少在使用構造函數時不會),並且它必須使用Service(由Spring連接到ServiceImpl)。

Repository不能被Controller訪問,因爲它受封裝保護,因此只能由Service使用。

只有Service(和Repository)才能修改Entity的內容,因爲所有設置程序都受到程序包保護。

我覺得這是一個相當不錯的解決方案,它可以防止大量的惡意代碼一樣

  • 控制器代碼裏面存儲庫訪問

  • 實體修改在控制器和保存到數據庫,而不Service有控制

  • 通過應用程序(例如沒有ID)傳遞無效(自建)對象。

當然,仍然可以使用反射繞過它,但這不是重點。整個事情是不是安全,它是關於乾淨的代碼結構良好的應用其中數據和控制流程明確規定