.NET 中的接口分解/接口设计

Posted

技术标签:

【中文标题】.NET 中的接口分解/接口设计【英文标题】:interface factoring / interface design in .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 实现,这可能是个问题。

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

【问题讨论】:

【参考方案1】:

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

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

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

【讨论】:

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

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

要点:

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

这里有一些示例代码:

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");

        

    

【讨论】:

以上是关于.NET 中的接口分解/接口设计的主要内容,如果未能解决你的问题,请参考以下文章

聊聊接口幂等性设计

如何设计一个安全的对外接口

再次思考一下go网络包中的接口设计

Java抽象接口技巧

软件工程第五周思考题

设计模式之设计原则-接口隔离原则