2010-02-16 70 views
11

我有這個懷疑很長一段時間...希望任何人都能照亮我。多態性和多層應用程序

假設我在模型中有3個類。

abstract class Document {} 
class Letter extends Document {} 
class Email extends Document {} 

和帶有返回文檔(Letter或Email)的方法的服務類。

class MyService { 
    public Document getDoc(){...} 
} 

所以在我的控制器,我想通過顯示爲MyService返回的文檔,我希望它使用的字母的電子郵件和其他視圖中顯示。 控制器如何知道哪個文檔視圖被調用? letterView或emailView ?.

我經常在控制器上做一個if語句來檢查服務層收到的Document的類型......但是我不認爲這是從OOP的角度來看最好的方法,如果我實現了一些布爾方法Document.isLetter(),Document.isEmail()的解決方案本質上是相同的。

另一件事是以某種方式將視圖選擇委託給文檔。例如:

class MyController { 
    public View handleSomething() { 
     Document document = myService.getDocument(); 
     return document.getView(); 
    } 
} 

但是,omg,爲什麼我的模型對象必須對視圖有所瞭解?

任何toughts讚賞:)

回答

11

這是一個很好的問題。這裏有不止一種合理的方法;你必須平衡權衡,並作出適合你情況的選擇。 (1)有些人會爭辯說,Document接口應該爲實例呈現自己提供一種方法。從面向對象的角度來看,這很有吸引力,但根據您的視圖技術,加載具體的文檔類(可能是簡單的領域模型類)並不瞭解JSP或Swing組件等方面的內容。

(2)有些人會建議把也許String getViewName()方法上Document返回,例如,路徑可以正確呈現該文檔類型的JSP文件。這樣可以避免#1在一個層次上的醜陋(庫依賴性/「繁重」代碼),但概念上也會帶來同樣的問題:域模型知道它是由JSP呈現的,並且它知道您的web應用程序的結構。

(3)儘管有這些觀點,但如果您的Controller類不知道會知道Universe中存在哪些類型的文檔以及Document的每個實例屬於哪種類型。考慮在某種基於文本的文件中設置某種視圖映射:.properties或.xml。你使用Spring嗎? Spring DI可以幫助您快速指定具體文檔類的Map和呈現它們的JSP /視圖組件,然後將其傳遞給Controller類的setter/constructor。這種方法允許:(1)您的Controller代碼保持不受Document類型的影響;(2)您的域模型保持對視圖技術的簡單和不可知性。它的代價是增量配置:.properties或.xml。如果我的預算(及時)處理這個問題很小 - 我會(4)在我的控制器中簡單地編寫Document類型的一些基本知識(正如你現在所說的那樣),以便在未來下次由於OO特性低於最佳而被迫更新控制器時,轉向#3。事實是#1-3比#4更復雜,比「#更復雜」,即使它們「更正確」。與#4一起堅持也是對YAGNI Principal的肯定:沒有把握能夠體驗#4的負面影響,支付預先避開它們的費用是否合理?

+0

如果可以的話,我會投票兩次。非常好的答案。 – 2010-02-16 21:07:36

1

也許你可以有一些像getView()Document,在每個實現覆蓋它?

+0

嗨!道歉爲-1,但這不是明智的。一個「模型」代表業務數據[有時我們懶惰並且增加業務邏輯:S]。然而,該模型並不知道應該如何呈現。考慮一個「最終用戶」應用程序與「管理」應用程序。這兩個應用程序都可以利用相同的業務層和Model,但每個應用程序可能希望有不同的View [管理員可能有更多數據]。在模型中嵌入視圖選擇會將同一視圖約束到兩個應用程序。除非您實施覆蓋,否則將使該方法無效。一般來說,模型應該與表現無關。 – 2010-02-16 21:41:32

2

您的控制器應該不知道知道。它應該要求Document顯示自己,而Document可以做到這一點,或提供足夠的信息讓視圖多態地處理。

