MVP中的组合与继承

20
我正在使用 MVP 模式开发一款大型应用程序。在开发过程中,我遇到了一个问题:是应该使用组合还是继承?例如:我们假设有一个名为 Foo 的表单,其中包含字段 AB。在应用程序的另一部分中,我有一个名为 Bar 的表单,它具有相同的字段 AB,但还有一个额外的字段 C

目前,代码采用继承方法编写,其中视图表单 Bar 继承自表单 Foo。然后,Presenter 以稍微不同的方式处理模型数据。这非常简单,但我不确定是否符合“is A”的规则,因为即使表单不同,它们也处理共同的输入(A 和 B)。

不过,我一直在思考“组合优于继承”以及里氏替换原则,并认为我应该使用组合而不是继承。但是,由于我正在使用 MVP,这比预期的要复杂,因为我将需要一个针对具有字段 AB 的表单 Foo 的 Presenter,然后再有一个针对 Bar 的 Presenter,它具有字段C和对 Foo 的 Presenter 的引用,以便可以将字段 AB 注入其中。

问题在于,这会产生更多的代码,因为我必须在 Foo 的 Presenter 中添加某种 getter 和 setter,以便能够将数据传递给 Bar。这感觉有些像我在打破 MVP 以提供组合。

所以我的问题是:

对于我的情况,使用组合真的比继承更好吗?为什么?

使用组合是否会“打破”MVP?

5个回答

10

使用组合比继承更好吗?为什么?

是的。因为在更大的应用程序中,组合更可靠、更安全、更易于维护、更易于发现、更易于记录和更易于理解。这是我的意见。:)

使用组合会“破坏”MVP模式吗?

是的。它会破坏你现在所使用的简单MVP模式。组合允许您选择如何耦合代码,这对于更大的应用程序非常有好处。它确实需要更多的代码,因为您必须具体说明如何进行耦合。

对于一个简单的应用程序来说,变得复杂然后从简单的MVP继承过渡到更复杂的组合非常合理。这是一步去耦合,使得可以以新的方式重新耦合。

这类似于许多简单的Web应用程序正在过渡成为前/后端API驱动的应用程序。这本质上是将前端用户视图与后端存储模型分离。


你有兴趣看一下这个相关问题吗:https://dev59.com/HlUM5IYBdhLWcg3wF9TZ? - stoefln

8
当表单不同时,它们处理共同的输入(A和B)。
这意味着Foo presenter在概念上与Bar presenter不同,只是碰巧分享一些共同的输入,因此它们不应该通过继承相关联。将处理公共输入的代码提取到实用程序类中,在Foo presenter和Bar presenter中重复使用。
如果Foo的概念发生变化,它不会影响Bar(反之亦然:如果Bar的概念不能改变而不改变Foo的概念,则“is A”关系和继承确实可以使用)
当有疑问时,始终优先使用组合。

6

一种更加清晰的组合方式是使用以下类:

模型(Models):A、B、C、Foo、Bar
视图(Views):AView、BView、CView、FooView、BarView
演示者(Presentors):APresentor、BPresentor、CPresentor、FooPresentor、BarPresentor

其中FooView包含一个AView和一个BView, BarView包含一个AView、一个BView和一个CView, 演示者有类似的组成。

这种组合使A、B和C(以及它们的视图和演示者)模块化,这样你可以随意搭配,而组合类(Foo和Bar)处理集成问题。

这可以与继承一起使用: 如果Bar是Foo的一个特定案例,则Bar应该从Foo继承,BarPresentor可以从FooPresentor继承。然而,我认为应该更具体地考虑视图的继承方式,因为视图的行为可能适合或不适合继承。


