系统的类设计是分层的,但并不整齐

本文关键字:不整齐 分层 系统 | 更新日期: 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和其他人)的想法。