【问题标题】:Design for Cross-Platform Classes in C#C# 中的跨平台类设计
【发布时间】:2022-04-14 16:14:03
【问题描述】:

总结:我想知道在C#中创建跨平台(例如桌面、Web和Silverlight)类的最佳设计,不重复代码,各有优缺点每个设计。

我经常为一个应用领域编写新的、有用的类;他们没有理由不能跨域工作。如何构建我的代码以使其成为理想的跨平台?

例如,假设我想创建一个具有间隔和 on-tick 事件的通用“MyTimer”类。在桌面上,这将使用内置的 .NET 计时器。在 Silverlight 中,我会使用 DispatchTimer。

设计#1 可能是“创建一个类并使用预处理器指令进行条件编译”,例如。 “#IF SILVERILGHT ...”。但是,这会导致代码难以理解、可读性和可维护性降低。

设计#2 可能是“创建名为 DesktopTimer 和 SilverlightTimer 的子类并使用来自 MyTimer 的子类”。这将如何运作?

虽然这是一个小例子,但我可能有更复杂的类,例如,使用特定于平台的类(IsolatedStorage、DispatchTimer 等)但不直接替换它们。

我还可以使用哪些其他设计/范式?

【问题讨论】:

    标签: c# architecture cross-platform


    【解决方案1】:

    我建议编写Interfaces,您只需为您的平台特定代码实现即可。然后,接口确保您的代码遵守接口给出的协定,否则将出现代码中断(如果一个成员未实现)。

    此外,在此库中驻留您的特定计时器类,为了坚持您的示例,我将为每个平台创建一个类,因此将 DispatchTimer 用于 Silverlight,并使用内置的 .NET 计时器用于桌面版本。

    最终,您将只使用一个接口,只有其实现者知道如何处理专门针对您的底层平台的合约。

    编辑#1

    条件设计不是好的设计的选择。这里有一个工具可以帮助你处理Dependancy Injection,叫做Unity Application Block,用来处理像你这样的场景。

    您只使用一个非常通用的 XML 配置来“告诉”在需要这个或那个接口时必须实例化什么。然后,UnityContainer 咨询您所做的配置,并为您实例化正确的类。这确保了良好的设计方法和架构。

    编辑#2

    我对依赖注入不是很熟悉,对 Unity 应用程序块也完全不熟悉。您能指出一些资源或进一步解释一下吗?

    1. Microsoft Enterprise Library 5.0 - April 2010;
    2. Microsoft Unity 2.0 – April 2010;
    3. Microsoft Unity 2.0 Documentation for Visual Studio 2008;
    4. Are there good tutorial/walkthroughs for unity that don't use configuration files?(关于应该为 Unity 提供有价值提示的主题的问题);
    5. Specifying Types in the Configuration File;
    6. Walkthrough: The Unity StopLight QuickStart;
    7. Walkthrough: The Unity Event Broker Extension QuickStart

    我认为这些资源将指导您完成学习。如果您需要进一步的帮助,请告诉我! =)

    编辑#3

    但无论如何,StopLight 快速入门 [...] 似乎暗示接口到具体类的依赖映射是在代码中完成的(这对我不起作用)。

    事实上,您可以同时进行代码和 XML 依赖映射,选择权在您手中! =)

    以下是一些示例,您或许应该从中获得启发,让 StopLight 快速入门使用 XML 配置而不是编码映射。

    1. Testing Your Unity XML Configuration;
    2. Using Design-Time Configuration;
    3. Source Schema for the Unity Application Block

    如果这不能帮助您解决问题,请告诉我。然后我将提供一个使用 XML 依赖映射的简单示例。 =)

    【讨论】:

    • 接口是设计#2的一个很好的建议。但是我如何在没有条件编译的情况下完成它?因此,在计时器示例中,我将有一个实现 IMyTimer 的 DesktopTimer 和 SilverlightTimer 类;但是 MyTimer 类是什么样的?
    • 假设您获得了必须在对象中使用的接口。然后,为您的 Silverlight 特定代码创建另一个项目将是一种方法,从而为您的桌面版本实施者创建另一个项目。当接口方法被调用时,您的平台特定代码将知道要实例化什么。
    • 在这种情况下Unity Application Block 的使用可能对Dependency Injection 有用。
    • 我对依赖注入不是很熟悉,对 Unity Application Block 一点也不熟悉。您能否指出一些资源或进一步解释这些资源?
    • Unity 似乎有点重。或者,也许我只是疯了。但无论如何,StopLight 快速入门 (msdn.microsoft.com/en-us/library/ff660908(v=PandP.20).aspx) 似乎暗示接口到具体类的依赖映射是在代码中完成的(这对我不起作用)。或者这应该是另一个问题?
    【解决方案2】:

    1) 在它们自己的程序集中具有特定于平台的类的接口:例如,共享程序集中的ITimer,以及包含WebTimer 的“WebAssembly”。然后按需加载“WebAssembly.dll”或“DesktopAssembly.dll”。这将其变成了更多的部署/配置问题,并且一切都可以编译。依赖注入或 MEF 在这里成为了很大的帮助。

    2) 接口(再次),但带有条件编译。这使其不再是部署问题,而是更多的编译问题。 WebTimer 周围会有 #ifdef WEB_PLATFORM,依此类推。

    就个人而言,我倾向于#1 - 但在一个复杂的应用程序中,很可能您最终不得不同时使用这两种方法,因为silverlight 和其他所有东西之间.net 框架的可用部分略有变化。您甚至可能希望应用的核心部分出现不同的行为,只是为了解决性能问题。

    【讨论】:

    • 动态链接程序集似乎是正确的方向。但是条件编译正是我想要避免的那种意大利面条式的代码混乱。调试、维护和简单理解任何大型代码的那种代码都是一场噩梦。
    • 我完全同意避免它,但是对于任何非常复杂的系统,您可能无法完全消除它。
    • 因此我们进行设计讨论:)
    【解决方案3】:

    我认为接口在这里是一个不错的选择(定义计时器将做什么而不实际实现它)

    public interface ITimer
    {
        void CreateTimer(int _interval, TimerDelegate _delegate);
        void StopTimer();
        // etc...
    } // eo interface ITimer
    

    由此,您可以得出具体的计时器:

    public class DesktopTimer : ITimer
    {
    } // eo DesktopTimer
    
    public class SilverlightTimer : ITimer
    {
    } // eo class SilverlightTimer
    
    public class WebTimer : Timer
    {
    } // eo class WebTimer
    

    接下来是有趣的部分。我们如何创建正确的计时器?在这里,您可以实现某种平台工厂,它根据运行的平台返回正确的计时器。这是一个快速而肮脏的想法(我会让它比这更动态,也许为多种类实现一个工厂,但这是一个例子)

    public enum Platform
    {
         Desktop,
         Web,
         Silverlight
    } // eo enum Platform
    
    public class TimerFactory
    {
        private class ObjectInfo
        {
            private string m_Assembly;
            private string m_Type;
    
            // ctor
            public ObjectInfo(string _assembly, string _type)
            {
                m_Assembly = _assembly;
                m_Type = _type;
            } // eo ctor
    
            public ITimer Create() {return(AppDomain.CurrentDomain.CreateInstanceAndUnwrap(m_Assembly, m_Type));}
        } // eo class ObjectInfo
    
        Dictionary<Platform, ObjectInfo> m_Types = new Dictionary<PlatForm, ObjectInfo>();
    
        public TimerFactory()
        {
            m_Types[Platform.Desktop] = new ObjectInfo("Desktop", "MyNamespace.DesktopTimer");
            m_Types[Platform.Silverlight] = new ObjectInfo("Silverlight", "MyNameSpace.SilverlightTimer");
            // ...
        } // eo ctor
    
        public ITimer Create()
        {
            // based on platform, create appropriate ObjectInfo
        } // eo Create
    } // eo class TimerFactory
    

    正如我上面提到的,我不会为每个对象创建一个工厂,而是创建一个通用的平台工厂,可以处理计时器、容器和任何你想要的东西。这只是一个例子。

    【讨论】:

    • 查看 Will 的评论——关于如何在 MyTimer 主类中连接它们的“秘诀”在哪里?
    • 太棒了,谢谢。我会看看 Unity,看看什么似乎有更多的优点和更少的缺点。我喜欢你在这里概述的直截了当的“没有大量额外库”的方式。
    • 你所做的本质上是依赖注入,尽管它是轻量级的。配置可能会通过 SomeClass.SetPlatform(blah) 或一些 XML 文件或其他东西来完成。
    • @ashes999,实际上我在想 PlatformFactory 会有一个 Platform 枚举,并根据环境检查将其设置为构造。
    【解决方案4】:

    如果您想将所有用户界面逻辑与您正在使用的实际 GUI 框架分开,那么模型-视图-演示者模式是一种非常好的方法。阅读 Michael Feather 的文章“The Humble Dialog Box”,了解其工作原理:

    http://www.objectmentor.com/resources/articles/TheHumbleDialogBox.pdf

    原始文章是为 C++ 编写的,如果您想要 C# 示例,请看这里:

    http://codebetter.com/blogs/jeremy.miller/articles/129546.aspx

    优点是:

    • 您将使您的 GUI 逻辑可重用
    • 您的 GUI 逻辑适用于单元测试

    缺点:

    • 如果您的程序不需要多个 GUI 框架,这种方法会产生更多的代码行,并且您必须处理更多的复杂性,因为您必须在整个编码过程中决定代码的哪些部分属于视图和哪个进入演示者

    【讨论】:

    • 我的问题是关于编码库,而不是实际的应用程序。
    • 好吧,第一手我不是很清楚。也许你会发现我的回答也很有帮助,因为文章中展示的一般技术都朝着相同的方向发展(如何通过使用接口、模拟等来制作可重用的类)。
    【解决方案5】:

    使用你所知道的所有 OOD。我建议创建与平台无关的(Windows、Mono/destkop、web)域模型。使用抽象类来建模依赖于平台的东西(如计时器)。使用依赖注入和/或工厂模式来使用特定的实现。

    编辑:在某些时候,您必须指定要使用的具体类,但使用上述模式可以将所有代码集中到一个位置,而无需使用条件编译。

    编辑:DI/Factory 的一个例子。当然,您可以使用现有的框架,这将为您提供更多的功能和表现力。对于简单的示例,这似乎有点过头了,但是代码越复杂,使用模式的收益就越大。

    // Common.dll
    
    public interface IPlatformInfo
    {
        string PlatformName { get; }
    }
    
    public interface PlatformFactory
    {
        IPlatformInfo CreatePlatformInfo();
        // other...
    }
    
    public class WelcomeMessage
    {
        private IPlatformInfo platformInfo;
    
        public WelcomeMessage(IPlatformInfo platformInfo)
        {
            this.platformInfo = platformInfo;
        }
    
        public string GetMessage()
        {
            return "Welcome at " + platformInfo.PlatformName + "!";
        }
    }
    
    // WindowsApp.exe
    
    public class WindowsPlatformInfo : IPlatformInfo
    {
        public string PlatformName
        {
            get { return "Windows"; }
        }
    }
    
    public class WindowsPlatformFactory : PlatformFactory
    {
        public IPlatformInfo CreatePlatformInfo()
        {
            return new WindowsPlatformInfo();
        }
    }
    
    public class WindowsProgram
    {
        public static void Main(string[] args)
        {
            var factory = new WindowsPlatformFactory();
            var message = new WelcomeMessage(factory.CreatePlatformInfo());
            Console.WriteLine(message.GetMessage());
        }
    }
    
    // MonoApp.exe
    
    public class MonoPlatformInfo : IPlatformInfo
    {
        public string PlatformName
        {
            get { return "Mono"; }
        }
    }
    
    public class MonoPlatformFactory : PlatformFactory
    {
        public IPlatformInfo CreatePlatformInfo()
        {
            return new MonoPlatformInfo();
        }
    }
    
    public class MonoProgram
    {
        public static void Main(string[] args)
        {
            var factory = new MonoPlatformFactory();
            var message = new WelcomeMessage(factory.CreatePlatformInfo());
            Console.WriteLine(message.GetMessage());
        }
    }
    

    【讨论】:

    • DI 和工厂模式听起来是正确的想法。你能再详细说明一下吗?
    • 当然,我添加了一个示例。查看 Wikipedia 以获取有关模式的更多描述。 HTH。
    【解决方案6】:

    正如其他人所建议的那样,接口是这里的必经之路。我会稍微改变一下 sugestion Moo-Juice 建议的界面...

    ` //为什么这个块不像code那样格式化???

    公共接口 ITimer{
    无效停止计时器(); // 等等...

    void StartTimer(); // 等等...

    TimeSpan Duration {get;} // eo 接口 ITimer

    }`

    现在您需要将 ITimer 放入正在使用它的类中。最简单的方法称为依赖注入。实现这一点的最常见方法称为构造函数注入。

    因此,当创建需要计时器的类时,您可以在创建时将计时器传递给该类。

    基本上你会这样做:

    var foo = new Foo(new WebTimer());

    由于这会很快变得复杂,您可以使用一些助手。这种模式称为控制反转。有一些框架可以帮助您,例如 ninject 或 Castle Windsor。

    两者都是控制反转 (IOC) 容器。 (这就是秘诀)

    基本上,您在 IOC 中“注册”您的计时器,同时也注册您的“Foo”。当你需要一个“Foo”时,你要求你的 IOC 容器创建一个。容器查看构造函数,发现它需要一个 ITimer。然后它将为您创建一个 ITimer,并将其传递给构造函数,最后将完整的类交给您。

    在您的课堂内,您不需要了解 ITimer 或如何创建它,因为所有这些都移到了外面。

    对于不同的应用程序,您现在只需注册正确的组件,就完成了...

    P.s.:小心,不要将 IOC Container 与服务定位器混淆......

    链接:

    http://ninject.org/download

    http://www.castleproject.org/container/index.html

    http://www.pnpguidance.net/Category/Unity.aspx

    【讨论】:

      【解决方案7】:

      为什么没有配置部分,它会告诉您的库有关主机应用程序平台的信息。您只需在应用程序中为主机配置文件(web.config 或 app.config)设置一次,然后您可以按照 Moo-Juice 的建议使用 Factory 方法。您可以在库的整个功能上使用平台详细信息。

      【讨论】:

        猜你喜欢
        • 2011-04-18
        • 2011-04-14
        • 2019-05-01
        • 1970-01-01
        • 2010-12-25
        • 1970-01-01
        • 2011-07-11
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多