通过向下转换继承树,在 C# 中进行“更宽松”的输入
-
09-06-2019 - |
题
我想问的问题是:
正在放弃继承树(即。从抽象类内部转向更专业的类)是情有可原的,甚至是一件好事,或者它总是一个糟糕的选择,有更好的选择?
现在,我举一个例子来说明为什么我认为它可以永久使用。
我最近实施了 从 BitTorrent 协议进行编码 在 C# 中。一个足够简单的问题,如何表示数据。我选择这样做,
我们有一个 abstract BItem
类,它提供了一些基本功能,包括 static BItem Decode(string)
用于将 Bencoded 字符串解码为必要的结构。
还有四个派生类, BString
, BInteger
, BList
和 BDictionary
, ,代表要编码的四种不同的数据类型。现在,这是棘手的部分。 BList
和 BDictionary
有 this[int]
和 this[string]
访问器分别允许访问这些数据类型的类似数组的特性。
潜在可怕的部分现在来了:
BDictionary torrent = (BDictionary) BItem.DecodeFile("my.torrent");
int filelength = (BInteger)((BDictionary)((BList)((BDictionary)
torrent["info"])["files"])[0])["length"];
嗯,看图你就懂了……哎呀,这对眼睛来说太难了,更不用说大脑了。所以,我在抽象类中引入了一些额外的东西:
public BItem this[int index]
{
get { return ((BList)this)[index]; }
}
public BItem this[string index]
{
get { return ((BDictionary)this)[index]; }
}
现在我们可以将旧代码重写为:
BDictionary torrent = (BDictionary)BItem.DecodeFile("my.torrent");
int filelength = (BInteger)torrent["info"]["files"][0]["length"];
哇,嘿,很快,代码可读性多了。但我是否只是为了将子类的知识隐含到抽象类中而出卖了我的一部分灵魂呢?
编辑:为了回应一些答案,你完全偏离了这个特定问题的轨道,因为结构是可变的,例如我的例子 torrent["info"]["files"][0]["length"]
是有效的,但也是如此 torrent["announce-list"][0][0]
, ,并且两者都存在于 90% 的 torrent 文件中。泛型不是解决办法,至少有这个问题:(。点击查看我链接的规格,它只有 4 个小点大。
解决方案
我想我会将 this[int] 和 this[string] 访问器设为虚拟,并在 BList/BDictionary 中覆盖它们。访问器没有意义的类应该强制转换 NotSupportedException()(可能通过在 BItem 中使用默认实现)。
这使得您的代码以相同的方式工作,并在您应该编写时给您一个更具可读性的错误
(BInteger)torrent["info"][0]["files"]["length"];
因为失误。
其他提示
您确实不应该从基类访问任何派生类,因为它几乎破坏了 OOP 的思想。可读性当然有很长的路要走,但我不会用它来换取可重用性。考虑当您需要添加另一个子类时的情况 - 您还需要相应地更新基类。
如果文件长度是您经常检索的内容,为什么不在 BDictionary (?) 类中实现一个属性...这样你的代码就变成:
BDictionary torrent = BItem.DecodeFile("my.torrent");
int filelength = torrent.FileLength;
这样,实现细节就对用户隐藏了。
在我看来,并非所有 BItem 都是集合,因此并非所有 BItem 都有索引器,因此索引器不应该位于 BItem 中。我将从 BItem 派生另一个抽象类,将其命名为 BCollection,并将索引器放在那里,如下所示:
abstract class BCollection : BItem {
public BItem this[int index] {get;}
public BItem this[string index] {get;}
}
并使BList和BDictionary继承自BCollection。或者您可以加倍努力,使 BCollection 成为一个泛型类。
我的建议是引入更多抽象。我发现 BItem 有一个返回 BDictionary 的 DecodeFile() 令人困惑。我不知道,这在 torrent 域中可能是合理的做法。
不过,我觉得像下面这样的 api 更合理:
BFile torrent = BFile.DecodeFile("my.torrent");
int filelength = torrent.Length;
您是否考虑过解析一个简单的“路径”,以便可以这样写:
BDictionary torrent = BItem.DecodeFile("my.torrent");
int filelength = (int)torrent.Fetch("info.files.0.length");
也许不是最好的方法,但可读性增加了(一点点)
- 如果您可以完全控制您的代码库和思维过程,请务必这样做。
- 如果没有,当某个新人注入你没有看到的 BItem 派生的那一天,你会后悔的 你的 BList 或 BDictionary。
如果必须这样做,至少将其包装在具有强类型方法签名的类中(控制对列表的访问)。
BString GetString(BInteger);
SetString(BInteger, BString);
接受并返回 BString,即使您在内部将其存储在 BItem 的 BList 中。 (让我在做 2 B 或不做 2 B 之前先分开)
唔。我实际上认为第一行编码比第二行更具可读性 - 需要更长的时间才能弄清楚它发生了什么,但更明显的是您将对象视为 BList 或 BDictionary。将方法应用于抽象类会隐藏该细节,这会使您更难弄清楚您的方法实际上在做什么。
如果引入泛型,就可以避免强制转换。
class DecodedTorrent : BDictionary<BDictionary<BList<BDictionary<BInteger>>>>
{
}
DecodedTorrent torrent = BItem.DecodeFile("mytorrent");
int x = torrent["info"]["files"][0]["length"];
嗯,但这可能行不通,因为类型可能取决于您通过结构的路径。
只有我吗
BDictionary torrent = BItem.DecodeFile("my.torrent");int filelength = (BInteger)((BDictionary)((BList)((BDictionary) torrent["info"])["files"])[0])["length"];
您不需要将 BDictionary 强制转换“torrent”声明为 BDictionary
public BItem this[int index]{ get { return ((BList)this)[index]; }}public BItem this[string index]{ get { return ((BDictionary)this)[index]; }}
这些并没有达到预期的结果,因为返回类型仍然是抽象版本,所以您仍然需要进行转换。
重写的代码必须是
BDictionary torrent = BItem.DecodeFile("my.torrent");int filelength = (BInteger)((BList)((BDictionary)torrent["info"]["files"])[0])["length"];
哪一个和第一批一样糟糕