可能你理解错了,因为Bar和Foo是GUI组件,也就是视图(view),而A、B、C是字段,也就是模型(model)。 ;) - Marcus
@Marcus 我的意思不是说错了,我的观点是如果A、B、C是模型类,并且有包含它们的模型类,每个模型类都应该有自己的Presenter。如果A、B、C是简单字段(例如字符串、整数等),那么组合就是一种过度设计(除非A、B、C有特殊限制(例如范围),需要特殊的UI控件来建模)。 - Danny Varod
但是A、B、C都是简单的字段。更重要的是,它们只是视图的字段,甚至不是数据模型的字段,因此这个模型需要由modelA和modelB创建。然而,为同一个模型创建视图和Presenter是没有意义的,通常你只需要有一个主Presenter包含视图并访问模型。否则,这真的是一种过度设计。;-) - Marcus
@Marcus 你怎么知道A、B和C是什么类型的字段?它们可能是任何对象——问题没有明确指定。 - Danny Varod

5
让我们从基础开始,关于类最重要的事情是:子类始终也是超类的完整实例。所以如果你在超类中定义了一个字段变量,当你创建子类实例时,该字段始终会被创建。你可以使用super.getVariable()在子类中获取那个变量,以重用字段(类变量、字段、标志,在面向对象编程中都是一样的)。但是你也可以直接从外部调用subclassInstance.getVariable()来获取相同的字段(不需要通过子类更改)。因此,通常情况下你根本不需要在子类中调用"super",因为你通常只想从外部获取/设置其超类(包括抽象类)的字段。由于应该始终将字段变量设置为私有,我建议永远不要调用"super"来访问任何字段变量(因为即使使用super,你也无法访问超类的私有方法/字段……实际上这是Java中最大的错误之一,因为它无法向其他类提供完全封装的类树……所以你通常需要调用像super.getField()这样的protected方法,这很烦人,但却是必须的)。
现在让我们谈谈数据模型:你有modelA和modelB。你可以从超类或Object中继承它们。但是如果你仅从Object中继承,你可以定义一个简单的(Java!)接口,并将此接口实现到modelA和modelB中。然后你只需通过接口处理这两个类(它们都是“接口对象”并且可以被通用地处理)。如果你有由modelA和modelB组成的modelC,你只需在modelC中使用这两个模型的实例(有时也称为“解引用”),无需编写更多代码。因此,你应该尽可能选择这些模型变得小而简单(“beans”)。组件化实现是数据结构的常规方式。
如果你有GUI或表单,情况就不同了。你可能有很多共同的代码,而且你不想将这些代码拆分成几十个不同的类,然后在控制器/Presenter类中将组件合并在一起。所以你可以定义一个抽象类,其中包含所有共享字段/标志以及若干方法来访问和更改它们。然后,你从formA和formB中调用这些公共方法并重用代码。
集合论对你有什么意义吗?你有两个圆,A和B,它们交叉部分是抽象类,子类formA和formB是集合差异。学习编写正确的程序……就是理解集合论。;-)
简单来说:表单Foo的大部分代码将在抽象超类Foobar中,这个类将能够处理AB。然后你从中继承FooBar,虽然C可能大多数情况下仍然是Foobar的子集,但你可以在Bar中添加处理C的功能,这就是集合差异。
最终,Bar在任何时候都不会是Foo,它们两个都只是Foobar。你有一些新的共享字段/标志吗?没问题,将它们的代码迁移到Foobar中,你可以在两个子类中使用它!
但是,如果有一天你需要一个略有不同于Foo的第三个组件FooToo怎么办?没问题,将FooBarFoo作为继承自FooBar的抽象类,然后创建FooFooToo作为子类。最终的结果将是一个类树,其中根通常是抽象类,叶子是真正的类,这种结构提供了代码的最大化重用(并且不更改类名,因此您无需更改已经使用类Foo的任何其他代码)。
你说你将在表单(或其Presenter)中实现setter/getter?那么你必须使用模型modelAmodelB(但不是模型C,因为C仅在Bar中使用而不是Foo)。这些模型用作包装器,在FooBar之间传输数据。这个数据流应该由Presenter控制,而不是由FooBar控制。
所以你的问题最终就是:Presenter是什么?实际上,Presenter是运行GUI组件和数据模型的代码。它是一个“框架”,一方面使用GUI组件,另一方面使用数据模型的getter/setter。它是两个层之间的中间件,GUI层和数据层,甚至在不同的GUI组件和不同的数据模型之间。
因此,通常只有两种方法可以做到这一点:没有Presenter/Controller或者有它。如果没有,你需要将很多Swing组件代码复制粘贴到Presenter类中。那又怎样?没错,当你使用Swing组件时,你将始终使用(M)VP模式,不可能做出不同的选择!
所以说,为了创建一个框架,你需要使用组件设计,因为你想要为使用你的框架的程序员提供最大的灵活性。但是一个高效的系统并不等同于一个框架,这是许多框架程序员的误区。因此,如果一个框架程序员告诉你“基于组件的实现就是一切”,那么他可能是错的。仅仅因为他正在为其框架编写组件,并不意味着你需要为你的Presenter做同样的事情!
所以当我们开始讨论GUI组件与GUI表示时,我们可以创建“尽可能多的Presenter组件”,就像你可以使用方法“include(...)”将简单的HTML网站制作成几十个PHP网站一样。但我向你保证,基于组件的设计并不总是提高代码的可维护性!如果我只需要一个类来完成某个任务,并且我能够清晰和易读地完成它,我宁愿选择一个类而不是十个类。一个Presenter=一个类,或者更具体地说:一个GUI框架/选项卡=一个类。
再次,如果你有两个相似的框架/选项卡,但它们不同,该怎么办呢?将共享的代码迁移到抽象类中,并创建2个子类,对吧?不,你首先需要考虑这些GUI共享了什么。它们有共享的标志吗?那么将标志移到一个抽象超类中。但是它们只是表现不同吗?那么你只需要在同一个类中实现两种不同的方法,并在需要时调用它们。这是最重要的。
用你的话说:GUI Pres1 共同使用 FooBarABC。如果GUI Pres2 只有不同的标志,那么它就在另一个类中。否则,你将设置一个标志 Pres1 和一个标志 Pres2 并在方法内检查这个标志。通过if(flag="Pres1"){} else if(flag="Pres2"){}这样做,你将获得最大的灵活性和代码可重用性。
不要将Java类视为不灵活、不可重用、不可更改的东西。如果你是一位优秀的程序员,当需要时,你会凭直觉改变程序的结构。你不需要考虑人为的概念,你只需要理解面向对象编程模式。
“组件”总是意味着“带有构造函数的东西”。但是,有时你会使用一个方法而不是组件来完成某些任务!因此,如果有人告诉你“基于组件的设计就是一切”,他实际上是在告诉你“基于构造函数的设计就是一切”。但是要创建构造函数,你需要有字段变量/标志!没有字段变量,仅仅为了制作而创建一个新类是完全没有意义的。

