我畫著圖,FluentAPI 她自己就生成了

  • 2020 年 11 月 17 日
  • 筆記

在 Newbe.ObjectVistor 0.3 版本中我們非常興奮的引入了一個緊張刺激的新特性:使用狀態圖來生成任意給定的 FluentAPI 設計。

開篇摘要

在非常多優秀的框架中都存在一部分 FluentAPI 的設計。這種 API 設計更加符合人類自言語言描述。使得代碼更加具備可讀性。

在 Newbe.ObjectVistor 0.3 版本中,我們設計引入了一種使用狀態圖來自動生成 FluentAPI 代碼的機制。極大了簡化了 FluentAPI 實現所需要的腦力勞動。

本篇我們將通過一些示例,來了解一下當前版本中該特性的主要效果。

整數累加 FluentAPI

假如,我們現在需要實現下面這樣效果的一個 API:

[Test]
public void SumList()
{
    var sumBuilder = new SumBuilder(new List<int>());
    var re = sumBuilder
        .AddNumber(1)
        .AddNumber(2)
        .AddNumber(3)
        .Sum();
    re.Should().Be(6);
}

這個 API 使用 FluentAPI 的方式來表述一個累加的過程。

為了實現這個 API 設計,在 Newbe.ObjectVisitor 0.3 中,使用下面這樣一個狀態圖標記表述這個 API 設計:

stateDiagram
    [*]  --> AddNumber : AddNumber(int number)
    AddNumber  --> AddNumber : AddNumber(int number)
    AddNumber --> [*] : Sum() return int

這實際上是 mermaid 狀態圖標記。轉換為圖形即為下面這個效果。不需要過多的解釋就可以理解:

SumBuilder

有了這個狀態圖之後,使用 Newbe.ObjectVisitor 中的 FluentApiDesignParserFluentApiFileGenerator 便可以生成如下代碼。

using System;
using System.Collections.Generic;
using System.Linq;

namespace Newbe.ObjectVisitor.Tests.SumBuilderFluentApi
{
    public class SumBuilder : Newbe.ObjectVisitor.IFluentApi
        , SumBuilder.ISumBuilder_AddNumber
    {
        private readonly List<int> _context;

        public SumBuilder(List<int> context)
        {
            _context = context;
        }

        #region UserImpl

        private void Core_AddNumber(int number)
        {
            throw new NotImplementedException();
        }


        private int Core_Sum()
        {
            throw new NotImplementedException();
        }

        #endregion

        #region AutoGenerate
/// 此處省略了自動生成的固定代碼部分,請到倉庫中查看
        #endregion
    }
}

有了這個模板之後,只要實現 Core_AddNumberCore_Sum,一個符合預期設計的 FluentAPI 就完成了!

累加後累乘

現在,我們稍微改變一下需求。上節我們實現的是一個 1+2+3 這樣的累加效果。現在我們需要一個 (1+2+3)*(4+5+6)*(7+8+9+10) 這樣的效果。

示例的調用代碼如下:

[Test]
public void MultipleSumList()
{
    var builder = new MultipleSumBuilder(new List<List<int>>());
    var re = builder
        .AddNumber(1)
        .AddNumber(2)
        .NextFactor()
        .AddNumber(3)
        .Sum();
    re.Should().Be(9);
}

為了實現這個效果,我們修改一下狀態圖,增加一條新的規則,得到:

stateDiagram
    [*]  --> AddNumber : AddNumber(int number)
    AddNumber  --> AddNumber : AddNumber(int number)
    AddNumber  --> AddNumber : NextFactor()
    AddNumber --> [*] : Sum() return int

如圖:

MultipleSumBuilder

創建數據庫鏈接字符串

前面的示例或許缺乏生產實際,現在添加一個生產示例。我們現在要實現一個 ConnectionStringBuilder 用來創建數據庫連接字符串,其中有以下限制:

  1. 必須指定 Host。
  2. 身份認證方式必須且只能指定一種,要麼是用戶名密碼方式,要麼是 Windows 憑據。

首先,我們有一個模型來保存上面提到的數據。

public class ConnectionStringModel
{
    public string Host { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
    public bool? IsWindowsAuthentication { get; set; }
}

接着,我們直接使用狀態圖來設計這個 FluentAPI。設計結果如下:

stateDiagram
    [*]  --> SetHost : SetHost(string host)
    SetHost  --> UseUsernamePassword : UseUsernamePassword(string username, string password)
    SetHost  --> UseWindowsAuthentication : UseWindowsAuthentication()
    UseUsernamePassword --> [*] : Build() return string
    UseWindowsAuthentication --> [*] : Build() return string

如圖:

ConnectionStringBuilder

有了設計,接下來就是使用生成器啪嗒一下生成代碼,然後添加實現,這裡只展示需要自己實現的內容:

#region UserImpl

private void Core_SetHost(string host)
{
    _context.Host = host;
}


private void Core_UseUsernamePassword(string username, string password)
{
    _context.Username = username;
    _context.Password = password;
}


private void Core_UseWindowsAuthentication()
{
    _context.IsWindowsAuthentication = true;
}

// 這裡使用 ObjectVisitor 將一個模型的非空字段拼接在一起
private static readonly ICachedObjectVisitor<ConnectionStringModel, StringBuilder> Builder =
    default(ConnectionStringModel)!.V()
        .WithExtendObject<ConnectionStringModel, StringBuilder>()
        .ForEach((name, value, sb) => Append(name, value, sb))
        .Cache();

private static void Append(string name, object? value, StringBuilder sb)
{
    if (value != null)
    {
        sb.Append($"{name}={value};");
    }
}

private string Core_Build()
{
    var sb = new StringBuilder();
    Builder.Run(_context, sb);
    return sb.ToString();
}

#endregion

下面是簡單的兩個測試用例:

public class ConnectionStringBuilderTest
{
    [Test]
    public void UseUsernamePassword()
    {
        var builder = new ConnectionStringBuilder(new ConnectionStringModel());
        var re = builder.SetHost("localhost")
            .UseUsernamePassword("yueluo", "dalao")
            .Build();
        re.Should().Be("Host=localhost;Username=yueluo;Password=dalao;");
    }

