我已经多次遇到过这种情况,所以我想使用一个真实的例子来了解更有经验的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和image}流类型有许多共同的功能。好吧,所以我想我可以创建一个像“GeneralVideoAudioTextImage”这样的臭名字的类,然后另一个名为GeneralVideoAudioText(继承自GeneralVideoAudioTextImage)的类,用于这些事物的共同功能,但不是“图像”流。我猜这会尴尬地遵循“等级”的阶级等级规则。
这已经不是很优雅了,但是有些像“宽度”这样的偶然情况不适合任何干净地成为另一组子集的组。这些案例可以在必要时简单地复制功能 - 单独在视频,文本和图像中实现,但这显然会违反DRY。
常见的第一种方法是MI,C#不支持。通常的答案似乎是“使用带接口的MI”,但我无法完全看到它如何跟随DRY。也许是我失败了。
之前已经讨论了类层次结构,如同alternatives到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;
}
我的问题是:你是如何处理这样的情况的,系统是层次结构但不完全?您是否找到了一个相当优雅的策略,或者只是接受了并非每个简单的问题都有一个?
答案 0 :(得分:5)
我认为你最好使用接口的组合,如果实现比一堆属性更复杂,那么组合提供接口的共享实现:
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.
}
答案 1 :(得分:0)
我将Andrew Kennan的答案标记为正确,因为我认为这可能是对某种复杂层次结构的最佳通用方法。但是,我没有在我的最终代码中使用他的建议。
我发布这个新答案是为了帮助任何因为类似问题而查询此问题的未来SO用户。
虽然接口可能是更干净的层次结构的最佳通用解决方案,但在这种情况下它们并不适用于我。如果我没有使用匿名名称,如果我进行了分层分组,我将无法将其作为OSS项目发布。隐含的界面名称开始闻起来非常糟糕,如“IGeneralVideoAudioTextImageMenuCommon”和“IVideoAudioTextImageMenuCommon”;继承声明中有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轻松获取媒体文件信息),此项目的Google Code页面为here。请让我知道任何想法,特别是那些发布了我遇到的大量漂亮代码的人(Jon Skeet,Marc Gravell和其他人)。