2010-02-16 66 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