我怀疑很长一段时间......希望任何人都可以加入我的行列。
假设我的模型中有3个类。
abstract class Document {}
class Letter extends Document {}
class Email extends Document {}
一个服务类,其方法返回一个Document(一个Letter或Email)。
class MyService {
public Document getDoc(){...}
}
因此,在我的控制器中,我想显示MyService返回的文档,我希望它使用电子邮件的视图和Letter的其他视图显示。 控制器如何知道调用哪个文档视图? letterView或emailView?。
我经常在控制器上创建一个if语句来检查服务层接收到的Document的类型...但是我不认为从OOP的角度来看这是最好的方法,如果我实现了一些布尔值方法Document.isLetter(),Document.isEmail()解决方案本质上是相同的。
另一件事是以某种方式将视图选择委托给Document。类似的东西:
class MyController {
public View handleSomething() {
Document document = myService.getDocument();
return document.getView();
}
}
但是,omg,为什么我的模型对象必须知道关于视图的任何信息?
任何强硬都表示赞赏:)
答案 0 :(得分:11)
这是一个很好的问题。这里有不止一种似是而非的方法;你必须平衡权衡并做出适合你情况的选择。
(1)有些人会争辩说,Document接口应该为实例提供一个呈现自己的方法。从OO的角度来看,这很有吸引力,但是根据您的视图技术,加载具体的Document类 - 可能是简单的域模型类 - 具有JSP,Swing组件等知识,这可能是不切实际或非常难看的。
(2)有些人会建议在String getViewName()
上放置一个Document
方法,例如,返回可以正确呈现该文档类型的JSP文件的路径。这避免了#1在一个层面上的丑陋(库依赖/“繁重”代码),但在概念上也会产生同样的问题:你的域模型知道它是由JSP呈现的,它知道你的webapp的结构。
(3)尽管有这些观点,但如果您的Controller类不知道Universe中存在哪些类型的文档以及Document
的每个实例属于哪种类型,那么它会更好。考虑在某种基于文本的文件中设置某种视图映射:.properties或.xml。你用春天吗? Spring DI可以帮助您快速指定具体Document类的Map和呈现它们的JSP /视图组件,然后将其传递给Controller类的setter /构造函数。这种方法允许:(1)您的Controller代码保持对Document
类型的不可知性,以及(2)您的域模型保持对视图技术的简单和不可知。它以增量配置为代价:.properties或.xml。
我会去#3或者 - 如果我的预算(及时)处理这个问题很小 - 我(4)只是硬编码Document
类型的一些基本知识我的控制器(正如你现在所说的那样),以便在下次由于不太理想的OO特性而被迫更新我的控制器时转向#3。事实上,#1-3每个都需要更长时间,并且比#4更复杂,即使它们“更正确”。坚持#4也是YAGNI Principal的一个点头:没有确定你会遇到#4的负面影响,是否有必要支付预先避免它们的费用?
答案 1 :(得分:2)
你的控制器不应该知道。它应该要求Document
显示自己,Document
可以执行此操作,或提供足够的信息让View以多态方式处理此事。
想象一下,如果您在稍后阶段添加新的Document
类型(例如,Spreadsheet
)。您真的只想添加Spreadsheet
对象(继承自Document
)并使一切正常。因此Spreadsheet
需要提供显示自身的能力。
也许它可以独立完成。 e.g。
new Spreadsheet().display();
也许它可以在连接中与View一起使用,例如双重调度机制
new Spreadsheet().display(view);
在任何一种情况下,电子表格/信件/电子邮件都将实施此view()
方法并负责显示。您的对象应该使用一些与视图无关的语言。例如您的文档显示“以粗体显示”。您的视图可以根据其类型进行解释。您的对象应该知道视图吗?也许它需要知道该视图具有的功能,但它应该能够以这种不可知的方式进行交谈而不了解视图细节。
答案 2 :(得分:2)
我不确定,但您可以尝试添加基于函数覆盖的工厂类,并假定根据文档类型返回视图。例如:
class ViewFactory {
public View getView(Letter doc) {
return new LetterView();
}
public View getView(Email doc) {
return new EmailView();
}
}
答案 3 :(得分:1)
也许你可以在getView()
中使用Document
,在每个实现中覆盖它?
答案 4 :(得分:1)
我在工作中多次看到过这种“模式”,并且已经看到很多方法来解决它。至此,我建议
创建新服务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;
}
}
答案 5 :(得分:1)
访客模式可能适用于此:
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);
}
访客是更具争议性的模式之一,尽管有许多变种试图克服原始模式的局限性。
如果可以在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中,因此它应该相对容易更改。我喜欢Drew Wills在关于依赖注入和配置的答案中提出的观点。
答案 6 :(得分:1)
首先,Drew Wills的反应非常好 - 我是新来的,我还没有在其上投票的声誉,否则我会。
不幸的是,这可能是我自己缺乏经验,我认为你不会有任何方式避免损害某些关注点的分离。有些东西必须要知道为给定的文档创建什么样的视图 - 没有办法解决这个问题。
正如Drew在第3点指出的那样,您可以使用某种外部配置来指示您的系统View类在哪种文档类型中使用。 Drew的第4点也是一个不错的选择,因为即使它违反了开放/封闭原则(我相信这是我正在考虑的原则),如果你只想要一些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类而不是在你的控制器内。
(我确信在这个例子中可以调整很多东西 - 验证,但是它应该足以让我很好地了解我的目标。)
希望这有帮助。
答案 7 :(得分:1)
做吧!
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"/>
别的!
答案 8 :(得分:0)
扩展您的服务以返回文档类型:
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
}
}