See through interface abstraction and factory

Preface

I'm going to take you through a new way to understand what interface polymorphism is. Many people, including me, feel helpless when they first learn this thing. They can't understand it. They can't understand it. They just know how to write interfaces, and then they won't be useful. I'll show you how to understand the interface through question introduction. First of all, you need to know that a concept or a function must be introduced to solve a problem. So what's the problem with the interface and why it's so popular. Now let's understand it from life, and gradually from reality to abstraction. I want to explain it in the most popular way.

Interface in life

Here we take the charging interface of the most common mobile phone as an example. You should know that this interface not only can charge, but also can carry out file transmission, network sharing, and can interact and communicate with headphones, U SB flash drives and other devices. Of course, its functions are not limited to what I just said, there are many and many, and we can also program it. How is it implemented? This thing is actually quite interesting.

This interface actually specifies some data transmission standards, so what is the data transmission standard? The first thing you need to think about is: what do we use this interface for? We will make a further generalization of what we have just said about file sharing, headphones, etc. that is: communication with mobile phones. So what is communication? What do we understand from real life is dialogue between people? What does dialogue need? Do you need language? So what is the nature of language? Is it an agreement? That's a standard! How to understand? For example, if I say apple to you, will you immediately think of the real apple? If I say pomme, can you think of apple? Can't, because Apple is a standard for communication between us. If you want to communicate with me, you must follow this standard, otherwise we can't communicate, so the interface is a standard for data transmission. Of course, we can use some adapters to complete the interface conversion. For example, the U disk just mentioned is unlikely to be plugged into the mobile phone, so we need an adapter, which undertakes the work of translation.

The following is an example of the most common data provider.

Common interface "data access layer interface"

What is the data access layer interface? Let's not introduce it directly here. Let's take an example first.

We first have a Student class, and then a StudentService class. The Student class is used to save and transfer data. The StudentService class provides two methods. The first method is to save the data in the Student object to the SQL Server database. The second method, GetStudents, is to read and save all the data in the SQL Server database, and then return the data. Here we ignore the specific code and only write the general steps.

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)
    {
        //Splicing SQL statements

        //Create SQLConnection object and open database link

        //Create CMD object to execute SQL statement

        //Receive and return operation results
        return 1;
    }
    public List<Entities.Student> GetStudents()
    {
        //Create a List object collection
        List<Entities.Student> students = new List<Entities.Student>();

        //Splicing SQL statements

        //Create SQLConnection object and open database link

        //Create CMD object to execute SQL statement

        //Read the returned results and save them to the students object

        //Return read result
        return students;
    }
}

Next, we will simulate user input and save the data. Below we created the student object and the studentService object, and then called the Add function of studentService to save the data successfully.

static void Main(string[] args)
{
    //1. Get user input data (skip to get here)
    int studentId = 1000;
    string studentName = "QyLost";
    int age = 18;
    //2. Create an instance of Student object
    Entities.Student student = new Entities.Student();
    student.StudentId = studentId;
    student.StudentName = studentName;
    student.Age = age;
    //3. Create an instance of StudentService object
    DAL.StudentService studentService = new DAL.StudentService();
    //4. Call the Add method of the studentService object and receive the result
    bool result = studentService.Add(student) == 1;
    if (result)
    {
        Console.WriteLine("Success");
    }
    else
    {
        Console.WriteLine("Failed");
    }
    Console.ReadKey();
}

It seems reasonable to write like this, but you can think that if our requirements have changed, we need to switch the database to json/xml file for some reason (maybe due to customer or technical reasons). What can you think of? Change code? Yes, you must change the code first, and the data storage place has changed. Then you must change the two functions in StudentService, so you have the code below.

There are no other changes, just all the code in the Add and GetStudents functions, which completes the change of requirements.

public int Add(Entities.Student student)
{
    //Get file path

    //Convert object to bit specific format (json/xml /...)

    //Create a file flow object and open the operation flow

    //Append file content

    //Close file stream

    return 1;
}
public List<Entities.Student> GetStudents()
{
    //Create a List object collection
    List<Entities.Student> students = new List<Entities.Student>();

    //Get file path

    //Create a file flow object and open the operation flow

    //Parsing text data (json/xml /...)

    //Read the parsing results and save them to the students object

    //Return read result
    return students;
}

Requirements are always unpredictable. As time goes on, all of a sudden, all the data will be migrated to MySQL database one day. Then you silently change the two methods in StudentService class. The following version appears.

public int Add(Entities.Student student)
{
    //Splicing SQL statements

    //Create MySQL data link object and open database link

    //Create MySQL CMD object to execute SQL statement

    //Receive and return operation results
    return 1;
}
public List<Entities.Student> GetStudents()
{
    //Create a List object collection
    List<Entities.Student> students = new List<Entities.Student>();

    //Splicing SQL statements

    //Create MySQL data link object and open database link

    //Create MySQL CMD object to execute SQL statement

    //Read the returned results and save them to the students object

    //Return read result
    return students;
}

Then I finally went around and found that it was the first one that was easy to use. I asked to change it back. Looking back at the code, I have iterated several versions. Looking back at you, I am ready to "jump".

From the above story, we can summarize that the data storage has changed three times and finally returned to the first version due to some uncertainty. In summary, the problem lies in the uncertainty of data storage type. These reasons may be caused by technical reasons or customer needs, so is there a better way to face this problem? Of course there are.

Let's analyze what has been revised in the three previous revisions? Are we just modifying the content of Add and GetStudents function in StudentService class? There is no change in other places, right!

