系统的类设计是分层的,但并不整齐
本文关键字:不整齐 分层 系统 | 更新日期: 2023-09-27 18:28:09
我已经遇到过好几次了,所以我想用一个真实的例子来了解更有经验的C#开发人员如何处理这个问题。
我正在围绕非托管的MediaInfo库编写一个.NET包装程序,该库收集有关媒体文件(电影、图像…)的各种数据
MediaInfo有许多功能,每个功能都适用于不同类型的文件。例如,"PixelAspectRatio"适用于图像和视频,但不适用于音频、字幕或其他内容。
我想包装的功能的子集如下:
General Video Audio Text Image Chapters Menu (Name of function)
x x x x x x x Format
x x x x x x x Title
x x x x x x x UniqueID
x x x x x x CodecID
x x x x x x CodecID/Hint
x x x x x Language
x x x x x Encoded_Date
x x x x x Encoded_Library
x x x x x InternetMediaType
x x x x x StreamSize
x x x x BitDepth
x x x x Compression_Mode
x x x x Compression_Ratio
x x x x x Delay
x x x x x Duration
x x x BitRate
x x x BitRate_Mode
x x x ChannelLayout
x x x FrameCount
x x x FrameRate
x x x MuxingMode
x x x MuxingMode
x x x Source_Duration
x x x Height
x x x Width
x x PixelAspectRatio
x SamplingRate
x Album
x AudioCount
x ChaptersCount
x EncodedBy
x Grouping
x ImageCount
x OverallBitRate
x OverallBitRate_Maximum
x OverallBitRate_Minimum
x OverallBitRate_Nominal
x TextCount
x VideoCount
正如您所看到的,一个不错的类映射的开始将是一个特定于每个流类型的功能的类,以及一个具有所有类型通用功能的基类。
然后这条路就不那么明显了。{general,video,audio,text,and image}流类型共有许多功能。好吧,我想我可以用一个臭名字做一个类,比如"GeneralVideoAudioTextImage",然后用另一个名为GeneralVideoAudieText(继承自GeneralVideoAudidTextImage)的类来实现这些东西的通用功能,但不能用"Image"流。我想,这将尴尬地遵循类层次结构的"is a"规则。
这看起来已经不优雅了,但偶尔也会出现像"宽度"这样的情况,它们不适合任何一个组,而这个组完全是另一个组的子集。这些情况可以在必要时简单地复制功能——分别在视频、文本和图像中实现,但这显然违反了DRY。
一个常见的第一种方法是MI,C#不支持它。对此,通常的答案似乎是"将MI与接口结合使用",但我无法完全理解DRY是如何做到这一点的。也许是我的失败。
类层次结构以前已经在SO上讨论过,MI的替代方案(扩展方法等)也讨论过,但这些解决方案似乎都不太适合。例如,扩展方法似乎更适合于源代码无法编辑的类,如String类,并且更难定位,因为它们与类没有真正的联系,尽管它们可能有效。我还没有发现关于这种情况的问题,尽管这可能是我使用搜索工具的失败。
一个MediaInfo功能的例子,包装,可能是:
int _width = int.MinValue;
/// <summary>Width in pixels.</summary>
public int width {
get {
if(_width == int.MinValue)
_width = miGetInt("Width");
return _width;
}
}
// ... (Elsewhere, in another file) ...
/// <summary>Returns a MediaInfo value as an int, 0 if error.</summary>
/// <param name="parameter">The MediaInfo parameter.</param>
public int miGetInt(string parameter) {
int parsedValue;
string miResult = mediaInfo.Get(streamKind, id, parameter);
int.TryParse(miResult, out parsedValue);
return parsedValue;
}
我的问题是:你是如何处理这样的情况的,这些系统有点等级森严,但并不完全?你是否找到了一个相当优雅的策略,或者只是接受了并非每个简单的问题都有一个?
我认为最好使用接口的组合,如果实现比一堆属性更复杂,那么组合可以提供接口的共享实现:
abstract class Media {
// General properties/functions
}
class VideoAndImageCommon { // Crappy name but you get the idea
// Functions used by both video and images
}
interface IVideoAndImageCommon {
// Common Video & Image interface
}
class Video : Media, IVideoAndImageCommon {
private readonly VideoAndImageCommon _commonImpl = new VideoAndImageCommon();
// Implementation of IVideoAndImageCommon delegates to _commonImpl.
}
class Image : Media, IVideoAndImageCommon {
private readonly VideoAndImageCommon _commonImpl = new VideoAndImageCommon();
// Implementation of IVideoAndImageCommon delegates to _commonImpl.
}
我认为Andrew Kennan的答案是正确的,因为我认为这可能是对某种复杂层次结构最好的通用方法。然而,我并没有在最后的代码中使用他的建议。
我发布这个新答案是为了帮助任何未来的SO用户,因为类似的问题而查找这个问题。
虽然接口可能是更干净层次结构的最佳通用解决方案,但在这种情况下,它们对我来说并不适用。如果我做了分层分组,如果不使用匿名名称,我就无法将其作为OSS项目发布。隐含的接口名称开始闻起来很难闻,比如"IGeneralVideoAudioTextImageMenuCommon"answers"IVideoAudioTextImage Menucommon";有4或5个这样的继承声明是不雅的,这是人类以前从未见过的:
///<summary>Represents a single video stream.</summary>
public sealed class VideoStream : Media, IGeneralVideoAudioTextImageMenuCommon,
IGeneralVideoAudioTextMenuCommon, IVideoAudioTextImageMenuCommon,
IVideoTextImageCommon, /* ...ad nauseum. */
{
// What would you even name these variables?
GeneralVideoAudioTextImageMenuCommon gvatimCommon;
GeneralVideoAudioTextMenuCommon gvatmCommon;
VideoAudioTextImageMenuCommon vatimCommon;
VideoTextImageCommon vticCommon;
public VideoStream(MediaInfo mi, int id) {
gvatimCommon = new GeneralVideoAudioTextImageMenuCommon(mi, id);
gvatmCommon = new GeneralVideoAudioTextMenuCommon(mi, id);
vatimCommon = new VideoAudioTextImageMenuCommon(mi, id);
vticCommon = new VideoTextImageCommon(mi, id);
// --- and more. There are so far at least 10 groupings. 10!
/* more code */
}
根据菲尔·卡尔顿(Phil Karlton)的说法,命名是计算机科学中的两个难题之一,实际上正在接近"是"的哲学定义。汽车是一种交通工具,是的,从技术上讲,汽车也是一种摩托车汽车卡车或飞机,但这一切都是为了让人类理解代码,对吧?
我考虑了一种混合方法,将具有一小组功能的分组合并。但后来我问自己,"这会让代码更容易理解吗?"我的回答是,"不会",因为看到继承层次结构对读者来说意味着某种代码组织。第二种方法可能会增加比节省下来的更大的复杂性:这不是一场胜利。
我的解决方案,尽管我仍然认为必须存在一个更好的方案,是将多个流的所有公共功能放在一个单独的类"MultiStreamCommon"中,在内部使用#regions进行分组:
public class MultiStreamCommon : Media
{
public MultiStreamCommon(MediaInfo mediaInfo, StreamKind kind, int id)
: base(mediaInfo, id) {
this.kind = kind;
}
#region AllStreamsCommon
string _format;
///<summary>The format or container of this file or stream.</summary>
///<example>Windows Media, JPEG, MPEG-4.</example>
public string format { /* implementation */ };
string _title;
///<summary>The title of the movie, track, song, etc..</summary>
public string title { /* implementation */ };
/* more accessors */
#endregion
#region VideoAudioTextCommon
/* Methods appropriate to this region. */
#endregion
// More regions, one for each grouping.
}
每个流创建一个MultiStreamCommon实例,并通过访问者公开相关功能:
public sealed class VideoStream : Media
{
readonly MultiStreamCommon streamCommon;
///<summary>VideoStream constructor</summary>
///<param name="mediaInfo">A MediaInfo object.</param>
///<param name="id">The ID for this audio stream.</param>
public VideoStream(MediaInfo mediaInfo, int id) : base(mediaInfo, id) {
this.kind = StreamKind.Video;
streamCommon = new MultiStreamCommon(mediaInfo, kind, id);
}
public string format { get { return streamCommon.format; } }
public string title { get { return streamCommon.title; } }
public string uniqueId { get { return streamCommon.uniqueId; } }
/* ...One line for every media function relevant to this stream type */
}
好的一面是,尽管丑陋仍然存在,但仅限于MultiStreamCommon类。我相信#region分组比每个有六个左右继承接口的流类可读性更强。维护人员如何知道在哪里添加新的MediaInfo函数?只要弄清楚它适用于哪些流,并将其放在适当的区域即可。如果它们分组错误,它仍然有效,并且可以通过访问器看到。
缺点是媒体流没有继承适当的功能——必须编写访问器。文档也不是继承的,因此每个流都需要为每个访问器使用一个标记,从而导致文档重复。话虽如此:重复的文档总比重复的代码好
在这种情况下,强制执行适当的继承层次结构远比我们必须记住的根本问题(只是包装一些库)复杂。
对于那些对与这些线程相关的代码或其目的感兴趣的人(使用.NET轻松获取媒体文件信息),这个项目的谷歌代码页面就在这里。请告诉我任何想法,特别是那些发布了我遇到的大量漂亮代码的人(Jon Skeet、Marc Gravell和其他人)的想法。