多态性和N层应用程序

12

我一直有这个疑问...希望有人可以给我启发。

假设我的模型中有3个类。

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

还有一个服务类,其中包含一个返回文档(信函或电子邮件)的方法。

class MyService {
    public Document getDoc(){...}
}
所以在我的控制器中,我想要显示我的服务返回的文档,并且我希望它使用电子邮件视图和信件视图来显示。控制器如何知道调用哪个文档视图?信件视图还是电子邮件视图?
通常,我在控制器中使用if语句来检查服务层接收到的文档类型... 但从面向对象编程的角度来看,这可能不是最佳方法,而且如果我实现一些布尔方法Document.isLetter()、Document.isEmail(),解决方案本质上是相同的。
另一种方法是通过某种方式将视图选择委托给文档。例如:
class MyController {
    public View handleSomething() {
        Document document = myService.getDocument();
        return document.getView();
    }
}

但是,天哪,为什么我的模型对象必须了解视图(view)的任何信息呢?

欢迎各位提出任何想法 :)

9个回答

11

这是一个很好的问题。有不止一种可行的方法,你需要平衡权衡并选择适合你情况的方法。

(1)有人会争辩说,Document接口应该提供一种方法,使实例能够呈现自己。从面向对象的角度来看,这很有吸引力,但根据你的视图技术,将具体的Document类(可能是简单的领域模型类)加载知道JSP、Swing组件或其他技术可能是不切实际的或非常丑陋的。

(2)有些人会建议在Document上放置一个String getViewName()方法,返回例如可以正确呈现该文档类型的JSP文件的路径。这避免了#1中的丑陋之处(库依赖/“重量级”代码),但在概念上提出了相同的问题:您的领域模型知道它正在被JSP渲染,并且知道您Web应用程序的结构。

(3)尽管存在这些观点,但最好如果你的Controller类不知道宇宙中存在哪种类型的文档以及每个Document实例属于哪种类型。考虑在某种文本文件中设置某种视图映射:无论是.properties还是.xml文件。你使用Spring吗?Spring DI可以帮助你快速指定具体的Document类和呈现它们的JSP/视图组件的Map,然后将其传递给你的Controller类的setter/构造函数。这种方法允许(1)你的Controller代码保持对Document类型不可知,以及(2)你的领域模型保持简单且不知道视图技术。它的成本是增量配置:无论是.properties还是.xml文件。

如果我时间预算不多,我会选择#3或者干脆在我的控制器中硬编码一些基本的Document类型知识(就像你现在正在做的那样),以便在将来的更新中切换到#3,因为1-3每个选项都比#4更复杂、耗时更长,即使它们“更正确”。坚持使用#4也是对YAGNI原则的认可:没有确定你会经历#4的负面影响,那么提前付出避免这些影响的代价是否有意义?


2

您的控制器不应该知道如何展示文档。它应该向Document请求展示自己,而Document可以执行此操作或提供足够的信息以让视图能够多态地处理。

想象一下,如果在以后的阶段中添加了一个新的Document类型(比如说,Spreadsheet),您只需要添加继承自DocumentSpreadsheet对象并使其正常工作即可。因此,Spreadsheet需要提供展示自己的能力。

也许它可以独立完成。例如:

new Spreadsheet().display();

也许它可以与视图结合使用,例如双重调度机制。
new Spreadsheet().display(view);

无论哪种情况,电子表格/信件/电子邮件都将实现此view()方法并负责显示。您的对象应该使用某种与视图无关的语言进行交流。例如,您的文档说“以粗体显示此内容”。然后,您的视图可以根据其类型进行解释。您的对象是否需要了解视图?也许它需要知道该视图具有的功能,但是它应该能够以这种不知道视图详细信息的方式进行交流。

@Brian Agnew - 我喜欢这个答案,但我认为它让人们感到困惑,不知道如何实现。即使您声明了 new spreadsheet().display();,我保证人们会质疑 display 是什么样子的...他们最终会用代码测试 typeof(eachObject)。如果您更深入地解释一下 display,我会给您点赞的。 :) - JonH

