前言

以下我将通过一种全新的方式带你理解到底什么才是接口多态,很多人包括我在内刚学习这个东西的时候感觉无从下手,别人讲不明白,自己也搞不懂,只单纯的知道如何写接口,之后就再也没有用过。以下我将通过问题引入的方式来带你理解接口。首先你要知道一个概念的提出或者一个功能的引入一定是为了解决某个问题的。那么接口解决了什么问题,为什么它如此受推崇。以下我们先从生活中来理解它,逐步由现实到抽象。我希望以最为通俗的说法来进行解释。

从生活中看接口

这里我们以最常见的手机的充电接口为例,大家要知道这个接口不光能够充电,而且还能进行文件传输、网络共享、并且和耳机、U盘等设备都可以进行交互和通信,当然它的功能不仅限于我刚才说的这些,还有好多好多,并且我们也可以对其进行编程。它是如何实现的?这个东西其实蛮有意思的。

这个接口其实规定了一些数据传输的标准,那么什么又是数据传输标准呢?你首先要想:我们用这个接口干嘛?我们将刚才说的文件共享、耳机等所做的事情做进一步的概括那就是:与手机进行交流。那么什么是交流,我们从现实生活中理解的话就是人与人之间对话,对话需要什么?是不是需要语言?那么语言的本质是什么?是不是就是一个协定呀?也就是一个标准!如何理解?例如我给你说苹果,你是不是会立马想到真实的苹果,那如果我说pomme你能想到苹果吗?是不是不能,因为苹果这个字是我们之间沟通/通信建立的一个标准,如果你想和我沟通就必须遵循这个标准,否则我们无法沟通,所以说接口建立的是一个数据传输的标准。当然我们可以借助一些适配器来完成接口的转换,例如刚才说的U盘想要插到手机上是不大可能的,所以就需要一个适配器,它所承担的就是一个翻译的工作。

以下我们以最为常见的数据访问接口举例说明。

常见的接口“数据访问层接口”

什么叫做“数据访问层接口”呢?这里我们先不直接来介绍,我们先来看一个例子。

我们首先有一个Student类,再者有一个StudentService的类,其中Student类用于保存和传递数据,StudentService类则提供两个方法第一个就是将Student对象中的数据保存到SQLServer数据库中,第二个方法GetStudents则是将SQLServer数据库中所有数据读取来并保存然后将数据返回。这里我们忽略具体代码,仅写出了大致步骤。

public class Student
{
    public int StudentId{ get; set; }
    public string StudentName{ get; set; }
    public int Age { get; set; }
}
public class StudentService
{
    public int Add(Entities.Student student)
    {
        //拼接SQL语句

        //创建SQLConnection对象并打开数据库链接

        //创建CMD对象执行SQL语句

        //接收并返回操作结果
        return 1;
    }
    public List<Entities.Student> GetStudents()
    {
        //创建List对象集合
        List<Entities.Student> students = new List<Entities.Student>();

        //拼接SQL语句

        //创建SQLConnection对象并打开数据库链接

        //创建CMD对象执行SQL语句

        //读取返回结果并保存到students对象中

        //返回读取结果
        return students;
    }
}

接下来我们来模仿用户输入,将数据进行保存操作。以下我们创建了student对象,以及studentService对象,然后调用studentService的Add函数成功的将数据进行了保存。

static void Main(string[] args)
{
    //1、获取用户输入数据(这里跳过获取)
    int studentId = 1000;
    string studentName = "QyLost";
    int age = 18;
    //2、创建Student对象的实例
    Entities.Student student = new Entities.Student();
    student.StudentId = studentId;
    student.StudentName = studentName;
    student.Age = age;
    //3、创建StudentService对象实例
    DAL.StudentService studentService = new DAL.StudentService();
    //4、调用studentService对象的Add方法并接收结果
    bool result = studentService.Add(student) == 1;
    if (result)
    {
        Console.WriteLine("Success");
    }
    else
    {
        Console.WriteLine("Failed");
    }
    Console.ReadKey();
}

这样写起来貌似看着很是合理,但是你想一下,如果说我们需求发生了改变,由于某种原因(可能是客户原因或者技术原因)我们需要将数据库切换为json/xml文件保存。这下你能想到什么?改代码?没错你肯定先去改了代码,数据保存的地方变了,那么你肯定要去修改StudentService中的两个函数了,于是就有了下边的代码。

