实现 MVP 的最佳方式
Best way to implement MVP
在MVP模式中,视图和模型之间没有依赖关系。一切由主持人完成。
在我们的 GWT 项目中,我们实现了所有 MVP 类,如 GWT .page 所示。要访问视图,我们的演示者使用如下界面:
public interface Display {
HasClickHandlers getAddButton();
HasClickHandlers getDeleteButton();
...
}
因此,演示者从视图中获取一个按钮,并将其点击处理程序应用于它。
但这对我来说感觉很不对劲。这违反了单一职责原则,以及信息隐藏原则。演示者不应该知道视图的任何内部结构(即使我们得到一个抽象的 HasClickHandlers)。
在我看来,我们最好使用这种模式:
public interface Display {
void setPresenter(Presenter p);
}
public interface Presenter {
void onAdd();
void onDelete();
}
因此,视图主动告诉演示者,发生了一些交互。但不是在哪个视图元素上。
我的团队伙伴争辩说,通过第一个解决方案,我们避免了循环依赖。那就对了。但无论如何,我更喜欢第二种方式,因为模块更好地分离并且可以独立维护。
优点/缺点是什么?
强烈同意 - 如果您继续阅读第二部分,您 link 的文章描述了双方。
我不构建我的 Views 公开小部件或其 HasSomethingHandlers
- 我发现通常构建 Presenter API 以公开那些可能的交互更好,原因如下:
单元测试 - 制作一个调用方法而不是触发事件的模拟视图更简单。这并不是说不可能(特别是如果您的 Presenter 可以在 JVM 上 运行 并且您可以为每个 Presenter 创建一个 Proxy 类型。
清理未使用的处理程序 - Presenter 通常很便宜并且每次都重新创建,但有时 View 很重并且被保存,重复使用。任何时候你重用一个视图,你必须确保所有那些外部添加的处理程序都被删除了。这可以通过一些 "lifecycle" 方法正确完成,例如 onStop() 或演示器上的某些方法,以便替换演示器的任何代码都可以将其关闭,但需要考虑在内。
多视图实现 - 如单元测试、不同的视图实现(移动与桌面、只读与读写等)可能有不同的方式导致相同的变化。现在,您可以使用多种方法公开 HasClickHandlers onAddClicked()
和 HasTouchHandlers onAddTapped()
,或者采用您描述的方法。 (你也可能最终将一些 UX 细节泄露给演示者,比如在最后一个字段中按下 Enter 键,而不是单击按钮 - 可以说属于视图,而不属于演示者)。
这种方法的一个缺点:视图需要更多的大脑,而 MVP 的部分目标是使视图尽可能瘦。
最后,虽然您 link 编辑的页面上并未实际比较和对比这种替代方法,但您 link 编辑的同一篇文章的第二页确实展示了您的方法 http://www.gwtproject.org/articles/mvp-architecture-2.html:
Now that we have the UI constructed, we need to hook up the associated UI events, namely Add/Delete button clicks, and interaction with the contact list FlexTable. This is where we’ll start to notice significant changes in the overall layout of our application design. Mainly due to the fact that we want to link methods within the view to UI interactions via the UiHandler annotation. The first major change is that we want our ContactsPresenter to implement a Presenter interface that allows our ContactsView to callback into the presenter when it receives a click, select or other event. The Presenter interface defines the following:
public interface Presenter<T> {
void onAddButtonClicked();
void onDeleteButtonClicked();
void onItemClicked(T clickedItem);
void onItemSelected(T selectedItem);
}
这是在描述 UiBinder 构建视图的方式的上下文中完成的,但它同样适用于没有 UiBinder 的情况。
因此,在讨论与组织您的团队如何构建应用程序相关的文章时,请务必考虑整套文章 - 这些文章都引用自 http://www.gwtproject.org/doc/latest/DevGuideMvpActivitiesAndPlaces.html,这也假设这非事件处理方法。在那篇文章中还有其他 "wrong" 做事的方式,比如手动依赖注入,而不是像 Gin 或 Dagger2 这样的自动化可靠工具。
这些文章并不是为了描述唯一正确的方法,而是为了理解可以使用的想法和模式。不要狂热地奉行这一点,盲目地应用模式,但一定要了解利弊 - 并作为一个团队工作,使用不一致的模式可以说比根本没有模式更糟糕!
编辑:我意识到我没有提到循环依赖问题!
这是一个或多或少的问题,如您所愿。如果您同时创建它们,并在构造函数中传递对另一个的引用,显然您会遇到问题 - GWT 无法代理它们(并且可以说这是一个坏主意)。但是,在未来,您可能会遇到这样一种情况,即视图的构建成本很高,并且可能 pooled/cached 而不是每次都重新创建,在这种情况下,需要将视图通知它现在可以使用的新演示者无论如何。
那就要求View接口无论如何都要支持一个void setPresenter(P)
方法,这样我们的圈子就破了。还要记住,这种方法将要求演示者确保清除视图中的数据,或者视图知道何时将新演示者设置为清除其自己的数据。
我个人的做法是让演示者拥有一个视图字段,在创建时注入(可能通过构造函数),并在演示者中有一个 start
方法。调用时,演示者控制视图,准备就绪后,将其附加到 dom 层次结构中它所属的位置。
是的,这需要在视图中使用一个额外的方法,但是如果您的视图有任何基础 class,这将是微不足道的,如果您最终要重用任何视图,则需要该方法无论如何。
在MVP模式中,视图和模型之间没有依赖关系。一切由主持人完成。
在我们的 GWT 项目中,我们实现了所有 MVP 类,如 GWT .page 所示。要访问视图,我们的演示者使用如下界面:
public interface Display {
HasClickHandlers getAddButton();
HasClickHandlers getDeleteButton();
...
}
因此,演示者从视图中获取一个按钮,并将其点击处理程序应用于它。
但这对我来说感觉很不对劲。这违反了单一职责原则,以及信息隐藏原则。演示者不应该知道视图的任何内部结构(即使我们得到一个抽象的 HasClickHandlers)。
在我看来,我们最好使用这种模式:
public interface Display {
void setPresenter(Presenter p);
}
public interface Presenter {
void onAdd();
void onDelete();
}
因此,视图主动告诉演示者,发生了一些交互。但不是在哪个视图元素上。 我的团队伙伴争辩说,通过第一个解决方案,我们避免了循环依赖。那就对了。但无论如何,我更喜欢第二种方式,因为模块更好地分离并且可以独立维护。
优点/缺点是什么?
强烈同意 - 如果您继续阅读第二部分,您 link 的文章描述了双方。
我不构建我的 Views 公开小部件或其 HasSomethingHandlers
- 我发现通常构建 Presenter API 以公开那些可能的交互更好,原因如下:
单元测试 - 制作一个调用方法而不是触发事件的模拟视图更简单。这并不是说不可能(特别是如果您的 Presenter 可以在 JVM 上 运行 并且您可以为每个 Presenter 创建一个 Proxy 类型。
清理未使用的处理程序 - Presenter 通常很便宜并且每次都重新创建,但有时 View 很重并且被保存,重复使用。任何时候你重用一个视图,你必须确保所有那些外部添加的处理程序都被删除了。这可以通过一些 "lifecycle" 方法正确完成,例如 onStop() 或演示器上的某些方法,以便替换演示器的任何代码都可以将其关闭,但需要考虑在内。
多视图实现 - 如单元测试、不同的视图实现(移动与桌面、只读与读写等)可能有不同的方式导致相同的变化。现在,您可以使用多种方法公开
HasClickHandlers onAddClicked()
和HasTouchHandlers onAddTapped()
,或者采用您描述的方法。 (你也可能最终将一些 UX 细节泄露给演示者,比如在最后一个字段中按下 Enter 键,而不是单击按钮 - 可以说属于视图,而不属于演示者)。
这种方法的一个缺点:视图需要更多的大脑,而 MVP 的部分目标是使视图尽可能瘦。
最后,虽然您 link 编辑的页面上并未实际比较和对比这种替代方法,但您 link 编辑的同一篇文章的第二页确实展示了您的方法 http://www.gwtproject.org/articles/mvp-architecture-2.html:
Now that we have the UI constructed, we need to hook up the associated UI events, namely Add/Delete button clicks, and interaction with the contact list FlexTable. This is where we’ll start to notice significant changes in the overall layout of our application design. Mainly due to the fact that we want to link methods within the view to UI interactions via the UiHandler annotation. The first major change is that we want our ContactsPresenter to implement a Presenter interface that allows our ContactsView to callback into the presenter when it receives a click, select or other event. The Presenter interface defines the following:
public interface Presenter<T> { void onAddButtonClicked(); void onDeleteButtonClicked(); void onItemClicked(T clickedItem); void onItemSelected(T selectedItem); }
这是在描述 UiBinder 构建视图的方式的上下文中完成的,但它同样适用于没有 UiBinder 的情况。
因此,在讨论与组织您的团队如何构建应用程序相关的文章时,请务必考虑整套文章 - 这些文章都引用自 http://www.gwtproject.org/doc/latest/DevGuideMvpActivitiesAndPlaces.html,这也假设这非事件处理方法。在那篇文章中还有其他 "wrong" 做事的方式,比如手动依赖注入,而不是像 Gin 或 Dagger2 这样的自动化可靠工具。
这些文章并不是为了描述唯一正确的方法,而是为了理解可以使用的想法和模式。不要狂热地奉行这一点,盲目地应用模式,但一定要了解利弊 - 并作为一个团队工作,使用不一致的模式可以说比根本没有模式更糟糕!
编辑:我意识到我没有提到循环依赖问题!
这是一个或多或少的问题,如您所愿。如果您同时创建它们,并在构造函数中传递对另一个的引用,显然您会遇到问题 - GWT 无法代理它们(并且可以说这是一个坏主意)。但是,在未来,您可能会遇到这样一种情况,即视图的构建成本很高,并且可能 pooled/cached 而不是每次都重新创建,在这种情况下,需要将视图通知它现在可以使用的新演示者无论如何。
那就要求View接口无论如何都要支持一个void setPresenter(P)
方法,这样我们的圈子就破了。还要记住,这种方法将要求演示者确保清除视图中的数据,或者视图知道何时将新演示者设置为清除其自己的数据。
我个人的做法是让演示者拥有一个视图字段,在创建时注入(可能通过构造函数),并在演示者中有一个 start
方法。调用时,演示者控制视图,准备就绪后,将其附加到 dom 层次结构中它所属的位置。
是的,这需要在视图中使用一个额外的方法,但是如果您的视图有任何基础 class,这将是微不足道的,如果您最终要重用任何视图,则需要该方法无论如何。