想象一下,如果在稍後階段添加新的Document類型(例如,Spreadsheet)。你真的只想添加Spreadsheet對象(繼承自Document)並且一切正常。因此Spreadsheet需要提供顯示自身的能力。

也許它可以做到獨立。例如

new Spreadsheet().display(); 

也許它可以與查看做到這一點在結合例如雙調度機制

new Spreadsheet().display(view); 

在這兩種情況下,電子表格/信件/郵件將都實現了這個view()方法和負責顯示。你的對象應該以某種視圖不可知的語言進行交談。例如你的文件說「以粗體顯示」。然後,您的視圖可以根據其類型進行解釋。你的對象應該知道視圖嗎?也許它需要知道這個觀點所具有的功能,但是它應該能夠在不知道視圖細節的情況下以這種不可知論的方式進行討論。

+0

@Brian Agnew - 我喜歡這個答案,但我認爲這讓人們不知道如何做到這一點。儘管你陳述了'新電子表格()。display();'我保證人們質疑什麼樣的顯示看起來像人類最終會進行代碼測試typeof(eachObject)。如果你更深入地瞭解展示,我會給你+1。 :) – JonH 2010-02-16 20:52:26

0

擴展您的服務來回報文檔的類型:​​

class MyService { 

    public static final int TYPE_EMAIL = 1; 
    public static final int TYPE_LETTER = 2; 

    public Document getDoc(){...} 
    public int getType(){...} 
} 

在一個更面向對象的方法,使用的ViewFactory返回的電子郵件和信件有不同的看法。使用視圖處理程序使用的ViewFactory,你可以問每一個處理程序,如果它可以處理文檔:

class ViewFactory { 
    private List<ViewHandler> viewHandlers; 

    public viewFactory() { 
     viewHandlers = new List<ViewHandler>(); 
    } 

    public void registerViewHandler(ViewHandler vh){ 
     viewHandlers.add(vh); 
    } 

    public View getView(Document doc){ 
     for(ViewHandler vh : viewHandlers){ 
      View v = vh.getView(doc); 
      if(v != null){ 
      return v; 
      } 
     } 
     return null; 
    } 
} 

有了這個工廠,當你添加新的視圖類型的工廠類並不需要改變。視圖類型可以分別檢查它們是否可以處理給定的文檔類型。如果他們不能,他們可以返回null。否則,他們可以返回您需要的視圖。如果沒有視圖可以處理您的文檔,則返回null。

的ViewHandlers可以很簡單:

public interface ViewHandler { 
    public getView(Document doc) 
} 

public class EmailViewHandler implements ViewHandler { 
    public View getView(Document doc){ 
     if(doc instanceof Email){ 
     // return a view for the e-mail type 
     } 
     return null; // this handler cannot handle this type 
    } 
} 
+0

問題是關於良好的面向對象編程實踐。你的答案使用程序方法,而不是面向對象的方法。問題在於程序方法不能很好地擴展,因爲「客戶代碼」與當前的「庫代碼」實現強烈耦合。 – richj 2010-02-16 20:41:47

+0

我意識到這一點。我希望我現在的回答更適合這個問題。 – Scharrels 2010-02-16 20:48:58

+0

當然 - 但如果你需要型式測試,我會傾向於把它們放在工廠班。通過在視圖類中註冊處理程序,您可能會做得更好。 – richj 2010-02-16 21:07:31

2

我不知道,但你可以嘗試添加基於覆蓋功能的工廠類,並承擔返回根據文件類型的視圖。例如:

class ViewFactory { 
    public View getView(Letter doc) { 
     return new LetterView(); 
    } 
    public View getView(Email doc) { 
     return new EmailView(); 
    } 
} 
+0

「工廠」模式是我認爲最好的方式。它應該在「通用」軟件包中,你的界面也是這樣。 +1 – 2010-02-16 20:48:33

+0