其他地方没有改动,只是修改了Add和GetStudents函数中的所有代码,这样就完成了需求的变更。

public int Add(Entities.Student student)
{
    //获取文件路径

    //将对象转换位特定格式(json/xml/.....)

    //创建文件流对象并打开操作流

    //追加文件内容

    //关闭文件流

    return 1;
}
public List<Entities.Student> GetStudents()
{
    //创建List对象集合
    List<Entities.Student> students = new List<Entities.Student>();

    //获取文件路径

    //创建文件流对象并打开操作流

    //解析文本数据(json/xml/.....)

    //读取解析结果并保存到students对象中

    //返回读取结果
    return students;
}

需求总是变换莫测,随着时间的推移,突然那么一天又要将所有数据迁移到MySQL数据库,然后你默默的咬牙将StudentService类中的两个方法进行了改动,出现了如下版本。

public int Add(Entities.Student student)
{
    //拼接SQL语句

    //创建MySQL数据链接对象并打开数据库链接

    //创建MySQLCMD对象执行SQL语句

    //接收并返回操作结果
    return 1;
}
public List<Entities.Student> GetStudents()
{
    //创建List对象集合
    List<Entities.Student> students = new List<Entities.Student>();

    //拼接SQL语句

    //创建MySQL数据链接对象并打开数据库链接

    //创建MySQLCMD对象执行SQL语句

    //读取返回结果并保存到students对象中

    //返回读取结果
    return students;
}

然后最后绕了一圈发现还是最开始的那个好用,又要求换回来,回头看代码,已经迭代了好几个版本,回头再看你,已准备好了"跳楼"。

从上边这个小故事中我们来进行以下总结,我们发现数据的存储发生了三次的改变,以及最后由于某种不确定性最终还是返回了第一个版本。总结来说问题就是在于数据存储类型的不确定性。这些原因可能是技术原因也可能是客户需求的原因所造成,那么面对这种问题是否存在一个较为好的办法呢?当然是有的。

我们分析一下,刚才三次的修改中都修改了什么?是不是我们只是修改了StudentService类中的Add以及GetStudents函数的内容,其他地方并没有变动对吧!

于是我们就将操作提取成为了接口。改成接口有什么好处呢?它可以让我们在刚才三种实现进行切换。

public interface IStudentService
{
    int Add(Entities.Student student);
    List<Entities.Student> GetStudents();
}

这里我们创建了三个类分别是StudentServiceSQLServerStudentServiceFileStudentServiceMySQL,然后让它们实现IStudentService这个接口,并将具体实现放在它们各自的Add和GetStudents中。

public class StudentServiceSQLServer: IStudentService
{
    public int Add(Entities.Student student)
    {
        //拼接SQL语句

        //创建SQLConnection对象并打开数据库链接

        //创建CMD对象执行SQL语句

        //接收并返回操作结果
        return 1;
    }
    public List<Entities.Student> GetStudents()
    {
        //创建List对象集合
        List<Entities.Student> students = new List<Entities.Student>();

        //拼接SQL语句

        //创建SQLConnection对象并打开数据库链接

        //创建CMD对象执行SQL语句

        //读取返回结果并保存到students对象中

        //返回读取结果
        return students;
    }
}

public class StudentServiceFile : IStudentService
{
    public int Add(Entities.Student student)
    {
        //获取文件路径

        //将对象转换位特定格式(json/xml/.....)

        //创建文件流对象并打开操作流

        //追加文件内容

        //关闭文件流

        return 1;
    }
    public List<Entities.Student> GetStudents()
    {
        //创建List对象集合
        List<Entities.Student> students = new List<Entities.Student>();

        //获取文件路径

        //创建文件流对象并打开操作流

        //解析文本数据(json/xml/.....)

        //读取解析结果并保存到students对象中

        //返回读取结果
        return students;
    }
}

public class StudentServiceMySQL : IStudentService
{
    public int Add(Entities.Student student)
    {
        //拼接SQL语句

        //创建MySQL数据链接对象并打开数据库链接

        //创建MySQLCMD对象执行SQL语句

        //接收并返回操作结果
        return 1;
    }
    public List<Entities.Student> GetStudents()
    {
        //创建List对象集合
        List<Entities.Student> students = new List<Entities.Student>();

        //拼接SQL语句

        //创建MySQL数据链接对象并打开数据库链接

        //创建MySQLCMD对象执行SQL语句

        //读取返回结果并保存到students对象中

        //返回读取结果
        return students;
    }
}

