【问题标题】:interface factoring / interface design in .NET.NET 中的接口分解/接口设计
【发布时间】:2016-01-31 02:34:59
【问题描述】:

我想设计一个由几个松散耦合的组件组成的内部框架。

在一本书中,我发现建议更喜欢通过具体类实现多个接口,而不是构建接口树。

数据访问接口(系列)示例:

我的做法:

interface ISession : IDisposable
{
    void OpenSession();
    void CloseSession();
}

interface IDataAccess: ISession
{
    void SetData(string data);
    string GetData();
}

class MyTextFileAccess : IDataAccess 
{
    // no-op Open, Close, Dispose
}

class SQLDataAccess : IDataAccess
{
    // all interface methods are really needed
}

这本书似乎偏爱的方法可能是这样的:

interface ISession
{
    void OpenSession();
    void CloseSession();
}

interface IDataAccess
{
    void SetData(string data);
    string GetData();
}


class MyTextFileAccess : IDataAccess 
{
    // don't have to implement unused interface methods
}

class SQLDataAccess : IDataAccess, IDisposable, ISession
{
    // same as above
}

接口的使用也会有所不同:

void UseMyWay(IDataAccess da) // IDataAccess inherits from ISession and IDisposable
{
    da.OpenSession();
    da.GetData();
    da.SetData("");
    da.CloseSession();

    da.Dispose();
}
void UseTheBooksWay(IDataAccess da) // IDataAccess doesn't inherit from ISession and IDisposable
{
    var da_session = da as ISession;
    var da_disposable  = da as IDisposable;

    if (da_session != null)
        da_session.OpenSession();
    da.GetData();
    da.SetData("");
    if (da_session != null)
        da_session.CloseSession();

    if (da_disposable != null)
    da_disposable.Dispose();
}

我更喜欢我的方法中的用法,因为它是统一的,用户不必考虑测试多个接口(仅通过查看IDataAccess 并不明显),因此不能忘记这样做.

但我也更喜欢简洁的界面,专门用于书籍中使用的一个主题;这还有一个好处是我不必实现显然不需要的接口(文本文件访问)。 我的方法中的“最终”接口需要了解所有看起来不正确的实现,因为接口不应该依赖于实现细节。如果我想添加一个需要首先进行身份验证的新 SecureDataAccess 实现,这可能是个问题。

有没有办法将这两种优势结合起来,或者你有其他方法吗?

