接口(interface)的概念,在现实生活中是很常见的,标准化接口带来的好处就是,不同厂商、不同类型的设备之间可以有效地配合工作,如电脑中的USB接口就是一个非常典型的代表。

软件开发中,使用接口的意义也是这样,通过接口对组件进行组合,可以提高软件系统各组件的独立性,使得开发、维护和扩展都会更加灵活和高效。在C#中,接口类型是比抽象类更加抽象的类型,因为接口中只允许定义成员,并没有任何实现代码。

下面的代码,我们定义了两个接口类型,分别是IDbEngine和IDbRecord接口。

namespace ConsoleTest
{
    //
    public interface IDbEngine
    {
        string CnnStr { get; }
        string Type { get; }
        bool Connected { get; }
        object GetValue(string sql);
    }
    //
    public interface IDbRecord
    {
        IDbEngine DbEngine { get; }
        string TableName { get;}
        void Insert();
        void Delete();
        void Update();
    }
}

这是模拟的数据库操作组件的两个接口类型,这里进行了一些简化,完整的组件设计思路可以参考作者的个人网站相关内容。

IDbEngine组件用于数据库的基本操作,成员包括:

  • CnnStr只读属性,表示数据库连接字符串。
  • Type只读属性,表示数据库类型,如SQL Server、MySQL等。
  • Connected只读属性,表示数据库是否能够正确连接。
  • GetValue()方法,用于执行SQL语句,并返回一个值。

IDbRecord组件用于数据表的记录操作,成员包括:

  • DbEngine属性,表示数据库引擎类型,即上述定义的IDbEngine类型。
  • TableName属性,表示所操作的数据表名称。
  • Insert()方法,用于在数据表中插入新记录。
  • Delete()方法,用于在数据表删除数据。
  • Update()方法,用于更新数据表记录。

数据库操作在很多软件系统中是非常重要的组成部分,而简化数据库操作代码也是开发者需要考虑的问题,IDbEngine和IDbRecord接口组件就是为了能够使用统一的代码操作多种数据库,从而简化数据操作代码,提高软件开发效率。

开发中,我们并不能创建接口类型的对象,而是需要真正的类来创建对象,下面的代码,我们创建了CSqlEngine和CMySqlEngine类,分别完成SQL Server数据库和MySQL数据库的基本操作。

//
public class CSqlEgnine : IDbEngine
{
    public string CnnStr
    {
        get { return "SQL Server Connection String"; }
    }
    public string Type
    {
        get { return "SQL Server"; }
    }
    public bool Connected
    {
        get { return true; }
    }
    public object GetValue(string sql)
    {
        return "来自SQL Server数据库的值";
    }
}
//
public class CMySqlEgnine : IDbEngine
{
    public string CnnStr
    {
        get { return "MySQL Connection String"; }
    }
    public string Type
    {
        get { return "MySQL"; }
    }
    public bool Connected
    {
        get { return true; }
    }
    public object GetValue(string sql)
    {
        return "来自MySQL数据库的值";
    }
}

下面的代码,我们创建CDbRecord类,其实现了IDbRecord接口。

public class CDbRecord : IDbRecord
{
    // 构造函数
    public CDbRecord(IDbEngine dbe,string sTableName)
    {
        DbEngine = dbe;
        TableName = sTableName;
    }
    //
    public IDbEngine DbEngine { get; private set; }
    //
    public string TableName { get; private set; }
    //
    public void Insert()
    {
        Console.WriteLine("插入{0}数据库{1}表的记录",
            DbEngine.Type, TableName);
    }
    //
    public void Delete()
    {
        Console.WriteLine("删除{0}数据库{1}表的记录",
            DbEngine.Type, TableName);
    }
    //
    public void Update()
    {
        Console.WriteLine("更新{0}数据库{1}表的记录",
            DbEngine.Type, TableName);
    }
}

下面的代码演示了如何通过接口类型灵活完成不同类型数据库中数据记录的操作。

static void Main(string[] args)
{
        // 操作SQL Server
        IDbEngine dbe = new CSqlEgnine();
        IDbRecord rec = new CDbRecord(dbe, "UserMain");
        rec.Insert();
        rec.Delete();
        rec.Update();
        // 操作MySQL数据库
        dbe = new CMySqlEgnine();
        rec = new CDbRecord(dbe, "UserMain");
        rec.Insert();
        rec.Delete();
        rec.Update();
}

