作为一名C ++开发人员,我已经习惯了C ++头文件,并发现在代码中包含某种强制性的“文档”很有用。通常,在这种情况下,我不得不读一些C#代码时会很糟糕:我没有正在使用的类的思维导图。
假设作为软件工程师,我正在设计程序的框架。将每个类都定义为抽象的未实现类是否太疯狂了,就像我们对C ++标头所做的那样,让开发人员实现它呢?
我猜可能有人可能会因为某些原因而认为这是一个糟糕的解决方案,但我不确定为什么。这样的解决方案必须考虑什么?
作为一名C ++开发人员,我已经习惯了C ++头文件,并发现在代码中包含某种强制性的“文档”很有用。通常,在这种情况下,我不得不读一些C#代码时会很糟糕:我没有正在使用的类的思维导图。
假设作为软件工程师,我正在设计程序的框架。将每个类都定义为抽象的未实现类是否太疯狂了,就像我们对C ++标头所做的那样,让开发人员实现它呢?
我猜可能有人可能会因为某些原因而认为这是一个糟糕的解决方案,但我不确定为什么。这样的解决方案必须考虑什么?
Answers:
在C ++中完成此操作的原因与使编译器更快,更易于实现有关。这不是使编程更容易的设计。
头文件的目的是使编译器能够快速进行第一遍,以了解所有预期的函数名称并为其分配内存位置,以便在cp文件中调用它们时都可以引用它们,即使定义它们的类具有尚未解析。
为每个类定义接口或抽象类将降低您的工作效率;那时候你还能做什么?另外,其他开发人员将不会遵循此约定。
实际上,其他开发人员可能会删除您的抽象类。如果我在代码中找到一个同时满足这两个条件的接口,则将其删除并从代码中重构出来:1. Does not conform to the interface segregation principle
2. Only has one class that inherits from it
另一件事是,Visual Studio附带了一些工具,这些工具可以自动完成您要实现的目标:
Class View
Object Browser
Solution Explorer
您可以单击三角形以展开类以查看其功能,参数和返回类型。在花时间在C#中复制C ++头文件之前,请尝试一下上述方法。
此外,出于技术原因,请勿执行此操作……这会使最终的二进制文件大于所需的大小。我将重复马克·本宁菲尔德的评论:
C ++头文件中的声明最终不会成为生成的二进制文件的一部分。它们在那儿供编译器和链接器使用。这些C#抽象类将成为生成的代码的一部分,毫无益处。
同样,正如罗伯特·哈维(Robert Harvey)提到的那样,从技术上讲,C#中最接近标头的等同物是接口,而不是抽象类。
首先,要了解一个纯粹的抽象类实际上只是一个不能进行多重继承的接口。
写类,提取接口,是一种脑死亡的活动。如此之多,以至于我们对其进行了重构。真可惜。遵循这种“每个类都有接口”的模式,不仅会造成混乱,而且会完全遗漏问题。
接口不应被视为仅仅是类可以做什么的正式重述。接口应被视为使用详细说明其需求的客户端代码所强加的契约。
我编写一个目前只有一个类实现该接口的接口完全没有问题。我实际上并不在乎是否还没有任何类实现它。因为我正在考虑使用代码的需求。该接口表示使用代码的要求。只要满足这些期望,以后发生的任何事情都可以做自己喜欢的事情。
现在,我不必每次一个对象使用另一个对象时都执行此操作。越过边界时,我会这样做。当我不希望一个对象确切知道它正在与哪个对象通信时,我就这样做。这是多态性起作用的唯一方法。当我期望客户代码正在谈论的对象可能会更改时,我会这样做。当我使用的是String类时,我当然不会这样做。String类很好并且很稳定,我觉得没有必要对它进行更改。
当您决定直接与具体的实现进行交互而不是通过抽象进行交互时,您将预测该实现足够稳定,以至于不会更改。
正确的方式是我改善依赖反转原理的方法。您不应盲目地将其应用于所有内容。当您添加抽象时,您实际上是在说您不相信实现类在整个项目生命周期中保持稳定的选择。
所有这些都假定您正在尝试遵循开放式封闭原则。仅当与直接更改已建立代码相关的成本很高时,此原则才重要。人们不同意去耦对象的重要性的主要原因之一是因为并非每个人在进行直接更改时都会承受相同的成本。如果重新测试,重新编译和重新分配整个代码库对您来说微不足道,那么直接修改解决变更需求可能是此问题的非常有吸引力的简化方法。
这个问题根本没有脑残的答案。接口或抽象类不是您应该添加到每个类中的东西,并且您不能仅仅计算实现类的数量并决定不需要它。这与应对变化有关。这意味着您正在展望未来。如果您弄错了,请不要感到惊讶。尽可能保持简单,而不会陷入困境。
因此,请不要仅仅为了帮助我们阅读代码而编写抽象。我们有用于此的工具。使用抽象来解耦需要解耦的内容。
我强烈建议您编写单元测试,而不要使接口或抽象类的代码混乱(除非出于其他原因保证)。
编写良好的单元测试不仅描述类的接口(就像头文件,抽象类或接口一样),而且还举例说明所需的功能。
示例:您可能在像这样编写头文件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之类的方法剧烈解决冲突的过程,但是对于您而言,这将是一个巨大的进步。