- 仅供参考:我生产有用的WinForms和控制台(C#),以及PowerShell应用程序,但我在OO设计方面很弱。我的专业编程生涯完全是过程式编程。通过OJT(在S.O.的大力帮助下),我学会了OOP,.NET,WinForms,C#和Word互操作的基础知识。就像他们说的,“我知道的足够危险了”。
我已经达到了我的“边编码边设计”的一个点,我卡住了。
背景及问题
我正在编写Word VSTO加载项。我使用自定义任务窗格(CTP)实现其UI。您可能知道,对于每个CTP,必须至少定义一个用户控制(UC)对象。为了便于讨论,我的加载项将包括5个用户控件对象
- P级(已实施)
- 从Microsoft UserControl类派生
- 承载各种与生产相关的UI选项控件
- T级(部分实施)
- 从Microsoft UserControl类派生
- 承载各种与测试相关的UI选项控件
- F1、F2和F3类(已实现)
- 目前,每个都派生自类P
- 每一个都是一个数据输入窗体,承载与数据输入方案1、2或3相关的各种UI控件。
1.注记
- 用户可以显示P表单或T表单。
- 当显示P表单时,用户可以显示表单F1或F2或F3并与表单F1或F2或F3交互。
这就是我被困住的地方
我正在实现类T,我意识到,像类P一样,当用户显示表单T时,他们也需要显示表单F1或F2或F3并与之交互。无论使用的是表单P还是表单T,表单F1、F2和F3上的控件背后的业务逻辑都是相同的。
我的直觉是从P和T派生F1、F2和F3,但C#不支持多重继承
我已经读了足够多的书,知道(但不是完全理解)一个接口可能会解决我的问题,但总的来说,它似乎有点笨拙。
我已经看过了OO设计模式,抽象工厂模式看起来很有希望,但在我把自己编码进兔子洞之前,我正在寻找第二种意见,因为:… employment of this pattern … may result in unnecessary complexity and extra work in the initial writing of code. Additionally, higher levels of separation and abstraction can result in systems that are more difficult to debug and maintain.
更新问题(7/28/2023):
你会使用什么模式或实现方法来让F1、F2和F3从P和T继承?在设计时还是在运行时?
这里有一个额外的问题:什么是情景(条件、动机?),这将迫使程序员实现一个C#接口?
2条答案
按热度按时间4ktjp1zp1#
无论使用的是表单P还是表单T,表单F1、F2和F3上的控件背后的业务逻辑都是相同的
我相信你可以将decouple a business logic注入到一个单独的类中,并将其注入到你的表单中,比如
F1
,F2
和F3
。在解耦业务逻辑之后,可以使用MVP pattern in Winforms。您可以通过创建抽象及其实现来解耦和提取业务逻辑。
这是业务逻辑的抽象。简单地说,它是一种Dependency inversion Principle:
字符串
这是抽象的具体实现:
型
然后是inject your dependencies like this:
型
w41d8nur2#
避免多重继承情况的一般解决方案称为组合。
由于您是所有参与类型的作者,因此您甚至可以考虑将您的类型重构为相关类型
P
和T
,并将其合并到一个继承树中(不推荐)。我们一般可以说,一个类型必须从多个超类继承是不自然的。虽然由于编译器的问题(通常要求编译器具有确定性),多重继承主要不受支持,但就语义而言,多重继承显然是错误的。
例如,
TypeX
应该继承自Type1
和TypeA
:如果TypeX
是Type1
并且TypeX
也是TypeA
,则Type1
和TypeA
都应该已经是同一继承树的子。通常情况下,他们不是,因为他们没有任何共同点。从语义上讲,从
Car
和Submarine
继承来创建Spaceship
是错误的,因为您需要每一个的特定功能。即使允许多重继承,这在语义上也是错误的。继承并不总是重用功能的解决方案。
继承始终是一种技术,主要是为了将 specialization 添加到通用超类型,而不是重用功能。
虽然
Car
是最一般化的类型,但RaceCar
表示一个专门化。因此,让RaceCar
从Car
派生在语义上是正确的,因为RaceCar
实际上是Car
。但是,当您需要
Car
的特定功能(例如,驾驶舱,以及Submarine
的功能,例如空调系统吗==>成分。虽然每个初学者都相信继承是OO编程的核心和必须使用的特性,但是组合是更自然和直观的技术。
事实上,自然界中的大多数物体都是由其他物体组成的。
例如,一个热气球由一个吊篮(吊篮)、一些索具和装空气的袋子以及一个燃烧丙烷气体的热源组成。空气本身是由分子组成的。一个分子由两个或多个原子组成。每个原子都是由原子核组成的,原子核由质子和中子组成,周围是电子云。可以对每个对象执行此操作。对象是由其他对象组成的,这些对象本身就是组合。
为了组成我们的
Spaceship
,我们必须创建可以组成它的对象。这意味着,我们必须从Car
中提取驾驶舱,并创建一个Cockpit
类。现在,Car
和Spaceship
都可以使用Cockpit
。同样适用于空调系统:我们将该功能提取到专用的AirConditioningSystem
类中,并使Submrine
和Spaceship
都能够使用它。Spaceship
现在可以使用这两种类型中最好的一种,而不必强制继承它们(即创建非常不自然关系)。很难判断这两种解决方案(将类型合并到一个公共继承树中还是组合)中哪一种更适合您的情况,因为您没有分享足够的细节。
作文表达了一种 * 有 * 的关系:
House
有一个Basement
,还有一个Stairway
.继承表示一个 * 是一个 * 关系:Warehouse
是一个House
。你必须决定哪种技术最能表达你的类型之间的关系。通常,您将始终混合使用组合和继承。子类继承了超类的功能,同时通过使用其他对象添加了更多的功能。
解决方案1:组成
您的意图是让
F1
、F2
和F3
延伸P
和T
。这意味着F1
、F2
和F3
使用P
ANDT
的功能。将太空船的例子应用到您的问题中,将引导我们将可重用的功能提取到一个新的类或甚至类中。
在您的方案中,我们将至少使用两个类:
Q
,其中包含我们要重用的P
的提取功能;以及U
,其中包含T
的提取功能。最终的类设计如下所示:
数量cs
从
P
中提取的可重用功能。字符串
美国cs
从
T
中提取的可重用功能。型
邮政编码
衍生自Microsoft
UserControl
类别,并由Q
所组成。型
*T.cs
衍生自Microsoft
UserControl
类别,并由U
所组成。型
F1.cs文件
F1
、F2
和F3
的示例代码。型
解决方案2:继承权
虽然
P
和T
看起来并不相关,但是我们仍然可以通过将P
和T
合并到一个公共的类型层次结构中来使用继承。这就消除了多重继承的情况。在不了解更多关于类的细节的情况下,我很确定继承在语义和上下文上都是错误的选择。
换句话说,
F1
不是P
,F1
也不是T
。P
不是T
。因此,F1
将继承它不应该具有的功能。如果继承反映了正确的关系,那么您已经直观地使用了它。
但是因为有些情况下这种转换是有意义的,我将继续表现得像你的场景一样,只是为了展示如何将多继承转换为单继承(在你可以完全访问源代码以重构类型层次结构的情况下)。
这个例子也说明了为什么组合物是更好的解决方案。如果你比较这两种解决方案,你会立刻发现,继承的唯一目的是为不相关的类型提供功能是错误的。
这个例子,特别是类设计的丑陋,表明继承不仅仅是简单地使功能可用。
同样,继承是关于类型层次结构的专门化,从继承树的根到叶子,从一般化的类型到最专门化的类型。
语义有助于确保我们没有违反专门化。Liskov替换原则(LSP)是另一个很好的原则,它有助于确定何时必须避免继承。
邮政编码
从.NET
UserControl
派生。型
*T.cs
现在,
T
通过扩展P
从.NETUserControl
类派生而来。这就是设计开始发臭的地方。
型
F1.cs文件
通过让
F1
、F2
和F3
从T
派生,它继承了P
和T
的所需功能,但也继承了UserControl
的功能。此外,F1
、F2
和F3
可能会从P
和T
继承许多原本不应该有的功能。在这一点上,错误的语义已经变得明显。这就是代码气味(或设计气味)变得令人讨厌的地方。
型
要回答有关接口的问题,请执行以下操作:
“实现一个接口”意味着提供或编写一个实现合约的类,即接口定义的公共成员。如果这真的是你的意思,那么你必须知道接口就像抽象类:你不能创建抽象类或接口的示例。为了创建一个示例,你必须实现一个接口或者扩展抽象类。
如果你想知道为什么要使用接口,我建议你查阅SOLID原则。字母“I”(接口隔离原则)和“D”(依赖反转原则)为您提供了如何使用接口的良好而重要的想法。接口是继承的替代方法,可以像基类型一样使用,以启用多态性。接口类似于抽象类,除了它们不允许虚拟和受保护的成员,因为它们不允许定义实现(除了从C# 8.0开始的默认成员),构造函数或从类派生。