这样我们就有三个可供选择的数据存储方式了,如果存储方式发生了变化我们仅仅需要更改对象的实例即可,调用依然不会有改变,代码如下:

1、如果需要SQLServer保存数据

//3、创建StudentService对象实例
DAL.IStudentService studentService = new DAL.StudentServiceSQLServer();
//4、调用studentService对象的Add方法并接收结果
bool result = studentService.Add(student) == 1;

2、如果需要文件保存数据

//3、创建StudentService对象实例
DAL.IStudentService studentService = new DAL.StudentServiceFile();
//4、调用studentService对象的Add方法并接收结果
bool result = studentService.Add(student) == 1;

3、如果需要MySQL保存数据

//3、创建StudentService对象实例
DAL.IStudentService studentService = new DAL.StudentServiceMySQL();
//4、调用studentService对象的Add方法并接收结果
bool result = studentService.Add(student) == 1;

这样我们就解决了在数据存储方式改变所带来的大量返工,这也体现了面向对象的开闭原则(开放封闭原则),即对添加是开放的对修改是封闭的,通俗来说就是当需求变更时我们尽量少的修改代码,而最好是新增代码来满足我们的需求。

以上虽然解决了数据存储方式的切换问题,但是还有一个问题,就是说当我们数据存储方式改变时还是需要去通过new的方式创建一个具体的实现类,然后重新编译。这里我们只是一个地方调用了,如果说有上百个地方调用呢,是不是要修改上百次,这显然是不行的。你想呀问题出现在什么地方?是不是有多个地方都会创建这个IStudentService的实现类呀,那我们直接让这个类都在一个地方创建不就解决了嘛,然后就有了如下的这个类出现。

public class StudentServiceFactory
{
    //数据存储类型(1:SQLServer,2、File,3、MySQL),通常会保存在App.config文件,然后在程序运行时读取
    private static int storeType = 0;

    public static IStudentService GetStudentService()
    {
        DAL.IStudentService studentService = null;
        switch (storeType)
        {
            case 1:
                studentService = new DAL.StudentServiceSQLServer();
                break;
            case 2:
                studentService = new DAL.StudentServiceFile();
                break;
            case 3:
                studentService = new DAL.StudentServiceMySQL();
                break;
        }
        return studentService;
    }
}

StudentServiceFactory中我们定义了一个成员变量storeType来表示我们当前使用的存储类型,只需要一个就行了,因为针对我们目前的需求来说,同一版本只会使用某一种存储类型。然后在所有地方的调用只要写成如下代码即可:

//3、创建StudentService对象实例
DAL.IStudentService studentService = DAL.StudentServiceFactory.GetStudentService();
//4、调用studentService对象的Add方法并接收结果
bool result = studentService.Add(student) == 1;

这样我们无论多少个地方用到同时调用只需要修改StudentServiceFactory中的代码即可,这也体现了面向对象的封装性,到这里还木有完,在深思一下StudentServiceFactory中的代码,你会发现GetStudentService函数存在了三个逻辑判断,那么我们要增加第四种是不是又要修改代码?答案是肯定的,那么有没有什么技术可以实现无编译替换呢,那么我们就要使用反射技术了。然后修改为如下代码:

public class StudentServiceFactory
{
    //数据存储类型(1:SQLServer,2、File,3、MySQL),通常会保存在App.config文件,然后在程序运行时读取
    private static string storeType = "InterfaceDemo.DAL.StudentServiceSQLServer";

    public static IStudentService GetStudentService()
    {
        DAL.IStudentService studentService = (DAL.IStudentService)System.Reflection.Assembly.Load("InterfaceDemo").CreateInstance(storeType);
        return studentService;
    }
}

到此就可以实现无编译动态替换了,我们可以在配置文件中存储类的完全限定名(命名空间+类名),然后通过反射技术在运行时进行动态替换。

以上就是接口多态+工厂,所谓工厂就是生产某些对象实例的特定类或者方法。

小总

以上只不过是接口的一种用途而已,如果想要灵活使用,那么你就必须要知道接口的本质到底是什么,接口的本质就是一种“协议”或者说叫做“协定”,什么是“协议”和“协定”?说白了就是一个“约定”,大家都必须遵守这个约定,它具有强制性。

最后

原创内容,转载请标明来源!

代码下载:点击下载


qylost
3 声望2 粉丝

水之积也不厚,则其负大舟也无力;风之积也不厚,则其负大翼也无力。