使用C#中的抽象类作为定义


21

作为一名C ++开发人员,我已经习惯了C ++头文件,并发现在代码中包含某种强制性的“文档”很有用。通常,在这种情况下,我不得不读一些C#代码时会很糟糕:我没有正在使用的类的思维导图。

假设作为软件工程师,我正在设计程序的框架。将每个类都定义为抽象的未实现类是否太疯狂了,就像我们对C ++标头所做的那样,让开发人员实现它呢?

我猜可能有人可能会因为某些原因而认为这是一个糟糕的解决方案,但我不确定为什么。这样的解决方案必须考虑什么?


13
C#是与C ++完全不同的编程语言。某些语法看起来相似,但是您需要了解C#才能很好地完成其中的工作。而且,您应该对依赖头文件的坏习惯做一些事情。我使用C ++已经有几十年了,我从未读过头文件,只写过头文件。
弯曲

17
C#和Java最好的事情之一就是没有头文件!只需使用一个好的IDE。
Erik Eidt

9
C ++头文件中的声明最终不会成为生成的二进制文件的一部分。它们在那儿供编译器和链接器使用。这些C#抽象类将成为生成的代码的一部分,毫无益处。
Mark Benningfield '18

16
C#中的等效构造是接口,而不是抽象类。
罗伯特·哈维

6
@DaniBarcaCasafont“我将标头视为查看类方法和属性是什么,期望使用什么参数类型以及返回什么的一种快速可靠的方法”。使用Visual Studio时,我的替代选择是Ctrl-MO快捷键。
wha7ever-恢复莫妮卡

Answers:


44

在C ++中完成此操作的原因与使编译器更快,更易于实现有关。这不是使编程更容易的设计。

头文件的目的是使编译器能够快速进行第一遍,以了解所有预期的函数名称并为其分配内存位置,以便在cp文件中调用它们时都可以引用它们,即使定义它们的类具有尚未解析。

不建议尝试在现代开发环境中复制旧硬件限制的结果!

为每个类定义接口或抽象类将降低您的工作效率;那时候你还能做什么?另外,其他开发人员将不会遵循此约定。

实际上,其他开发人员可能会删除您的抽象类。如果我在代码中找到一个同时满足这两个条件的接口,则将其删除并从代码中重构出来:1. Does not conform to the interface segregation principle 2. Only has one class that inherits from it

另一件事是,Visual Studio附带了一些工具,这些工具可以自动完成您要实现的目标:

  1. Class View
  2. Object Browser
  3. 在其中,Solution Explorer您可以单击三角形以展开类以查看其功能,参数和返回类型。
  4. 文件选项卡下面有三个小下拉菜单,最右边的一个列出了当前类的所有成员。

在花时间在C#中复制C ++头文件之前,请尝试一下上述方法。


此外,出于技术原因,请勿执行此操作……这会使最终的二进制文件大于所需的大小。我将重复马克·本宁菲尔德的评论:

C ++头文件中的声明最终不会成为生成的二进制文件的一部分。它们在那儿供编译器和链接器使用。这些C#抽象类将成为生成的代码的一部分,毫无益处。


同样,正如罗伯特·哈维(Robert Harvey)提到的那样,从技术上讲,C#中最接近标头的等同物是接口,而不是抽象类。


2
您还将最终获得许多不必要的虚拟调度调用(如果您的代码对性能敏感,则不好)。
卢卡斯Trzesniewski

5
这是所指的接口隔离原则
NH。

2
也许还可以简单地折叠整个类(右键单击->概述->折叠为定义)以鸟瞰它。
jpmc26

