полиморфизм и n-tier приложения

У меня есть это сомнение, в течение долгого времени... надеются, что любой может enlight меня.

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

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

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

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

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

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

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

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

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

Любые мысли ценятся:)

11
задан Mauricio 16 February 2010 в 20:29
поделиться

9 ответов

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

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

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

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

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

.
11
ответ дан 3 December 2019 в 05:57
поделиться

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

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
ответ дан 3 December 2019 в 05:57
поделиться

Шаблон «Посетитель» может работать здесь:

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

1
ответ дан 3 December 2019 в 05:57
поделиться

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

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

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

new Spreadsheet().display();

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

new Spreadsheet().display(view);

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

2
ответ дан 3 December 2019 в 05:57
поделиться

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

class ViewFactory {
    public View getView(Letter doc) {
         return new LetterView();
    }
    public View getView(Email doc) {
         return new EmailView();
    }
}
2
ответ дан 3 December 2019 в 05:57
поделиться

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

1
ответ дан 3 December 2019 в 05:57
поделиться

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

  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;
    }
}
1
ответ дан 3 December 2019 в 05:57
поделиться

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

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

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

В качестве варианта последнего пункта, если вы хотите избежать использования проверок типов, вы можете реализовать фабричный класс / метод, который полагается на подтипы 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, а не внутри вашего контроллера.

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

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

1
ответ дан 3 December 2019 в 05:57
поделиться

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

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

Больше ничего!

1
ответ дан 3 December 2019 в 05:57
поделиться
Другие вопросы по тегам:

Похожие вопросы: