Подтвердить что ты не робот

Полиморфизм и n-ярусные приложения

У меня есть это сомнение в течение долгого времени... надеюсь, что кто-то может рассказать мне.

Предположим, что у меня есть 3 класса в моей модели.

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

и класс обслуживания с методом, который возвращает документ (либо письмо, либо электронную почту).

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

Итак, в моем контроллере я хочу отобразить Документ, возвращенный MyService, и я хочу, чтобы он отображался, используя представление для электронной почты и другое для письма. Как контроллер может узнать, какой вид документа вызывается? letterView или emailView?.

Часто я делаю инструкцию if на контроллере, чтобы проверить тип документа, полученный уровнем обслуживания... однако я не думаю, что это лучший подход с точки зрения ООП, также если я реализую несколько логических методы Document.isLetter(), Document.isEmail(), по сути, это решение.

Еще одна вещь - как-то делегировать выбор представления в Документ. что-то вроде:

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

Но, omg, почему мои объекты модели должны ничего знать о представлении?

Любые нагрудные знаки оценены:)

4b9b3361

Ответ 1

Это отличный вопрос. Здесь существует более чем один правдоподобный подход; вы должны сбалансировать компромиссы и сделать выбор, который соответствует вашим обстоятельствам.

(1) Некоторые утверждают, что интерфейс Document должен предоставить метод для экземпляров, чтобы сделать сами. Это привлекательно с точки зрения OO, но в зависимости от ваших технологий просмотра может оказаться непрактичным или совершенно уродливым, чтобы загрузить ваши конкретные классы документов, которые, вероятно, являются простыми классами моделей домена - со знанием JSP, Swing Components или что-то еще.

(2) Некоторые из них предполагают, что возможно использовать метод String getViewName() на Document, который возвращает, например, путь к JSP файлу, который может правильно отобразить этот тип документа. Это позволяет избежать уродства №1 на одном уровне (библиотека зависимости/код тяжелой атлетики), но концептуально ставит ту же проблему: ваша модель домена знает, что она отображается JSP, и она знает структуру вашего веб-сервера.

(3) Несмотря на эти моменты, лучше, если ваш класс Controller не знает, какие типы документов существуют в юниверсе и к какому типу принадлежит каждый экземпляр Document. Подумайте о настройке какого-либо вида отображения в виде текстового файла:.properties или .xml. Используете ли вы Spring? Spring DI может помочь вам быстро определить карту конкретных классов документов и компоненты JSP/view, которые их выполняют, а затем передать в конструктор/конструктор класса Controller. Этот подход позволяет: (1) ваш код контроллера оставаться агностиком типов Document и (2) модель вашего домена оставаться простой и агностикой технологий просмотра. Он приходит за счет инкрементной конфигурации: либо .properties, либо .xml.

Я бы пошел на # 3 или - если мой бюджет (по времени) для работы над этой проблемой невелик - я бы (4) просто жестко кодировал некоторые базовые знания типов Document в моем контроллере (как вы говорите, вы делаете сейчас) с целью перехода на # 3 в будущем в следующий раз, когда я вынужден обновить свой контроллер из-за менее оптимальных характеристик OO. Дело в том, что # 1-3 каждый занимает больше времени и сложнее, чем # 4, даже если они "более правильные". Приклеивание С# 4 также является кивком YAGNI Principal: нет уверенности, что вы когда-нибудь столкнетесь с отрицательными эффектами №4, это иметь смысл оплатить расходы, чтобы избежать их на передний план?

Ответ 2

Ваш контроллер не должен знать. Он должен попросить Document отобразить себя, и Document может это сделать или предоставить достаточную информацию, чтобы позволить View обрабатывать это полиморфно.

Представьте, если на более позднем этапе вы добавите новый тип Document (скажем, Spreadsheet). Вы действительно хотите добавить объект Spreadsheet (наследующий от Document) и все работает. Следовательно, Spreadsheet должен обеспечить возможность отображения самого себя.

Возможно, он может сделать это отдельно. например.

new Spreadsheet().display();

Возможно, он может сделать это в сочетании с View, например. механизм двойной отправки

new Spreadsheet().display(view);

В любом случае электронная таблица/письмо/электронная почта будут реализовывать этот метод view() и отвечать за отображение. Ваши объекты должны говорить на некотором взгляде - агностический язык. например ваш документ говорит "отобразить это жирным шрифтом". Затем ваш взгляд может интерпретировать его в соответствии с его типом. Должен ли ваш объект знать о представлении? Возможно, ему нужно знать возможности, которые есть у этого взгляда, но он должен уметь говорить этим агностическим способом, не зная подробностей представления.

Ответ 3

Я не уверен, но вы можете попробовать добавить класс factory, основанный на переопределении функций, и предположительно вернуть представление в зависимости от типа документа. Например:

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

Ответ 4

Может быть, у вас может быть что-то вроде getView() в Document, переопределяя его в каждой реализации?

Ответ 5

Я видел эту "модель" много раз в своей работе и видел много подходов к ее решению. К моменту, я бы предложил

  • Создайте новый сервис IViewSelector

  • Внедрите 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;
    }
}

Ответ 6

Здесь может работать шаблон посетителя:

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 (...) может быть реализован в документе, но шаблон использует статический тип параметра "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, поэтому ее относительно легко изменить. Мне нравятся моменты, которые Дрю Виллс сделал в его ответе относительно вливания и конфигурации зависимостей.

Ответ 7

Во-первых, ответ Дрю Уиллса абсолютно велик - я здесь новый, и у меня нет репутации, чтобы проголосовать за него еще, или я бы это сделал.

К сожалению, и это может быть мой собственный недостаток опыта, я не вижу никакого способа, чтобы вы избежали компрометации некоторого разделения беспокойства. Что-то нужно будет знать, какой вид создать для данного документа - там просто нет.

Как указал Дрю в пункте № 3, вы могли бы найти какую-то внешнюю конфигурацию, которая бы проинструктировала вашу систему, какой класс View использовать для какого типа документа. Drew point # 4 также является достойным способом, потому что, хотя он нарушает принцип Open/Closed (я считаю, что тот, о котором я думаю), если у вас будет только несколько подтипов Document, это, вероятно, не стоит суетиться.

Для варианта этой последней точки, если вы хотите избежать использования проверок типов, вы можете реализовать класс/метод factory, который опирается на подтипы Map of Document на 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;
    }
}

Конечно, вам все равно придется редактировать метод initializeViews всякий раз, когда вам нужно добавить новое представление на карту, поэтому он по-прежнему нарушает OCP, но по крайней мере ваши изменения будут централизованы для вашего factory вместо внутреннего контроллера.

(Я уверен, что в этом примере могут быть исправлены ошибки - валидация, для одного - но это должно быть достаточно хорошо, чтобы получить представление о том, что я получаю.)

Надеюсь, что это поможет.

Ответ 8

Просто сделай это!

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

Расширьте службу, чтобы вернуть тип документа:

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

С помощью этого factory класс factory не должен изменяться при добавлении новых типов просмотров. Типы просмотра могут проверять, могут ли они обрабатывать указанный тип документа. Если они не могут, они могут вернуть 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
   }
}