полиморфизм и n-уровневые приложения - PullRequest
11 голосов
/ 16 февраля 2010

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

Предположим, у меня есть 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();
    }
}

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

Любые жесты приветствуются:)

Ответы [ 9 ]

11 голосов
/ 17 февраля 2010

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

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

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

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

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

2 голосов
/ 16 февраля 2010

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

class ViewFactory {
    public View getView(Letter doc) {
         return new LetterView();
    }
    public View getView(Email doc) {
         return new EmailView();
    }
}
2 голосов
/ 16 февраля 2010

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

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

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

new Spreadsheet().display();

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

new Spreadsheet().display(view);

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

1 голос
/ 31 марта 2010

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

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 голос
/ 23 февраля 2010

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

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

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

Для изменения этого последнего пункта, если вы хотите избежать использования проверок типов, вы можете реализовать фабричный класс / метод, который использует подтипы Map of Document для просмотра экземпляров:

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 голос
/ 17 февраля 2010

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

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» зависит от класса, содержащего реализацию.

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

1 голос
/ 17 февраля 2010

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

  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 голос
/ 16 февраля 2010

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

0 голосов
/ 16 февраля 2010

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

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

С этой фабрикой, ваш класс фабрики не должен изменяться, когда вы добавляете новые типы представлений. Каждый тип представления может проверить, могут ли они обрабатывать данный тип документа. Если они не могут, они могут вернуть ноль. В противном случае они могут вернуть нужный вам вид. Если ни одно представление не может обработать ваш документ, возвращается ноль.

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
   }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...