So we extract the operation as an interface. What's the benefit of changing to an interface? It allows us to switch between the three implementations just now.

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

Here we create three classes: StudentServiceSQLServer, StudentServiceFile, StudentServiceMySQL, and let them implement the IStudentService interface, and put the specific implementation in their respective Add and GetStudents.

public class StudentServiceSQLServer: IStudentService
{
    public int Add(Entities.Student student)
    {
        //Splicing SQL statements

        //Create SQLConnection object and open database link

        //Create CMD object to execute SQL statement

        //Receive and return operation results
        return 1;
    }
    public List<Entities.Student> GetStudents()
    {
        //Create a List object collection
        List<Entities.Student> students = new List<Entities.Student>();

        //Splicing SQL statements

        //Create SQLConnection object and open database link

        //Create CMD object to execute SQL statement

        //Read the returned results and save them to the students object

        //Return read result
        return students;
    }
}

public class StudentServiceFile : IStudentService
{
    public int Add(Entities.Student student)
    {
        //Get file path

        //Convert object to bit specific format (json/xml /...)

        //Create a file flow object and open the operation flow

        //Append file content

        //Close file stream

        return 1;
    }
    public List<Entities.Student> GetStudents()
    {
        //Create a List object collection
        List<Entities.Student> students = new List<Entities.Student>();

        //Get file path

        //Create a file flow object and open the operation flow

        //Parsing text data (json/xml /...)

        //Read the parsing results and save them to the students object

        //Return read result
        return students;
    }
}

public class StudentServiceMySQL : IStudentService
{
    public int Add(Entities.Student student)
    {
        //Splicing SQL statements

        //Create MySQL data link object and open database link

        //Create MySQL CMD object to execute SQL statement

        //Receive and return operation results
        return 1;
    }
    public List<Entities.Student> GetStudents()
    {
        //Create a List object collection
        List<Entities.Student> students = new List<Entities.Student>();

        //Splicing SQL statements

        //Create MySQL data link object and open database link

        //Create MySQL CMD object to execute SQL statement

        //Read the returned results and save them to the students object

        //Return read result
        return students;
    }
}

In this way, we have three alternative data storage methods. If the storage method changes, we only need to change the instance of the object, and the call will not change. The code is as follows:

1. If SQL server is required to save data

//3. Create an instance of StudentService object
DAL.IStudentService studentService = new DAL.StudentServiceSQLServer();
//4. Call the Add method of the studentService object and receive the result
bool result = studentService.Add(student) == 1;

2. Save data in files if necessary

//3. Create an instance of StudentService object
DAL.IStudentService studentService = new DAL.StudentServiceFile();
//4. Call the Add method of the studentService object and receive the result
bool result = studentService.Add(student) == 1;

3. If MySQL is needed to save data

//3. Create an instance of StudentService object
DAL.IStudentService studentService = new DAL.StudentServiceMySQL();
//4. Call the Add method of the studentService object and receive the result
bool result = studentService.Add(student) == 1;

In this way, we have solved a lot of rework caused by the change of data storage mode, which also embodies the open and closed principle of object-oriented (open and closed principle), that is, to add is open and to modify is closed, generally speaking, when the requirements change, we modify the code as little as possible, and the best way is to add new code to meet our needs.

Although the above solves the problem of data storage mode switching, there is another problem, that is, when we change the data storage mode, we still need to create a specific implementation class through the new method, and then recompile. Here we are just one local call. If there are hundreds of local calls, is it necessary to modify them hundreds of times? This is obviously not possible. What do you think is the problem? There are many places where the implementation class of IStudentService will be created. Let's just let this class be created in one place. Then the following class will appear.

public class StudentServiceFactory
{
    //Data storage type (1: SQLServer, 2, File, 3, MySQL), usually saved in App.config File, and then read when the program is running
    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;
    }
}

In StudentServiceFactory, we define a member variable, storeType, to represent the storage type we currently use. Only one is needed, because for our current requirements, only one storage type will be used in the same version. Then the call in all places can be written as follows:

//3. Create an instance of StudentService object
DAL.IStudentService studentService = DAL.StudentServiceFactory.GetStudentService();
//4. Call the Add method of the studentService object and receive the result
bool result = studentService.Add(student) == 1;

In this way, no matter how many places we need to call at the same time, we only need to modify the code in StudentServiceFactory, which also reflects the encapsulation of object-oriented. So far, we have not finished. After thinking about the code in StudentServiceFactory, you will find that there are three logical judgments in GetStudentService function, so we need to add the fourth one to modify the code ? The answer is yes, so is there any technology that can realize no compile replacement? Then we will use reflection technology. Then change to the following code:

public class StudentServiceFactory
{
    //Data storage type (1: SQLServer, 2, File, 3, MySQL), usually saved in App.config File, and then read when the program is running
    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;
    }
}

At this point, we can realize dynamic replacement without compilation. We can store the fully qualified name (namespace + class name) of the class in the configuration file, and then use reflection technology to dynamically replace it at runtime.

The above is interface polymorphism + factory. Factory is a specific class or method that produces some object instances.

Small total

The above is just a purpose of the interface. If you want to use it flexibly, you must know what the essence of the interface is. The essence of the interface is a kind of "Protocol" or "agreement", what is "Protocol" and "agreement"? To put it bluntly, it is a "Convention", which everyone must abide by. It is mandatory.

Last

Original content, reprint please indicate the source!

Code download: Click to download

Keywords: C# SQL Database MySQL JSON

Added by gmdavis on Wed, 20 Nov 2019 15:09:48 +0200