注意:“组件”指的不是一个方法。显然,在处理GUI类中,您将使用许多方法轻松处理事情,因此最终您只需调用几个方法即可。因此,请不要混淆组件和方法!我始终建议采用强大的方法导向设计,因为这关乎使用更少的代码行。因此,尽可能定义更多的方法......但也尽可能少定义类/组件。


好的,你实际上讨论并对比了我在悬赏中提到的选项。 - Sednus

4
当然,如果Foo没有继承Bar,你需要添加更多的代码,因为你有额外的getter和setter。但最大的好处是Foo不再依赖于Bar。这可能看起来是一个非常小的好处,但想象一下,如果你使用超过50个类的继承,会变得非常混乱,没有任何逻辑,而且如果你必须更改一个组件,它会变得非常复杂,因为这个组件被多个类继承。

出于维护的原因,请避免使用继承。正如你所说,“Bar不是Foo”,所以Bar不应该扩展Foo。根据我的经验,继承从来都不是一个好的解决方案,只应该用于一组类(例如使用组合模式)。

我的问题更多地涉及MVP模式中的继承。与一般用途中的继承不同,因为我认为这是有争议的。我想看到选项的详细对比。 - Sednus
MVP模式中的继承与其他上下文中的继承存在相同的问题。我将编写一段代码,其中每个类都依赖于其他类,您将无法重构系统。 - funkygono

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接