I have this doubt for a long time... hope anyone can enlight me.
Suppose I have 3 classes in my model.
abstract class Document {}
class Letter extends Document {}
class Email extends Document {}
and a service class with a method that returns a Document (either a Letter or Email).
class MyService {
public Document getDoc(){...}
}
So in my controller I want to display the Document returned by MyService, and I want it to be displayed using a view for the Email and other for the Letter. How could a controller know which document view invoke? the letterView or the emailView?.
Often I make an if statement on the controller to check the type of the Document received by the service tier... however I dont think it is the best approach from the OOP point of view, also if I implement a few boolean methods Document.isLetter(), Document.isEmail() the solution is, in essence, the same.
Another thing is to delegate the view selection to the Document somehow. something like:
cla开发者_如何转开发ss MyController {
public View handleSomething() {
Document document = myService.getDocument();
return document.getView();
}
}
But, omg, why my model objects must know anything about the view?
Any toughts are appreciated :)
This is a great question. There is more than one plausible approach here; you have to balance the trade-offs and make a choice that suits your circumstances.
(1) Some will argue that that Document interface should provide a method for instances to render themselves. This is attractive from an OO standpoint, but depending on your view technologies, it may be impractical or downright ugly to load your concrete Document classes -- which are probably simple domain model classes -- with knowledge of JSPs, Swing Components, or whatever.
(2) Some will suggest putting perhaps a String getViewName()
method on Document
that returns, for example, the path to a JSP file that can properly render that document type. This avoids the ugliness of #1 at one level (library dependencies/"heavy lifting" code), but conceptually poses the same problem: your domain model knows that it's being rendered by JSPs and it knows the structure of your webapp.
(3) Despite these points, it's better if your Controller class doesn't know what types of documents exist in the universe and which type each instance of Document
belongs to. Consider setting up some sort of view-mapping in some sort of text-based file: either .properties or .xml. Do you use Spring? Spring DI can help you quickly specify a Map of concrete Document classes and the JSP/view components that render them, then pass it to a setter/constructor of you Controller class. This approach allows both: (1) your Controller code to remain agnostic of Document
types and (2) your domain model to remain simple and agnostic of view technologies. It comes at the cost of incremental configuration: either .properties or .xml.
I'd go for #3 or -- if my budget (in time) for working on this problem is small -- I'd (4) simply hard-code some basic knowledge of Document
types in my Controler (as you say you're doing now) with a view toward switching to #3 in the future the next time I'm forced to update my Controller due to less-than-optimal OO characteristics. The fact is that #s 1-3 each take longer and are more complex than #4, even if they are "more correct." Sticking with #4 is also a nod to the YAGNI Principal: there's no certainty you'll ever experience the negative effects of #4, does it make sense to pay the cost of avoiding them up-front?
Your controller shouldn't know. It should ask the Document
to display itself, and the Document
can do this, or provide sufficient information to let the View handle this polymorphically.
Imagine if at a later stage you add a new Document
type (say, Spreadsheet
). You really only want to add the Spreadsheet
object (inheriting from Document
) and have everything work. Consequently the Spreadsheet
needs to provide the capability to display itself.
Perhaps it can do it standalone. e.g.
new Spreadsheet().display();
Perhaps it can do it in conjunction with the View e.g. a double-dispatch mechanism
new Spreadsheet().display(view);
In either case, the Spreadsheet/Letter/Email would all implement this view()
method and be responsible for the display. Your objects should talk in some view-agnostic language. e.g. your document says "display this in bold". Your view can then interpret it according to its type. Should your object know about the view ? Perhaps it needs to know capabilities that that view has, but it should be able to talk in this agnostic fashion without knowing the view details.
I am not sure, but you can try add a factory class that based on functions overriding, and assumed to return a view depending on Document type. For example:
class ViewFactory {
public View getView(Letter doc) {
return new LetterView();
}
public View getView(Email doc) {
return new EmailView();
}
}
Maybe you could have something like getView()
in the Document
, overriding it in each implementation?
I have seen this "pattern" many times in my work, and have seen many approaches to solve it. To the point, I would suggest
Create a new service
IViewSelector
Implement
IViewSelector
, either by hardcoding mappings or by configuration, and throwingNotSupportedException
whenever an invalid request is made.
This performs the mapping you require while facilitating Separation of Concern [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);
}
As an example of usage, consider the following
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;
}
}
The Visitor pattern might work here:
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 is one of the more controversial patterns, although there are a number of variants that try to overcome the original pattern's limitations.
It would be easier to implement if the accept(...) method could be implemented in Document, but the pattern relies on the static type of the "this" parameter, so I don't think this is possible in Java - you have to repeat yourself in this case because the static type of "this" depends on the class holding the implementation.
If the number of document types is relatively small and unlikely to grow, and the number of view types is more likely to grow, then this would work. Otherwise I would look for an approach that uses a third class to coordinate the display and try to keep View and Document independent. This second approach might look like 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) { ... }
}
The purpose of the ViewManager is to associate document instances (or document types if only one document of a given type can be open) with view instances (or view types if only one view of a given type can be open). If a document can have multiple associated views then the implementation of ViewManager would look like this instead:
class ViewManager {
public void display(Document document) {
List<View> views = getAssociatedViews(document);
for (View view : views) {
view.display();
}
}
protected List<View> getAssociatedViews(Document document) { ... }
}
The view-document association logic depends on your application. It can be as simple or as complicated as it needs to be. The association logic is encapsulated in the ViewManager so it should be relatively easy to change. I like the points that Drew Wills made in his answer regarding dependency injection and configuration.
First off, Drew Wills' response is absolutely great -- I'm new here and I don't have the reputation to vote on it yet, or else I would.
Unfortunately, and this may be my own lack of experience, I don't see any way that you're going to avoid compromising some separation of concern. Something is going to have to know what kind of View to create for a given Document -- there's just no way around that.
As Drew pointed out in point #3, you could go with some kind of external configuration that would instruct your system on which View class to use for which document type. Drew's point #4 is also a decent way to go, because even though it breaks the Open/Closed principle (I believe that's the one I'm thinking of), if you're only going to have a handful of Document sub types, it probably isn't really worth fussing with.
For a variation on that latter point, if you want to avoid using type checks, you could implement a factory class/method that relies on a Map of Document sub types to View instances:
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;
}
}
Of course, you'll still need to edit the initializeViews method whenever you need to add a new view to the map -- so it still violates OCP -- but at least your changes are going to be centralized to your Factory class instead of inside your controller.
(I'm sure there's lots that could be tweaked in that example -- validation, for one -- but it should be good enough to get a good idea of what I'm getting at.)
Hope this helps.
Just do it!
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"/>
Nothing else!
Extend your service to return the type of the document:
class MyService {
public static final int TYPE_EMAIL = 1;
public static final int TYPE_LETTER = 2;
public Document getDoc(){...}
public int getType(){...}
}
In a more object oriented approach, use a ViewFactory return a different view for e-mails and letters. Use view handlers with a ViewFactory and you can ask each of the handlers if it can handle the document:
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;
}
}
With this factory, your factory class doesn't need to change when you add new view types. The view types can each check if they can handle the document type given. If they can't they can return null. Otherwise, they can return the view you need. If no view can handle your document, null is returned.
The ViewHandlers can be really simple:
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
}
}
精彩评论