CSharpFunctionalExtensions和面向铁路的编程

时间:2019-05-21 21:43:27

标签: c# functional-programming

从这惊人的过程中:Applying Functional Principles in C#

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

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

我知道我需要创建一些T扩展方法的结果,但是我无法使其正常工作。

伪代码的作用是...

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

(我们在访问数据库以进行更新操作之前执行此操作,如果我们收到无效的EmailAddress,则根本无法访问数据库)

(由于后来需要使用变量emailAddress,铁路被打破了((很可悲!!更糟糕的是...未检查失败的EmailAddress Result实体,应该检查)

  1. 通过玩家存储库->存储库中的ID提取玩家。GetAsync(“ 336978e9-837a-4e8d-6b82-08d6347fe6b6”)
  2. 结果存储在T变量playerResult的结果中

(铁路再次断裂是由于后来需要使用变量playerResult :(真可悲!!。更糟糕的是...未检查未能返回的Result Player实体,应该)

  1. 将新创建的EmailAddress添加到播放器电子邮件地址集合-> p.AddEmailAddress(emailAddress.Value)

(铁路再次断裂:(真可悲!!更糟糕的是……没有检查将EmailAddress添加到玩家集合的失败,应该这样)

  1. 最后一次异步保存更改并返回受影响的数字或行。

为简洁起见,在下面减少了代码行

   var emailAddress = Email.Create("jamesbond@gmail.com")
                .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()

我执行了一些单元测试,并且代码可以正常工作,但就您所知,它完全面向铁路。

谢谢。

1 个答案:

答案 0 :(得分:1)

我个人更喜欢使用局部变量。我认为它更易于维护。在处理诸如工作单元之类的非功能性设计时,尤其如此。但是,您可以使用功能性编程构造来完成您的工作。

您需要使用以下几项来删除本地变量。首先,您的单子需要一个bind;这就是允许您解开多个值,然后将它们映射到新值的原因。第二个是元组。

我发现让我的域类型本身不了解功能类型很有用。因此,只有常规方法而已,不返回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,因此可以在multi-from子句中使用它来展开多个实例:

static async Task Main(string[] args)
{
    var emailAddress = Try.Create(() => new Email("jamesbond@gmail.com"))
            .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("jamesbond@gmail.com"))
            .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("jamesbond@gmail.com"))
                .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("jamesbond@gmail.com"))
                .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");
}

要重申我在开始时所说的话,可以这样做,但是IMO丑陋且难以维护。最重要的是,C#是命令性语言,而不是功能性语言。在代码的部分中采用功能语言的 some 方面可以使其更美观;试图强制所有代码完全发挥功能是导致难以维护的秘诀。