“到那个时候您还能做什么?”->嗯,复制粘贴,然后做很少的后期工作?大约要花我3秒钟。当然,我本来可以用这段时间取出滤嘴以备卷烟。一点都不相关。[免责声明:我既喜欢惯用的C#,也喜欢惯用的C ++,以及其他一些语言]
phresnel

1
@phresnel:我想看到您尝试重复一个类的定义,并从3s的抽象类中继承原始类,并确认没有错误,对于任何足够大以至于不能轻易俯瞰的类它(因为这是OP的建议用例:据称不会使原本复杂的类复杂化)。几乎从本质上讲,原始类的复杂性质意味着您不能仅凭心地编写抽象类定义(因为这意味着您已经心地了解它),然后考虑整个代码库所需的时间。
更加平坦

14

首先,要了解一个纯粹的抽象类实际上只是一个不能进行多重继承的接口。

写类,提取接口,是一种脑死亡的活动。如此之多,以至于我们对其进行了重构。真可惜。遵循这种“每个类都有接口”的模式,不仅会造成混乱,而且会完全遗漏问题。

接口不应被视为仅仅是类可以做什么的正式重述。接口应被视为使用详细说明其需求的客户端代码所强加的契约。

我编写一个目前只有一个类实现该接口的接口完全没有问题。我实际上并不在乎是否还没有任何类实现它。因为我正在考虑使用代码的需求。该接口表示使用代码的要求。只要满足这些期望,以后发生的任何事情都可以做自己喜欢的事情。

现在,我不必每次一个对象使用另一个对象时都执行此操作。越过边界时,我会这样做。当我不希望一个对象确切知道它正在与哪个对象通信时,我就这样做。这是多态性起作用的唯一方法。当我期望客户代码正在谈论的对象可能会更改时,我会这样做。当我使用的是String类时,我当然不会这样做。String类很好并且很稳定,我觉得没有必要对它进行更改。

当您决定直接与具体的实现进行交互而不是通过抽象进行交互时,您将预测该实现足够稳定,以至于不会更改。

正确的方式是我改善依赖反转原理的方法。您不应盲目地将其应用于所有内容。当您添加抽象时,您实际上是在说您不相信实现类在整个项目生命周期中保持稳定的选择。

所有这些都假定您正在尝试遵循开放式封闭原则。仅当与直接更改已建立代码相关的成本很高时,此原则才重要。人们不同意去耦对象的重要性的主要原因之一是因为并非每个人在进行直接更改时都会承受相同的成本。如果重新测试,重新编译和重新分配整个代码库对您来说微不足道,那么直接修改解决变更需求可能是此问题的非常有吸引力的简化方法。

这个问题根本没有脑残的答案。接口或抽象类不是您应该添加到每个类中的东西,并且您不能仅仅计算实现类的数量并决定不需要它。这与应对变化有关。这意味着您正在展望未来。如果您弄错了,请不要感到惊讶。尽可能保持简单,而不会陷入困境。

因此,请不要仅仅为了帮助我们阅读代码而编写抽象。我们有用于此的工具。使用抽象来解耦需要解耦的内容。


5

是的,这将是可怕的,因为(1)引入了不必要的代码(2)会使读者感到困惑。

如果要使用C#编程,则应该习惯于阅读C#。无论如何,其他人编写的代码将不会遵循这种模式。


1

单元测试

我强烈建议您编写单元测试,而不要使接口或抽象类的代码混乱(除非出于其他原因保证)。

编写良好的单元测试不仅描述类的接口(就像头文件,抽象类或接口一样),而且还举例说明所需的功能。

示例:您可能在像这样编写头文件myclass.h的地方:

class MyClass
{
public:
  void foo();
};

相反,在c#中编写如下测试:

[TestClass]
public class MyClassTests
{
    [TestMethod]
    public void MyClass_should_have_method_Foo()
    {
        //Arrange
        var myClass = new MyClass();
        //Act
        myClass.Foo();
        //Verify
        Assert.Inconclusive("TODO: Write a more detailed test");
    }
}

这个非常简单的测试传达的信息与头文件相同。(我们应该有一个名为“ MyClass”的类,该类具有无参数的函数“ Foo”。)尽管头文件更加紧凑,但是测试包含的信息却更多。

一个警告:一个让高级软件工程师为其他开发人员提供(失败)测试以使用TDD之类的方法剧烈解决冲突的过程,但是对于您而言,这将是一个巨大的进步。


您如何进行单元测试(隔离测试)=需要模拟,没有接口?框架支持实际实现中的模拟,我所见过的大多数人都使用接口将实现与模拟实现交换出去。
布兰

我认为对于OP:s而言,并不是必须要使用模拟程序。请记住,这不是将单元测试作为TDD的工具,而是将单元测试作为头文件的替代。(它们当然可能会演变成带有模拟等的“常规”单元测试)
Guran

好吧,如果我仅考虑OP方面,我会更好地理解。阅读您的答案作为更通用的应用答案。谢谢澄清!
布兰

@Bulan需要进行广泛的模拟通常表明设计不好
TheCatWhisperer

@TheCatWhisperer是的,我很清楚这一点,所以没有论点,也看不到我在任何地方都这样说过::DI在谈论测试时,我使用的所有Mock框架都使用接口进行交换排除实际的实现,如果没有接口,还有其他方法可以模拟。
布兰'18年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.