代码之家  ›  专栏  ›  技术社区  ›  Mauricio

多态性与n层应用

  •  11
  • Mauricio  · 技术社区  · 15 年前

    我有这个疑问很久了…希望有人能照亮我。

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

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

    以及一个服务类,该类具有返回文档(信件或电子邮件)的方法。

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

    所以在我的控制器中,我想显示myservice返回的文档,我想用电子邮件视图和信件视图来显示它。 控制器如何知道哪个文档视图调用?字母视图还是电子邮件视图?。

    我经常在控制器上发出if语句来检查服务层接收到的文档的类型…但是,从面向对象的角度来看,我认为这不是最好的方法,而且如果我实现了一些boolean方法document.isleter(),document.isemail(),那么解决方案在本质上是相同的。

    另一件事是以某种方式将视图选择委托给文档。类似于:

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

    但是,天哪,为什么我的模型对象必须了解视图?

    任何吹捧者都很感激:)

    9 回复  |  直到 15 年前
        1
  •  11
  •   Drew Wills    15 年前

    这是个很好的问题。这里有不止一种可行的方法;你必须权衡取舍,做出适合你情况的选择。

    (1)有人认为文档接口应该为实例提供一种自我呈现的方法。从面向对象的角度来看,这是很有吸引力的,但是根据您的视图技术,加载您的具体文档类(可能是简单的域模型类)并了解jsp、swing组件或其他东西可能是不切实际的,或者是非常难看的。

    (2)有些人建议 String getViewName() 方法打开 Document 例如,返回可以正确呈现该文档类型的jsp文件的路径。这在某种程度上避免了1的丑陋(库依赖关系/“繁重的”代码),但在概念上提出了相同的问题:域模型知道它是由jsp呈现的,它知道webapp的结构。

    (3)尽管有这些问题,但最好是你的控制器类 知道宇宙中存在哪些类型的文档,以及每个实例的类型 文件 属于。考虑在某种基于文本的文件中设置某种视图映射:.properties或.xml。你用弹簧吗?spring di可以帮助您快速指定具体文档类和呈现它们的jsp/view组件的映射,然后将其传递给控制器类的setter/constructor。这种方法允许两者:(1)控制器代码保持不可知 文件 类型和(2)域模型保持简单和不可知的视图技术。它以增量配置为代价:要么.properties要么.xml。

    我会选择3或者——如果我处理这个问题的预算(及时)很小——我会(4)简单地硬编码一些基本知识 文件 输入我的控制器(正如你现在所说的),以便在下次由于不太理想的oo特性而被迫更新控制器时切换到3。事实上,1-3比4需要更长的时间,更复杂,即使它们“更正确”。坚持4也是对 YAGNI Principal :不确定你是否会经历4的负面影响,提前支付避免这些负面影响的成本有意义吗?

        2
  •  2
  •   Brian Agnew    15 年前

    你的控制器 不应该 知道。它应该问 Document 展示自己, 文件 可以这样做,或者提供足够的信息让视图以多态方式处理此问题。

    想象一下,如果在后期添加一个新的 文件 类型(比如, Spreadsheet )你真的只想添加 电子表格 对象(继承自 文件 )一切正常。因此 电子表格 需要提供自我展示的能力。

    或许它可以独立完成。例如

    new Spreadsheet().display();
    

    也许它能做到 结合 以双重调度机制为例

    new Spreadsheet().display(view);
    

    无论哪种情况,电子表格/信件/电子邮件都将实现这一点 view() 方法并负责展示。你的对象应该用某种不可知论的语言来说话。你的文件上写着“用粗体显示”。然后,您的视图可以根据它的类型来解释它。你的物体应该知道风景吗?也许它需要知道视图所具有的功能,但它应该能够以这种不可知的方式进行对话,而不必知道视图的详细信息。

        3
  •  2
  •   woo    15 年前

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

    class ViewFactory {
        public View getView(Letter doc) {
             return new LetterView();
        }
        public View getView(Email doc) {
             return new EmailView();
        }
    }
    
        4
  •  1
  •   Tronic    15 年前

    也许你可以 getView() Document ,在每个实现中重写它?

        5
  •  1
  •   johnny g    15 年前

    我在工作中见过很多次这种“模式”,也见过很多解决方法。说到重点,我建议

    1. 创建新服务 IViewSelector

    2. 实施 选举人 ,或者通过硬编码映射,或者通过配置,然后抛出 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;
        }
    }
    
        6
  •  1
  •   richj    15 年前

    访问者模式可能在这里工作:

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

    visitor是比较有争议的模式之一,尽管有许多变体试图克服原始模式的限制。

    如果“接受”(…)方法可以在文档中实现,那么实现起来更容易,但是模式依赖于“这个”参数的静态类型,所以我认为Java中不可能做到这一点——在这种情况下,您必须重复自己,因为静态的“这”取决于类。保持执行。

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

    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中,因此应该相对容易更改。我喜欢DrewWills在他的回答中关于依赖注入和配置的观点。

        7
  •  1
  •   MCory    15 年前

    首先,德鲁·威尔斯的反应非常好——我是新来的,我还没有资格投票,否则我会的。

    不幸的是,这可能是我自己的经验不足,我不认为你会避免妥协一些分离的关注。有些东西必须知道要为给定的文档创建什么样的视图——这是不可能的。

    正如Drew在第3点中指出的那样,您可以使用某种外部配置来指示系统在哪个视图类上使用哪个文档类型。Drew的Point 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——但至少您的更改将集中到工厂类,而不是控制器内部。

    (我相信在这个例子中有很多地方可以调整——比如验证——但是它应该足够好,能够很好地了解我正在做的事情。)

    希望这有帮助。

        8
  •  1
  •   Arthur Ronald    15 年前

    想做就做!

    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"/>
    

    没别的了!

        9
  •  0
  •   Scharrels    15 年前

    扩展服务以返回文档类型:

    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。

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