65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 中的流式接口模式 - 带有继承问题

starIconstarIconstarIconstarIconstarIcon

5.00/5 (24投票s)

2022年3月2日

MIT

4分钟阅读

viewsIcon

29734

downloadIcon

187

关于 C# 中流式接口模式的教程文章

流畅接口模式 – 简介

流畅接口模式是一种面向对象语言的设计指导原则,它建议暴露的 API,也就是类 `public` 方法,为了提高可读性,应该尝试“表现”得像一种特定领域的语言 (DSL)。创建 DSL 的主要工具被建议为“方法链式调用”。我们希望拥有像这样的接口 (API)

Employee empl = new Employee();
empl.SetFirstName("John").SetLastName("Smith").SetAge(30).Print();

在 C# 中,就像许多面向对象语言一样,方法链式调用是通过返回对象本身,即“this”对象,作为方法的返回值来实现的。这使得下一个方法可以链接到前一个方法的返回值。从抽象层面来看,我们实际上是将一个领域上下文返回给方法链式调用中的后续方法。方法链式调用以返回 `void` 上下文的方法结束。

流畅接口模式只关心接口应该是什么样的,它并没有规定实现它的经典模式。只要你的接口形式是方法链式调用,看起来像自然语言,因此被认为是“用户友好”的,它就认为你创建了自己形式的“特定领域语言 (DSL)”,并且对此感到满意。

经典方法 - Setter - 示例代码

这里是一个使用标准 Setter 的经典代码示例。这种编程方式在面向对象和 C# 文献中被广泛推荐。

public class Employee
{
    private string FirstName = null;
    private string LastName = null;
    private int Age = 0;

    public void SetFirstName(string fName)
    {
        FirstName = fName;
    }

    public void SetLastName(string lName)
    {
        LastName = lName;
    }

    public void SetAge(int age)
    {
        Age = age;
    }

    public void Print()
    {
        string tmp = String.Format("FirstName:{0}; LastName:{1}; Age:{2}", 
             FirstName, LastName, Age);
        Console.WriteLine(tmp);
    }
}
class Client
{
    static void Main(string[] args)
    {
        Employee empl = new Employee();
        empl.SetFirstName("John");
        empl.SetLastName("Smith");
        empl.SetAge(30);
        empl.Print();

        Console.ReadLine();
    }
}

这是执行结果

FirstName:John; LastName:Smith; Age:30

流畅接口 - 方法链式调用 - 示例代码

这里是带有方法链式调用的代码示例。这基本上是上面示例中的同一个类,遵循了流畅接口模式的建议。这里应用的主要技巧是返回“this”对象,以便可以链式调用方法。

public class Employee
{
    private string FirstName = null;
    private string LastName = null;
    private int Age = 0;

    public Employee SetFirstName(string fName)
    {
        FirstName = fName;
        return this;
    }

    public Employee SetLastName(string lName)
    {
        LastName = lName;
        return this;
    }

    public Employee SetAge(int age)
    {
        Age = age;
        return this;
    }

    public void Print()
    {
        string tmp = String.Format("FirstName:{0}; LastName:{1}; Age:{2}", 
           FirstName, LastName, Age);
        Console.WriteLine(tmp);
    }
}

class Client
{
    static void Main(string[] args)
    {
        Employee empl = new Employee();
        empl.SetFirstName("John").SetLastName("Smith").SetAge(30).Print();

        Console.ReadLine();
    }
}

这是执行结果

FirstName:John; LastName:Smith; Age:30

流畅接口 - 使用扩展方法进行方法链式调用 - 示例代码

当然,我们可以在 C# 中使用扩展方法来实现与上面相同的结果。请注意,我们需要将 `Employee` 属性的访问级别更改为 `public`,以便扩展方法可以使用它们。

public class Employee
{
    public string FirstName = null;
    public string LastName = null;
    public int Age = 0;
}

public static class EmployeeExtensions
{
    public static Employee SetFirstName(this Employee emp, string fName)
    {
        emp.FirstName = fName;
        return emp;
    }

    public static Employee SetLastName(this Employee emp, string lName)
    {
        emp.LastName = lName;
        return emp;
    }

    public static Employee SetAge(this Employee emp, int age)
    {
        emp.Age = age;
        return emp;
    }

    public static void Print(this Employee emp)
    {
        string tmp = String.Format("FirstName:{0}; LastName:{1}; Age:{2}", 
          emp.FirstName, emp.LastName, emp.Age);
        Console.WriteLine(tmp);
    }
}

class Client
{
    static void Main(string[] args)
    {
        Employee empl = new Employee();
        empl.SetFirstName("John").SetLastName("Smith").SetAge(30).Print();

        Console.ReadLine();
    }
}