2

我不确定,但你可以尝试添加一个基于函数重写的工厂类,并假设根据文档类型返回视图。例如:

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

“工厂”模式是我认为最好的方式。它应该在“common”包中,与您的接口一起。+1 - Clement Herreman
我认为那样做行不通。为了调用ViewFactory.getView()方法,你需要一个正确类型(Letter或Email)的引用,但是服务返回一个Document,这让我们面临着原始问题。谢谢! - Mauricio
我认为该服务返回特定类型的对象,然后将其转换为文档。我错了吗? - woo
是的,该服务返回一个文档。 - Mauricio
是的,根据签名,这是一个文件。我认为它降低了一个正确的类型,比如电子邮件或信件。 - woo
2
是的,但至少在Java中,方法调用的参数匹配是在编译时完成的。因此,使用Document引用,您无法调用getView(Letter d)或getView(Email d)。 - Mauricio

1
首先,Drew Wills的回答非常好 - 我在这里是新手,还没有足够的声望来投票,否则我会的。
不幸的是,也许是因为我自己缺乏经验,我看不到任何避免妥协某些关注点分离的方法。必须有一些东西知道要为给定的文档创建哪种视图 - 这是无法避免的。
正如Drew在第3点中指出的那样,您可以使用某种外部配置来指示系统使用哪个View类来处理哪种文档类型。Drew的第4点也是一个不错的选择,因为即使它违反了开闭原则(我相信这是我想到的那个),如果您只有少量的文档子类型,那么可能并不值得费心。
对于后一点的变化,如果您想避免使用类型检查,您可以实现一个工厂类/方法,该方法依赖于文档子类型到视图实例的映射。
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;
    }
}

当然,每当您需要向地图添加新视图时,仍然需要编辑initializeViews方法--因此它仍然违反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"/>

仅此而已!


@Mauricio 如上所示,它可以输出 render-Email.jsp 或 render-Letter.jsp。 - Arthur Ronald

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

理论上是正确的,但实现可以显著改进。使用泛型,您应该能够消除对对象的引用和显式类型检查/转换。 - CurtainDog

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

访问者是较具争议的模式之一,尽管有许多变种试图克服原始模式的限制。

如果可以在 Document 中实现 accept(...) 方法,那么它将更容易实现,但该模式依赖于“this”参数的静态类型,因此我认为在 Java 中这是不可能的 - 因为“this”的静态类型取决于实现它的类,所以你必须在此情况下重复自己。

如果文档类型的数量相对较少且不太可能增长,而视图类型的数量更有可能增长,则这将起作用。否则,我会寻找一种使用第三个类来协调显示并尝试保持 View 和 Document 独立的方法。这第二种方法可能如下:

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
也许你可以在每个实现中重写 Document 中的 getView() 方法,这样就能得到类似的东西了。

嗨!抱歉,但这不是一个好建议。 "模型" 表示业务数据 [有时我们会变懒并添加业务逻辑:S]。然而,模型不知道它应该如何呈现。考虑到“最终用户”应用程序与“管理员”应用程序。两个应用程序都可以利用相同的业务层和模型,但每个应用程序可能希望有不同的视图[管理员可能有更多的数据]。将视图选择嵌入模型中将限制相同的视图到两个应用程序。除非您实现覆盖,否则该方法将无效。一般来说,模型应该是与演示无关的。 - johnny g

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

问题是关于良好的面向对象编程实践。您的答案采用了过程化方法,而不是面向对象的方法。问题在于,过程化方法无法很好地扩展,因为“客户端代码”与“库代码”的当前实现紧密耦合。 - richj
我意识到了。希望我的当前答案更适合这个问题。 - Scharrels
当然可以 - 但如果您需要类型测试,我倾向于将它们放在工厂类中。通过针对视图类注册处理程序,您可能可以做得更好。 - richj

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接