    [Test]
    public void UseWindowsAuthentication()
    {
        var builder = new ConnectionStringBuilder(new ConnectionStringModel());
        var re = builder.SetHost("localhost")
            .UseWindowsAuthentication()
            .Build();
        re.Should().Be("Host=localhost;IsWindowsAuthentication=True;");
    }
}

值得特別提出但是,這和直接使用 ConnectionStringModel 模型來構建字符串,通過 FluentAPI 的形式,約束了開發者能夠賦值的屬性。可以避免忘記對必要的屬性賦值或者錯誤賦值等等出錯情況。

Get 和 Delete 沒有 Body,Post 和 Put 才有

和上一節類型,我們使用 FluentAPI 來構建請求,但是需要滿足以下約束:

  1. 可以指定 Uri
  2. Get 和 Delete 不能指定 Body,但是 Post 和 Put 可以

上設計:

stateDiagram
    [*]  --> Get : Get()
    Get --> GetUri : SetUri(Uri uri) share _SetUriCore

    [*]  --> Delete : Delete()
    Delete --> DeleteUri : SetUri(Uri uri) share _SetUriCore

    [*]  --> Post : Post()
    Post --> PostUri : SetUri(Uri uri) share _SetUriCore
    PostUri --> SetContent : _SetContent share _SetContentCore

    [*]  --> Put : Put()
    Put --> PutUri : SetUri(Uri uri) share _SetUriCore
    PutUri --> SetContent : _SetContent share _SetContentCore

    SetContent --> [*] : _Build return HttpRequestMessage
    GetUri --> [*] : _Build return HttpRequestMessage
    DeleteUri --> [*] : _Build return HttpRequestMessage

上圖:

RequestBuilder

注意,這裡引入了一些奇怪的關鍵詞 share ,由於這些關鍵詞還未全部定稿,因此不展開說明。

可以通過以下鏈接,查看生成的代碼和測試用例。

//github.com/newbe36524/Newbe.ObjectVisitor/tree/main/src/Newbe.ObjectVisitor/Newbe.ObjectVisitor.Tests/HttpClientFluentApi

//gitee.com/yks/Newbe.ObjectVisitor/tree/main/src/Newbe.ObjectVisitor/Newbe.ObjectVisitor.Tests/HttpClientFluentApi

造一輛汽車一定要四個輪子一個引擎

我們需要實現一個 CarBuilder,有一些約束:

  1. CarBuilder 當且僅當在調用四次 AddWheel 和一次 AddEngine 之後才能出現 Build 方法
  2. 雖然限制了次數,但是,順序不能限定,什麼順序都可以。

上設計:

stateDiagram
    [*]  --> W1 : AddWheel(int size) share AddWheel
    W1 --> W2 : AddWheel(int size) share AddWheel
    W2 --> W3 : AddWheel(int size) share AddWheel
    W3 --> W4 : AddWheel(int size) share AddWheel

    [*] --> E : AddEngine(string engine) share AddEngine
    E --> WE1 : AddWheel(int size) share AddWheel
    WE1 --> WE2 : AddWheel(int size) share AddWheel
    WE2 --> WE3 : AddWheel(int size) share AddWheel
    WE3 --> WE4 : AddWheel(int size) share AddWheel

    W1 --> WE1 : AddEngine(string engine) share AddEngine
    W2 --> WE2 : AddEngine(string engine) share AddEngine
    W3 --> WE3 : AddEngine(string engine) share AddEngine
    W4 --> WE4 : AddEngine(string engine) share AddEngine

    WE4 --> [*] : Build() return Car

上圖,這個圖從出發點出發,不論怎麼走都會經過四次 AddWheel 和 一次 AddEngine:

CarBuilder

注意,雖然設計看起來非常複雜,但是,需要手寫的代碼只有非常簡短的兩段:

#region UserImpl

private void Shared_AddWheel(int size)
{
    if (_context.Wheel1 == 0)
    {
        _context.Wheel1 = size;
        return;
    }

    if (_context.Wheel2 == 0)
    {
        _context.Wheel2 = size;
        return;
    }

    if (_context.Wheel3 == 0)
    {
        _context.Wheel3 = size;
        return;
    }

    if (_context.Wheel4 == 0)
    {
        _context.Wheel4 = size;
        return;
    }
}


private void Shared_AddEngine(string engine)
{
    _context.Engine = engine;
}


private Car Core_Build()
{
    return _context;
}

#endregion

可以通過以下鏈接,查看生成的代碼和測試用例。

//github.com/newbe36524/Newbe.ObjectVisitor/tree/main/src/Newbe.ObjectVisitor/Newbe.ObjectVisitor.Tests/CarBuilder

//gitee.com/yks/Newbe.ObjectVisitor/tree/main/src/Newbe.ObjectVisitor/Newbe.ObjectVisitor.Tests/CarBuilder

本篇總結

這是一個很有意思的設計,如果你對這個設計很感興趣,有新奇的想法,歡迎關注 Newbe.ObjectVisitor 項目,提出您的寶貴想法。

發佈說明

使用樣例

番外分享

GitHub 項目地址://github.com/newbe36524/Newbe.ObjectVisitor

Gitee 項目地址://gitee.com/yks/Newbe.ObjectVisitor

Newbe.ObjectVisitor