这是执行结果

FirstName:John; LastName:Smith; Age:30

分层流畅接口

如前所述,流畅接口模式将如何实现其特定版本的“特定领域语言 (DSL)”的决定权留给程序员及其判断。程序员可以自由选择方法名称和方法链式调用的组织方式,以使其看起来最“用户友好”并类似于自然语言。

一个典型的用例是创建分层流畅接口。在这种情况下,程序员决定将属性分组到分层组中,并以此方式将其暴露给库/类的用户。例如,程序员可能会决定将 `Employee` 数据分为两组

  1. PersonalData;以及
  2. EmploymentData

这将导致一个像这样的接口

Employee empl = new Employee();

empl.Fluent
    .PersonalData
        .FirstName("John").LastName("Smith").Age(30)
    .EmploymentData
        .Company("CNN").Position("Host").Salary(50000);

我们将在此提供一个创建此类接口的示例项目。这不一定是程序员为实现分层流畅接口应遵循的模式。如果程序员愿意,可以自由选择自己的实现技术。但是,这提供了一个良好且可重用的示例代码。我们不会深入探讨该项目的设计/实现细节,因为大部分内容是不言自明的。

这是类图

这是分层流畅接口的代码

public class Employee
{
    //PersonalData
    public string FirstName = null;
    public string LastName = null;
    public int Age = 0;

    //EmploymentData
    public string Company = null;
    public string Position = null;
    public int Salary = 0;

    public FluentEmployee Fluent
    {
        get { return new FluentEmployee(this); }
    }

    public override string ToString()
    {
        string tmp = String.Format("FirstName:{0}; LastName:{1}; Age:{2} 
              \nCompany:{3}; Position:{4}; Salary:{5}",
            this.FirstName, this.LastName, this.Age, this.Company, 
            this.Position, this.Salary);
        return tmp;
    }
}

public class FluentEmployee
{
    protected Employee _employee = null;

    public FluentEmployee()
    {
        _employee = new Employee();
    }

    public FluentEmployee(Employee emp)
    {
        _employee = emp;
    }

    public FluentEmployeePersonalData PersonalData
    {
        get { return new FluentEmployeePersonalData(_employee); }
    }

    public FluentEmployeeEmploymentData EmploymentData
    {
        get { return new FluentEmployeeEmploymentData(_employee); }
    }
}

public class FluentEmployeeEmploymentData : FluentEmployee
{
    public FluentEmployeeEmploymentData(Employee emp)
        : base(emp)
    {
    }

    public FluentEmployeeEmploymentData Company(string comp)
    {
        _employee.Company = comp;
        return this;
    }

    public FluentEmployeeEmploymentData Position(string pos)
    {
        _employee.Position = pos;
        return this;
    }

    public FluentEmployeeEmploymentData Salary(int sal)
    {
        _employee.Salary = sal;
        return this;
    }
}

public class FluentEmployeePersonalData : FluentEmployee
{
    public FluentEmployeePersonalData(Employee emp)
        : base(emp)
    {
    }

    public FluentEmployeePersonalData FirstName(string fName)
    {
        _employee.FirstName = fName;
        return this;
    }

    public FluentEmployeePersonalData LastName(string lName)
    {
        _employee.LastName = lName;
        return this;
    }

    public FluentEmployeePersonalData Age(int age)
    {
        _employee.Age = age;
        return this;
    }
}

class Client
{
    static void Main(string[] args)
    {
        Employee empl = new Employee();

        empl.Fluent
            .PersonalData
                .FirstName("John").LastName("Smith").Age(30)
            .EmploymentData
                .Company("CNN").Position("Host").Salary(50000);

        Console.WriteLine(empl.ToString());
        Console.ReadLine();
    }
}

这是执行结果

FirstName:John; LastName:Smith; Age:30
Company:CNN; Position:Host; Salary:50000

继承流畅接口类

当我们要继承实现流畅接口的类时,问题就出现了。假设我们有一个 `Employee` 类和一个继承自它的 `Manager` 类。类图如下

这是代码

public class Employee
{
    protected string FirstName = null;
    protected string LastName = null;
    protected int Age = 0;

    public Employee SetFirstName(string fName)
    {
        FirstName = fName;
        return this;
    }

    public Employee SetLastName(string lName)
    {
        LastName = lName;
        return this;
    }

    public Employee SetAge(int age)
    {
        Age = age;
        return this;
    }

    public override string ToString()
    {
        string tmp = String.Format("FirstName:{0}; LastName:{1}; Age:{2}",
            FirstName, LastName, Age);
        return tmp;
    }
}

public class Manager : Employee
{
    protected int Bonus = 0;

    public Manager SetBonus(int bonus)
    {
        Bonus = bonus;
        return this;
    }