我不認爲這會起作用。爲了調用ViewFactory.getView()方法,您需要一個適當類型的引用(Letter或Email),但該服務返回一個Document,並留下原始問題。感謝名單! – Mauricio 2010-02-16 20:53:59

+0

我認爲服務返回一個特定類型的對象,這是一個文件。我錯了嗎? – woo 2010-02-16 21:20:00

1

我在工作中多次看到過這種「模式」,並且已經看到很多方法來解決它。爲了這一點,我建議

  1. 創建新的服務IViewSelector

  2. 或者通過硬編碼映射或通過配置實現IViewSelector,和投擲NotSupportedException每當一個無效的請求。

這將執行您需要同時促進相關分離映射[的SoC]

// a service that provides explicit view-model mapping 
// 
// NOTE: SORRY did not notice originally stated in java, 
// pattern still applies, just remove generic parameters, 
// and add signature parameters of Type 
public interface IViewSelector 
{ 

    // simple mapping function, specify source model and 
    // desired view interface, it will return an implementation 
    // for your requirements 
    IView Resolve<IView>(object model); 

    // offers fine level of granularity, now you can support 
    // views based on source model and calling controller, 
    // essentially contextual views 
    IView Resolve<IView, TController>(object model); 

} 

由於使用的例子,請考慮以下

public abstract Document { } 
public class Letter : Document { } 
public class Email : Document { } 

// defines contract between Controller and View. should 
// contain methods common to both email and letter views 
public interface IDocumentView { } 
public class EmailView : IDocumentView { } 
public class LetterView : IDocumentView { } 

// controller for a particular flow in your business 
public class Controller 
{ 
    // selector service injected 
    public Controller (IViewSelector selector) { } 

    // method to display a model 
    public void DisplayModel (Document document) 
    { 
     // get a view based on model and view contract 
     IDocumentView view = selector.Resolve<IDocumentView> (model); 
     // er ... display? or operate on? 
    } 
} 

// simple implementation of IViewSelector. could also delegate 
// to an object factory [preferably a configurable IoC container!] 
// but here we hard code our mapping. 
public class Selector : IViewSelector 
{ 
    public IView Resolve<IView>(object model) 
    { 
     return Resolve<IView> (model, null); 
    } 

    public IView Resolve<IView, TController>(object model) 
    { 
     return Resolve<IView> (model, typeof (TController)); 
    } 

    public IView Resolve<IView> (object model, Type controllerType) 
    { 
     IVew view = default (IView); 
     Type modelType = model.GetType(); 
     if (modelType == typeof (EmailDocument)) 
     { 
      // in this trivial sample, we ignore controllerType, 
      // however, in practice, we would probe map, or do 
      // something that is business-appropriate 
      view = (IView)(new EmailView(model)); 
     } 
     else if (modelType == typeof (LetterDocument)) 
     { 
      // who knows how to instantiate view? well, we are 
      // *supposed* to. though named "selector" we are also 
      // a factory [could also be factored out]. notice here 
      // LetterView does not require model on instantiation 
      view = (IView)(new LetterView()); 
     } 
     else 
     { 
      throw new NotSupportedOperation (
       string.Format (
       "Does not currently support views for model [{0}].", 
       modelType)); 
     } 
     return view; 
    } 
} 
+0

理論+1,但實現可以顯着提高。使用泛型您應該能夠消除對對象的引用和顯式類型檢查/鑄造。 – CurtainDog 2010-02-17 00:10:01

1

訪問者模式可能會在這裏工作:

abstract class Document { 
    public abstract void accept(View view); 
} 

class Letter extends Document { 
    public void accept(View view) { view.display(this); } 
} 

class Email extends Document { 
    public void accept(View view) { view.display(this); } 
} 

abstract class View { 
    public abstract void display(Email document); 
    public abstract void display(Letter document); 
} 

訪客是比較有爭議的模式之一,雖然有一些變體試圖克服原始模式的侷限性。