【问题讨论】:

    标签: c# interface components


    【解决方案1】:

    我不会说你是对还是错,这可能是关于使用哪一个的间接情况。在您的方法的情况下,如果您实现 IDataAccess,则需要实现 ISession 和 IDisposable,如果您始终需要这样做,那是可以的。

    另一种方法具有优势,因为您可以独立使用所有 3 种方法,使其更加灵活。您可以使用 IDataAccess 和 ISession 而不必实现 IDiposable。我无法真正告诉您哪些可以使用,哪些不能使用,因为您应该根据您的场景权衡优势/劣势。

    我想将两者结合起来意味着在一个项目中你有一些有你的方法,一些有书籍的方法,我个人会使用书籍的方法,就好像你走得更远,需要使用一个没有否则它可能会成为可维护性的问题,也许您可​​以说您的方法与您的松散耦合架构背道而驰。

    【讨论】:

    • 我添加了我的最新方法作为另一个答案。非常感谢您对此的看法。
    【解决方案2】:

    这是迄今为止我想出的最佳解决方案: 它可以从库端扩展,但从用户端也可以安全(用户无法知道哪些接口也应该使用(ISessionIDisposable...)。

    要点:

    • 主要想法是在消费者和库之间建立一个定义合同的类,例如哪些接口可用/必须使用。
    • 如果合同中不包含ISessionIDisposable,则SQLDataAccess 将不可用。
    • 通过此合约使用接口的客户端至少知道预期的接口并有机会正确使用它们。
    • FileDataAccess 可以使用“简单”合约 PlainDataAccess 并且 - 在一些无操作实现的帮助下 - 更高级的 SessionedDataAccess
    • FileDataAccess 不需要自己实现这些接口。

    这里有一些示例代码:

    using System;
    namespace InterfaceDesign
    {
    
        // the interface we're really interested in
        public interface IDataAccess
        {
            string GetData();
            void SetData(string data);
        }
    
        // a requirement for SQLDataAccess
        public interface ISession
        {
            void OpenSession();
            void CloseSession();
        }
    
        /// <summary>
        /// This "interface for the consumer" is intended only for classes that have no special needs before/after accessing data
        /// </summary>
        public class PlainDataAccess
        {
            internal PlainDataAccess(IDataAccess dataAccess)
            {
                this.DataAccess = dataAccess;
            }
            public IDataAccess DataAccess { get; private set; }
        }
    
        /// <summary>
        /// This "interface for the consumer" is intended for classes that have only the special needs to open/close sessions and be disposed.
        /// </summary>
        public class SessionedDataAccess
        {
            public IDataAccess DataAccess { get; private set; }
            public ISession Session { get; private set; }
            public IDisposable Disposable { get; private set; }
    
    
            /// <summary>
            /// Simplifies creation by not having to pass in the same variable 3 times
            /// </summary>
            internal static SessionedDataAccess Create<T>(T sessionedDataAccess) where T :IDataAccess, ISession, IDisposable
            {
                return new SessionedDataAccess(dataAccess: sessionedDataAccess , session: sessionedDataAccess , disposable: sessionedDataAccess);
            }
    
            private SessionedDataAccess(IDataAccess dataAccess, ISession session, IDisposable disposable)
            {
                this.DataAccess = dataAccess;
                this.Session = session;
                this.Disposable = disposable;
            }
    
            public  static SessionedDataAccess Create(PlainDataAccess dataAccess)
            {
                return new SessionedDataAccess(dataAccess: dataAccess.DataAccess, session: NoOpSession.Instance,
                    disposable: NoOpDisposable.Instance);
            }
    
            private class NoOpSession : ISession
            {
                public static readonly ISession Instance = new NoOpSession();
    
                public void OpenSession()
                {
                    Console.WriteLine("no op session opened");
                }
    
                public void CloseSession()
                {
                    Console.WriteLine("no op session closed");
                }
            }
            private class NoOpDisposable : IDisposable
            {
                public static readonly IDisposable Instance = new NoOpDisposable();
                public void Dispose()
                {
                    Console.WriteLine("no op disposed");
                }
            }
        }
    
        // further "consumer interfaces" can be added. Extension methods provide easy promotions to more "demanding" interfaces by using no-op implementations.
    
    
    
        /// <summary>
        /// A sample class that has no special need for sessions etc.
        /// </summary>
        public class FileDataAccess : IDataAccess
        {
            private FileDataAccess()
            {
            }
    
    
            public static PlainDataAccess Create()
            {
                return new PlainDataAccess(dataAccess: new FileDataAccess());
            }
    
            public string GetData()
            {
                return "from file";
            }
    
            public void SetData(string data)
            {
                Console.WriteLine("written to file");
            }
        }
    
    
        /// <summary>
        /// A more complex data access requiring sessions
        /// </summary>
        public class SQLDataAccess : IDataAccess, ISession, IDisposable
        {
            private SQLDataAccess()
            {
            }
    
    
            public static SessionedDataAccess Create()
            {
                return SessionedDataAccess.Create(new SQLDataAccess());
            }
    
    
            public string GetData()
            {
                return "from sql";
            }
    
            public void SetData(string data)
            {
                Console.WriteLine("written to SQL");
            }
    
            public void OpenSession()
            {
                Console.WriteLine("session opened");
            }
    
            public void CloseSession()
            {
                Console.WriteLine("Session closed");
            }
    
            public void Dispose()
            {
                Console.WriteLine("disposed");
            }
        }
    
    
    
    
        public static class Extensions
        {
            /// <summary>
            /// allows to use a plain data access as a sessioned data access by providing no-op  ISession and IDisposable implementations
            /// </summary>
            public static SessionedDataAccess Promote(this PlainDataAccess self)
            {
                return SessionedDataAccess.Create(dataAccess: self);
            }
        }
    
    
    
    
        public class ConsumerExamples
        {
            public void Provider()
            {
                var fileAccess = FileDataAccess.Create();
                var sqlDataAccess = SQLDataAccess.Create();
    
    
                Consume1(sqlDataAccess);
    
                //Consume2(sqlDataAccess);  // this won't compile
    
                Consume1(fileAccess.Promote());
                Consume2(fileAccess);
    
    
            }
    
    
    
            /// <summary>
            /// This consumer is prepared to handle sessions.
            /// Can handle our sql data access and (after promotion) the basic file data access
            /// </summary>
            public void Consume1(SessionedDataAccess sessionedDataAccess)
            {
                sessionedDataAccess.Session.OpenSession();
    
                Console.WriteLine(
                    sessionedDataAccess.DataAccess.GetData()
                    );
                sessionedDataAccess.DataAccess.SetData("data");
    
                sessionedDataAccess.Session.CloseSession();
                sessionedDataAccess.Disposable.Dispose();
            }
    
            // can consume only file access
    
            /// <summary>
            /// This consumer is NOT prepared to handle sessions.
            /// It can only handle the file access.
            /// There is no way (without cheating) to pass in a SQL data access here
            /// </summary>
            public void Consume2(PlainDataAccess dataAccess)
            {
                Console.WriteLine(
                    dataAccess.DataAccess.GetData()
                    );
                dataAccess.DataAccess.SetData("data");
    
            }
    
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2011-04-01
      • 2010-09-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-12-09
      • 1970-01-01
      相关资源
      最近更新 更多