现在,我正在学习 OOP,主要是在 c# 中。我对创建一个无法实例化的类的主要原因很感兴趣。何时创建抽象类的正确示例是什么?我发现自己以继承方式使用抽象类过于热情。当类在系统中是抽象的并且类不应该是抽象的时,是否有一些规则?例如,我创建了在某种程度上相似的医生和病人类,所以我从抽象类 Person 派生了它们(因为它们都有名字和姓氏)。那是错的吗?抱歉,如果这个问题很愚蠢,我对此很陌生。
8 回答
到目前为止,有几件事没有人指出,所以我只想指出它们。
您只能从一个基类(可能是抽象的)继承,但您可以实现许多接口。所以从这个意义上说,继承抽象类比实现接口的关系更密切。
因此,如果您后来意识到您需要一个实现两个不同抽象类的类,那么您就大错特错了 :)
要回答你的问题“什么时候做一个抽象类”我会说永远不要,如果可能的话,避免它,从长远来看它永远不会得到回报,如果主类不适合作为普通类,它可能不是真的需要抽象,使用接口。如果您遇到复制代码的情况,它可能适合使用抽象类,但请始终先查看接口和行为模式(例如,策略模式解决了很多人们错误地使用继承来解决的问题,总是更喜欢组合而不是继承)。使用抽象类作为最后的解决方案,而不是作为设计。
为了更好地理解 OOP,我建议您看一下Design Patterns: Elements of Reusable Object-Oriented Software(一本书),它很好地概述了 OO 设计和 OO 组件的可重用性。OO 设计不仅仅是继承 :)
例如:您有一个场景,您需要从不同的来源提取数据,例如“Excel 文件、XML、任何数据库等”并保存在一个共同的目的地。它可以是任何数据库。所以在这种情况下你可以使用这样的抽象类。
abstract class AbstractImporter
{
public abstract List<SoldProduct> FetchData();
public bool UploadData(List<SoldProduct> productsSold)
{
// here you can do code to save data in common destination
}
}
public class ExcelImporter : AbstractImporter
{
public override List<SoldProduct> FetchData()
{
// here do code to get data from excel
}
}
public class XMLImporter : AbstractImporter
{
public override List<SoldProduct> FetchData()
{
// here do code to get data from XML
}
}
public class AccessDataImporter : AbstractImporter
{
public override List<SoldProduct> FetchData()
{
// here do code to get data from Access database
}
}
和调用可以是这样的
static class Program
{
static void Main()
{
List<SoldProduct> lstProducts;
ExcelImporter excelImp = new ExcelImporter();
lstProducts = excelImp.FetchData();
excelImp.UploadData(lstProducts);
XMLImporter xmlImp = new XMLImporter ();
lstProducts = xmlImp.FetchData();
xmlImp.UploadData(lstProducts);
AccessDataImporterxmlImp accImp = new AccessDataImporter();
lstProducts = accImp .FetchData();
accImp.UploadData(lstProducts);
}
}
因此,在上面的示例中,数据导入功能的实现在扩展(派生)类中分离,但数据上传功能对所有人来说都是通用的。
本质上,如果您从不想实例化 Person 类,那么您所做的一切都很好,但是我猜您可能希望在将来的某个时候实例化 Person 类,那么它不应该是抽象的。
尽管有一种说法是您编写代码是为了解决当前问题,而不是为了迎合可能永远不会出现的问题,因此如果您需要实例化 Person 类,请不要将其标记为抽象类。
抽象类是不完整的,必须在派生类中实现......一般来说,我更喜欢抽象基类而不是接口。
看看抽象类和接口之间的区别......
“抽象类和接口的区别在于,抽象类可以有方法的默认实现,所以如果你不在派生类中重写它们,就会使用抽象基类实现。接口不能有任何实现。 " 取自这篇 SO 帖子
如前所述,没有人会强迫您使用抽象类,它只是一种抽象某些功能的方法,这在许多类中很常见。
您的案例是使用抽象类的一个很好的例子,因为您在两种不同类型之间具有共同的属性。但它当然会限制您将 Person 本身用作一种类型。如果你想有这个限制基本上取决于你。
通常,除非您想阻止 Person 被实例化,否则我不会像您那样对 Model 类的类使用抽象类。
通常,如果我还定义了一个接口并且我需要为该接口编写不同的实现,但还希望拥有一个已经涵盖所有实现的一些通用功能的 BaseClass,我会使用抽象类。
从抽象类 'Person' 派生 'Doctor' 和 'Patient' 很好,但您可能应该让 Person 只是一个普通类。不过,这取决于使用“Person”的上下文。
例如,您可能有一个名为“GameObject”的抽象类。游戏中的每个对象(例如 Pistol、OneUp)都扩展了“GameObject”。但是你不能单独拥有一个“GameObject”,因为“GameObject”描述了一个类应该有什么,但没有详细说明它们是什么。
例如,GameObject 可能会这样说:“所有 GameObjects 必须看起来像什么东西”。手枪可能会在 GameObject 所说的基础上扩展,它会说“所有 Pistols 必须看起来像一个长枪管,一端有把手和扳机。”
这可能是一个非学术定义,但抽象类应该表示一个如此“抽象”的实体,以至于实例化它没有意义。
它通常用于创建必须由具体类扩展的“模板”。所以一个抽象类可以实现一些共同的特性,例如实现一个接口的一些方法,一个委托给具体类实现特定行为。
关键是该类的实例化是否有意义。如果实例化该类永远不合适,那么它应该是抽象的。
一个经典的例子是 Shape 基类,它有 Square、Circle 和 Triangle 子类。Shape 永远不应该被实例化,因为根据定义,你不知道你想要它是什么形状。因此,将 Shape 设为抽象类是有意义的。
顺便说一句,另一个尚未提及的问题是,可以将成员添加到抽象类,让现有实现自动支持它们,并允许消费者使用知道新成员的实现和不知道新成员的实现,可以互换。虽然有一些合理的机制可以让未来的 .NET 运行时允许接口也以这种方式工作,但目前它们还没有。
例如,如果 IEnumerable 是一个抽象类(当然有很多理由不这样做),那么Count
当它的用处变得明显时,可以添加类似方法之类的东西;它的默认实现Count
可能很像IEnumerable<T>.Count
扩展方法,但是知道新方法的实现可以更有效地实现它(尽管IEnumerable<T>.Count
会尝试利用ICollection<T>.Count
or的实现ICollection.Count
,但它首先必须确定它们是否存在;相比之下,任何override 会知道它有代码可以Count
直接处理)。
本来可以添加一个ICountableEnumerable<T>
继承自IEnumerable<T>
但包含的接口Count
,并且现有代码将继续IEnumerable<T>
像往常一样正常工作,但是任何时候ICountableEnumerable<T>
通过现有代码传递,接收者都必须重新转换它ICountableEnumerable<T>
以使用Count
方法。远不如直接分派的Count
方法方便得多,它可以简单地直接作用于IEnumerable<T>
[Count
扩展方法并不可怕,但它的效率远低于直接分派的虚拟方法]。
如果有一种方法可以让接口包含静态方法,并且如果类加载器在发现Boz
声称实现的类IFoo
缺少方法string IFoo.Bar(int)
时,会自动添加到该类:
stringIFoo.Bar(int p1) { return IFoo.classHelper_Bar(Boz this, int p1); }
[假设接口包含该静态方法],那么可以在不破坏现有实现的情况下让接口添加成员,前提是它们还包括可以由默认实现调用的静态方法。不幸的是,我知道没有添加任何此类功能的计划。