如果accept(...)方法可以在Document中實現,但該模式依賴於「this」參數的靜態類型,那麼實現會更容易,所以我不認爲這是可能的Java - 你必須在這種情況下重複自己,因爲「this」的靜態類型取決於持有實現的類。

如果文檔類型的數量相對較小並且不可能增長,並且視圖類型的數量更可能增長,那麼這將起作用。否則,我會尋找一種方法,使用第三類來協調顯示,並嘗試保持視圖和文檔獨立。這第二種方法可能是這樣的:

abstract class Document {} 
class Letter extends Document {} 
class Email extends Document {} 

abstract class View {} 
class LetterView extends View {} 
class EmailView extends View {} 

class ViewManager { 
    public void display(Document document) { 
     View view = getAssociatedView(document); 
     view.display(); 
    } 

    protected View getAssociatedView(Document document) { ... } 
} 

的ViewManager的目的是記錄實例與視圖實例(或視圖類型,如果僅關聯(或文檔類型,如果只有一個給定類型的文件即可打開)一個給定類型的視圖可以打開)。如果一個文件可以有多個相關的意見,然後ViewManager的實現可能像下面這個:

class ViewManager { 
    public void display(Document document) { 
     List<View> views = getAssociatedViews(document); 

     for (View view : views) { 
      view.display(); 
     } 
    } 

    protected List<View> getAssociatedViews(Document document) { ... } 
} 

視圖文檔關聯邏輯,取決於你的應用。它可以像它需要的那樣簡單或複雜。關聯邏輯封裝在ViewManager中,因此它應該相對容易更改。我喜歡Drew Wills在關於依賴注入和配置的回答中所提出的觀點。

1

首先,德魯威爾斯的回答非常好 - 我在這裏是新來的,我還沒有投票的聲望呢,否則我會的。

不幸的是,這可能是我自己缺乏的經驗,我不認爲你會以任何方式避免損害一些分離的擔憂。一些東西將不得不知道爲給定的文檔創建什麼樣的視圖 - 這是沒有辦法的。

正如Drew在第3點指出的那樣,您可以使用某種外部配置來指導您的系統在哪個View類中使用哪種文檔類型。德魯的第四點也是一個不錯的方法,因爲即使它打破了開放/封閉的原則(我相信那是我想到的),如果你只有少數的文檔子類型,它可能不值得大驚小怪。

關於這後一點的變化,如果你想避免使用類型檢查,你可以實現一個工廠類/方法依賴於地圖文檔子類型查看實例:

public final class DocumentViewFactory { 
    private final Map<Class<?>, View> viewMap = new HashMap<Class<?>, View>(); 

    private void addView(final Class<?> docClass, final View docView) { 
     this.viewMap.put(docClass, docView); 
    } 

    private void initializeViews() { 
     this.addView(Email.class, new EmailView()); 
     this.addView(Letter.class, new LetterView()); 
    } 

    public View getView(Document doc) { 
     if (this.viewMap.containsKey(doc.getClass()) { 
      return this.viewMap.get(doc.getClass()); 
     } 

     return null; 
    } 
} 

中當然,當你需要添加一個新視圖到地圖時,你仍然需要編輯initializes方法 - 所以它仍然違反OCP - 但至少你的改變將集中到你的Factory類而不是你的內部控制器。

(我敢肯定有很多,可能在例如進行調整 - 驗證,對一個 - 但它應該是足夠好得到什麼,我在得到一個好主意)

希望這可以幫助。

1

只要做到這一點!

public class DocumentController { 
    public View handleSomething(request, response) { 
     request.setAttribute("document", repository.getById(Integer.valueOf(request.getParameter("id")))); 

     return new View("document"); 
    } 
} 

...

// document.jsp 

<c:import url="render-${document.class.simpleName}.jsp"/> 

沒有別的!

+0

@Mauricio如上所示,它可以輸出render-Email.jsp或render-Letter.jsp – 2010-03-31 05:44:56