代码中首先创建了SQL Server数据库操作引擎dbe对象,它定义为IDbEngine接口;接下来,创建了操作SQL Server数据库的rec对象,其声明为IDbRecord类型,实例化为CDbRecord类;然后,将dbe实例化为操作MySQL数据库的CMySqlEngine类型对象,使用相同的代码创建重新创建了CDbRecord对象,并IDbRecord接口中相同的Insert()、Delete()和Update()方法完成了不同的数据库中的记录操作。执行结果如下图所示。

enter image description here

在这个示例中,只需要修改IDbEngine对象的定义就可以使用相同的代码来操作数据记录(IDbRecord组件);一方面,如果项目中需要使用不同的数据库,可以很方便的切换;另一方面,可以大量地简化数据操作代码,从而将注意力更多地放在主要的业务和软件功能上。

当然,如果要实现完整的数据库操作,还需要大量的代码,在后续的课程中会讨论SQL Server数据库的基础应用,而更多关于数据库和数据操作组件的内容可以参考作者个人网站中的相关信息。

接下来,我们讨论一些在C#中使用接口时需要注意的问题。

首先来看一下接口的继承问题。类在继承时只能指定一个父类,而接口则不同,我们可以让一个接口继承多个接口,也可以让一个类实现多个接口。先来看下面的代码。

//
public interface I1
{
    void DoWork1();
}
//
public interface I2
{
    void DoWork2();
}
//
public interface I3 : I1, I2
{

}
//
public class C3 : I3
{
    public void DoWork1()
    {
        Console.WriteLine("I1.DoWork1");
    }
    //
    public void DoWork2()
    {
        Console.WriteLine("I2.DoWork2");
    }
}

代码,I3接口继承了I1和I2接口,这样,在C3类实现I3接口时,就必须实现DoWork1()和DoWork2()两个方法;下面的代码演示了这几个接口和C3类的应用。

static void Main(string[] args)
    {
        I3 c3 = new C3();
        c3.DoWork1();
        c3.DoWork2();
        //
        (c3 as I1).DoWork1();
        (c3 as I2).DoWork2();
    }

代码执行结果如下图所示。

enter image description here

现在的问题是,如果类实现的多个接口中有同名的成员时应该如何处理,如下面的代码。

//
public interface I1a
{
    void DoWork();
}
//
public interface I1b
{
    void DoWork();
}
//
public class C1 : I1a,I1b
{
    public void DoWork()
    {
        Console.WriteLine("I1a.DoWork");
    }
}

如果两个接口中同名成员的功能是相同的,就可以只在类中实现一个方法,如上述代码一样,下面的代码演示了C1类的使用。

static void Main(string[] args)
{
        C1 c1 = new C1();
        c1.DoWork();
}

代码执行结果如下图所示。

enter image description here

那么,如果I1a和I1b接口中的DoWork()方法功能不同该怎么办呢?答案是使用一个不同的成员来实现,如下面的代码,我们在C1类中添加一个方法,并指定其实现的是哪个接口中的哪个方法。

public class C1 : I1a, I1b
{
    public void DoWork()
    {
        Console.WriteLine("I1a.DoWork");
    }
    //
    void I1b.DoWork()
    {
        Console.WriteLine("I1b.DoWork");
    }
}

这里,默认实现的是I1a接口中的DoWork()方法,而I1b接口中的DoWork()方法就必须显示调用。下面的代码演示了这两个方法的使用。

static void Main(string[] args)
{
        C1 c1 = new C1();
        c1.DoWork();
        (c1 as I1b).DoWork();
}

代码执行结果如下图所示。

enter image description here

实际应用中,还可以将所有同名成员都使用接口进行限定,只是需要注意在定义这些成员时不需要使用public等修饰符,因为作为接口成员,它们默认都应用是public访问级别。

接口的应用,为代码组合带来极大的灵活性,大家可以在学习和工作中逐步掌握其强大功能,并能够灵活、高效地应用。

CHY软件小屋原创作品!