c# - CSharpFunctionalExtensions 和面向铁路的编程

标签 c# functional-programming

来自这个令人惊叹的类(class):Applying Functional Principles in C#

我正在尝试应用域驱动设计、功能原理和面向铁路的编程方法。

有人可以帮我简化下面的代码行吗?

我知道我需要创建一些 T 扩展方法的结果,我尝试过,但无法让它们工作。

伪代码的作用是......

  1. 创建电子邮件值对象 -> Email.Create
  2. 创建 EmailAddress 实体 -> EmailAddress.Create
  3. 结果存储在 T 变量 emailAddress 的 Result 中

(我们在访问数据库进行更新操作之前执行此操作,如果我们得到无效的电子邮件地址,则根本没有意义访问数据库)

(由于稍后需要使用变量 emailAddress,铁路被破坏了:(太伤心了!!。更糟糕的是......失败的 EmailAddress 结果实体没有被检查,它应该被检查)

  • 通过播放器存储库按 ID 获取播放器 ->repository.GetAsync("336978e9-837a-4e8d-6b82-08d6347fe6b6")
  • 结果存储在T变量playerResult的Result中
  • (由于稍后需要提供变量playerResult,铁路再次被破坏:(太伤心了!!。更糟糕的是......未能返回结果的Player实体未被检查,它应该是)

  • 将新创建的 EmailAddress 添加到玩家电子邮件地址集合中 -> p.AddEmailAddress(emailAddress.Value)
  • (铁路再次损坏:(太悲伤了!!。更糟糕的是...未检查将电子邮件地址添加到玩家集合中的失败,而且应该检查)

  • 最后异步保存更改并返回受影响的数量或行数。
  • 下面为简洁起见减少了代码行

       var emailAddress = Email.Create("<a href="https://stackoverflow.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="caa0aba7afb9a8a5a4ae8aada7aba3a6e4a9a5a7" rel="noreferrer noopener nofollow">[email protected]</a>")
                    .OnSuccess(email => EmailAddress.Create(email, default));
    
                var playerResult = await emailAddress.OnSuccess(e => repository.GetAsync("336978e9-837a-4e8d-6b82-08d6347fe6b6")).ToResult(""));
    
                var wasAdded = playerResult.OnSuccess(p => p.AddEmailAddress(emailAddress.Value));
    
                var wasSaved = await wasAdded.OnSuccess(a => unitOfWork.SaveChangesAsync());
    
                var message = wasSaved
                    .Ensure(r => r > 0, "No record were saved")
                    .OnBoth(result => result.IsSuccess ? "Ok" : result.Error);
    

    下面是方法的签名

    Email.Create -> public static Result<Email> Create(string email)
    EmailAddress.Create -> public static Result<EmailAddress> Create(Email mailAddress, string mailAddressInstructions)
    GetAsync -> public Task<Maybe<Player>> GetAsync(string id)
    AddEmailAddress -> public Result<bool> AddEmailAddress(EmailAddress emailAddress)
    SaveChangesAsync -> public async Task<int> SaveChangesAsync()
    

    我执行了一些单元测试,代码正在运行,但据您所见,距离面向铁路还很远。

    提前致谢。

    最佳答案

    就我个人而言,我更喜欢使用局部变量;我认为它更易于维护。在处理非功能性设计(例如工作单元)时尤其如此。但是,您想要做的事情可以使用函数式编程结构来实现。

    您需要使用一些东西来删除局部变量。首先,你需要一个bind为你的单子(monad);这就是允许您解包多个值然后将它们映射到新值的原因。第二个是元组。

    我发现让我的域类型本身不了解函数类型是很有用的。所以只有常规方法之类的,不返回 Result<T>类型:

    private static Task<Player> RepositoryGetAsync(string id) => Task.FromResult(new Player());
    private static Task<int> RepositorySaveChangesAsync() => Task.FromResult(0);
    
    public sealed class Player
    {
        public bool AddEmailAddress(EmailAddress address) => true;
    }
    
    public sealed class Email
    {
        public Email(string address) => Address = address ?? throw new ArgumentNullException(nameof(address));
    
        public string Address { get; }
    }
    
    public sealed class EmailAddress
    {
        public static EmailAddress Create(Email address, int value) => new EmailAddress();
    }
    

    这是代码的第一个版本 - 使用 my own Try<T> type ,因为我不确定哪个Maybe<T>Result<T>您正在使用的类型。我的Try<T>类型支持SelectMany这样它就可以在多个中使用-from用于解开多个实例的子句:

    static async Task Main(string[] args)
    {
        var emailAddress = Try.Create(() => new Email("<a href="https://stackoverflow.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="84eee5e9e1f7e6ebeae0c4e3e9e5ede8aae7ebe9" rel="noreferrer noopener nofollow">[email protected]</a>"))
                .Map(email => EmailAddress.Create(email, default));
        var playerResult = await Try.Create(() => RepositoryGetAsync("336978e9-837a-4e8d-6b82-08d6347fe6b6"));
    
        var wasAdded = from address in emailAddress
                        from player in playerResult
                        select player.AddEmailAddress(address);
    
        var wasSaved = await wasAdded.Map(_ => RepositorySaveChangesAsync());
    
        if (wasSaved.Value == 0)
            throw new Exception("No records were saved");
    }
    

    如果我们开始使用元组,我们就可以组合几个变量。语法有点尴尬(例如,解构 lambda 参数),但它是可行的:

    static async Task Main(string[] args)
    {
        var emailAddressAndPlayerResult = await Try.Create(() => new Email("<a href="https://stackoverflow.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="bfd5ded2daccddd0d1dbffd8d2ded6d391dcd0d2" rel="noreferrer noopener nofollow">[email protected]</a>"))
                .Map(email => EmailAddress.Create(email, default))
                .Map(async emailAddress => (emailAddress, await RepositoryGetAsync("336978e9-837a-4e8d-6b82-08d6347fe6b6")));
    
        var wasAdded = emailAddressAndPlayerResult.Map(((EmailAddress Address, Player Player) v) => v.Player.AddEmailAddress(v.Address));
    
        var wasSaved = await wasAdded.Map(_ => RepositorySaveChangesAsync());
    
        if (wasSaved.Value == 0)
            throw new Exception("No records were saved");
    }
    

    一旦我们混合了元组,其余变量就可以很好地折叠起来。剩下唯一尴尬的部分是await通常需要括号。例如,此代码与上面相同:

    static async Task Main(string[] args)
    {
        var wasAdded =
            (
                await Try.Create(() => new Email("<a href="https://stackoverflow.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="d5bfb4b8b0a6b7babbb195b2b8b4bcb9fbb6bab8" rel="noreferrer noopener nofollow">[email protected]</a>"))
                    .Map(email => EmailAddress.Create(email, default))
                    .Map(async emailAddress => (emailAddress, await RepositoryGetAsync("336978e9-837a-4e8d-6b82-08d6347fe6b6")))
            )
            .Map(((EmailAddress Address, Player Player) v) => v.Player.AddEmailAddress(v.Address));
    
        var wasSaved = await wasAdded.Map(_ => RepositorySaveChangesAsync());
    
        if (wasSaved.Value == 0)
            throw new Exception("No records were saved");
    }
    

    现在,当我们组合这些变量时,您可以看到 await 是如何变化的。表达式尤其会在道路上造成“颠簸”。丑陋,但可行。要删除最后一个变量:

    static async Task Main(string[] args)
    {
        var wasSaved =
            await
            (
                await Try.Create(() => new Email("<a href="https://stackoverflow.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="f99398949c8a9b96979db99e94989095d79a9694" rel="noreferrer noopener nofollow">[email protected]</a>"))
                    .Map(email => EmailAddress.Create(email, default))
                    .Map(async emailAddress => (emailAddress, await RepositoryGetAsync("336978e9-837a-4e8d-6b82-08d6347fe6b6")))
            )
            .Map(((EmailAddress Address, Player Player) v) => v.Player.AddEmailAddress(v.Address))
            .Map(_ => RepositorySaveChangesAsync());
    
        if (wasSaved.Value == 0)
            throw new Exception("No records were saved");
    }
    

    重申一下我在开始时所说的,这样做是可能的,但在我看来,它很丑陋且不易维护。最重要的是,C# 是一种命令式语言,而不是一种函数式语言。在代码的部分中采用函数式语言的某些方面可以使其更加优雅;试图强制所有代码都功能齐全会导致不可维护性。

    关于c# - CSharpFunctionalExtensions 和面向铁路的编程,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56246749/

    相关文章:

    c# - 启动保持事件状态的非阻塞线程的可能方法

    f# - 在 F# 中非常简单的 RogueLike,使它更 "functional"

    scala - 如何在 HUET zipper 内向上导航

    f# - 谁能告诉我这个 F# 片段有什么问题?

    python - 无法在 python3.1 中以功能方式关闭文件?

    java - 枚举声明调用方法

    c# - 是否可以查看使用接口(interface)实例化的类方法的摘要?

    c# - 如何在 Umbraco 6 中实现自定义 Controller

    c# - char 和 char 有什么区别?

    c# - 为 LINQ 查询构建带有表达式树的 Any()