    public override string ToString()
    {
        string tmp = String.Format("FirstName:{0}; LastName:{1}; Age:{2}; Bonus:{3}",
            FirstName, LastName, Age, Bonus);
        return tmp;
    }
}

我们希望拥有一个像这样的接口

Manager mgr = new Manager();

mgr.SetFirstName("John").SetLastName("Smith")
    .SetAge(30).SetBonus(1000);     //will not compile

但这将无法编译。原因是现在我们返回了两种类型的“this”对象,分别是 `Employee` 类型和 `Manager` 类型。一旦我们得到基类对象“this”作为 `Employee`,`Manager` 类的所有方法将不再可用。

因此,`Employee.SetAge()` 方法将返回 `Employee` 类型的对象,而无法对 `Manager` 类中的方法进行链式调用。

那么,有没有办法让 `Employee.SetAge()` 方法返回子类 `Manager` 的对象呢?我们需要这样做才能实现我们的流畅接口,也就是方法链式调用?

答案是肯定的,这是可能的,但技术上很复杂。它涉及使用“递归泛型”。我将在此展示一个可行的代码示例。

我不会详细解释该解决方案的工作原理。如果你能读懂 C# 泛型代码,你就能理解这个相当高级的设计。

这个解决方案需要解决的关键问题是如何将派生类的返回类类型传达给基类。关键技巧是始终在流畅接口中返回原始(派生)类的对象。这是通过泛型和类型参数 SELF 来解决的。从最派生的类(如 CEO、Manager、Employee)沿着继承层次结构向下跟踪 SELF,你就会理解这段代码是如何工作的。“where”子句(如“where SELF : EmployeeFluent<SELF>”)只是为了确保该类被用作继承层次结构的一部分。

这是继承流畅接口类的解决方案的类图。

这是继承流畅接口类的解决方案的代码。

public class EmployeeFluent<SELF>
        where SELF : EmployeeFluent<SELF>
{
    protected string FirstName = null;
    protected string LastName = null;
    protected int Age = 0;

    public SELF SetFirstName(string fName)
    {
        FirstName = fName;
        return (SELF)this;
    }

    public SELF SetLastName(string lName)
    {
        LastName = lName;
        return (SELF)this;
    }

    public SELF SetAge(int age)
    {
        Age = age;
        return (SELF)this;
    }

    public override string ToString()
    {
        string tmp = String.Format("FirstName:{0}; LastName:{1}; Age:{2}",
            FirstName, LastName, Age);
        return tmp;
    }
}

public class Employee : EmployeeFluent<Employee> { };

public class ManagerFluent<SELF> : EmployeeFluent<SELF>
        where SELF : ManagerFluent<SELF>
{
    protected int Bonus = 0;

    public SELF SetBonus(int bonus)
    {
        Bonus = bonus;
        return (SELF)this;
    }

    public override string ToString()
    {
        string tmp = String.Format("FirstName:{0}; LastName:{1}; Age:{2}; Bonus:{3}",
            FirstName, LastName, Age, Bonus);
        return tmp;
    }
}

public class Manager : ManagerFluent<Manager> { };

public class CEOFluent<SELF> : ManagerFluent<SELF>
        where SELF : CEOFluent<SELF>
{
    protected int CompanyShares = 0;

    public SELF SetCompanyShares(int shares)
    {
        CompanyShares = shares;
        return (SELF)this;
    }

    public override string ToString()
    {
        string tmp = String.Format("FirstName:{0}; LastName:{1}; Age:{2}; Bonus:{3}; 
            CompanyShares:{4}",
            FirstName, LastName, Age, Bonus, CompanyShares);
        return tmp;
    }
}

public class CEO : CEOFluent<CEO> { };

class Client
{
    static void Main(string[] args)
    {
        CEO ceo1 = new CEO();

        ceo1.SetFirstName("John").SetLastName("Smith")
            .SetAge(30).SetBonus(1000).SetCompanyShares(5000);

        Manager mgr = new Manager();

        mgr.SetFirstName("Cedomir").SetLastName("Jokic")
            .SetAge(40).SetBonus(2000);

        Employee emp = new Employee();

        emp.SetFirstName("Novak").SetLastName("Djokovic")
            .SetAge(20);

        Console.WriteLine(ceo1.ToString());
        Console.WriteLine(mgr.ToString());
        Console.WriteLine(emp.ToString());
        Console.ReadLine();
    }
}

这是继承流畅接口类的解决方案的示例执行。

结论

流畅接口模式已经获得了巨大的普及,并且将会一直存在。仅举 LINQ 为例,它就被广泛使用了。

历史

  • 2022年3月2日:初始版本
© . All rights reserved.