面向对象程序中的功能原则

本文关键字:功能 原则 程序 面向对象 | 更新日期: 2023-09-27 18:05:59

今天我看到了一段代码,我在想它的意义。所以我做了一个小例子,你可以看到它是如何工作的。据我所知,它应该是嵌套的功能,大量使用lambda -微积分,就像在函数式语言中使用的那样。因为我发现这不是很好理解(从一个文件跳到另一个文件),我只是想知道你们中是否有人有类似的经历。

功能示例一

    public void DoSomethingA(int inputInt, Action<int?, Exception> result)
    {
        try
        {
            int? retVal = inputInt;
            result(retVal, null);
        }
        catch (Exception e)
        {
            result(null, e);
        }
    }

函数二示例

    static void DoSomethingB(int? baseInt, Action<Exception> result)
    {
        try
        {
            result(null);
        }
        catch (Exception e)
        {
            result(e);
        }
    }

示例两者同时调用。

    public int? CalculateSomething(int input)
    {
        int? result;
        DoSomethingA(input, (resultInt, aException) =>
        {
            if (aException != null)
            {
                return;
            }
            DoSomethingB(resultInt, bException =>
            {
                if (bException != null)
                {
                    return;
                }
                result = resultInt;
            });
        });
        return null;
    }

这可能是有趣的正常化"Main"函数将有某种承诺。所以它注册了函数,等待结束,然后得到结果。

面向对象程序中的功能原则

你所描述的是一个单子。特别是Try单子。我将在下面展示如何实现一个Try单子,您的CalculateSomething代码将如下所示:

public Try<int> CalculateSomething(int input) =>
    from x in DoSomethingA(input)
    from y in DoSomethingB(x)
    select y;

无论如何都很容易理解。

首先声明一个代表Try操作的委托:

public delegate TryResult<T> Try<T>();

下一步定义TryResult<T>。它将捕获成功时的返回值,以及失败时的异常:

public struct TryResult<T>
{
    internal readonly T Value;
    internal readonly Exception Exception;
    public TryResult(T value)
    {
        Value = value;
        Exception = null;
    }
    public TryResult(Exception e)
    {
        Exception = e;
        Value = default(T);
    }
    public static implicit operator TryResult<T>(T value) =>
        new TryResult<T>(value);
    internal bool IsFaulted => Exception != null;
    public override string ToString() =>
        IsFaulted
            ? Exception.ToString()
            : Value.ToString();
    public static readonly TryResult<T> Bottom = new InvalidOperationException();
}

接下来为Try委托定义一些扩展方法(这是c#委托支持扩展方法的一个鲜为人知的特性):

public static class TryExtensions
{
    public static TryResult<T> Try<T>(this Try<T> self)
    {
        try
        {
            if (self == null) return TryResult<T>.Bottom;
            return self();
        }
        catch (Exception e)
        {
            return new TryResult<T>(e);
        }
    }
    public static R Match<T, R>(this Try<T> self, Func<T, R> Succ, Func<Exception, R> Fail)
    {
        var res = self.Try();
        return res.IsFaulted
            ? Fail(res.Exception)
            : Succ(res.Value);
    }
}

它们都允许以安全的方式调用Try委托。关键是Match扩展方法,它在结果上'模式匹配':

int res = CalculateSomething(1).Match(
              Succ: value => value,
              Fail: excep => 0 
              );

因此,您被迫承认函数可能会抛出异常以获取该值。

这里缺少的一件事是它如何与LINQ:

一起工作。
public static class TryExtensions
{
    public static Try<U> Select<T, U>(this Try<T> self, Func<T, U> select) =>
        new Try<U>(() =>
        {
            var resT = self.Try();
            if (resT.IsFaulted) return new TryResult<U>(resT.Exception);
            return select(resT.Value);
        });
    public static Try<V> SelectMany<T, U, V>(
        this Try<T> self,
        Func<T, Try<U>> bind,
        Func<T, U, V> project ) =>
            new Try<V>(() =>
            {
                var resT = self.Try();
                if (resT.IsFaulted) return new TryResult<V>(resT.Exception);
                var resU = bind(resT.Value).Try();
                if (resU.IsFaulted) return new TryResult<V>(resT.Exception);
                return new TryResult<V>(project(resT.Value, resU.Value));
            });
}

Select允许此操作:

var res = from x in y
          select x;

SelectMany允许此操作:

var res = from x in y
          from z in x
          select z;

也就是说它允许多个from语句按顺序运行。这就是所谓的monadic绑定(但是你不需要知道它的工作原理——我不想在这里写monad教程)。从本质上讲,它捕获了CalculateSomething示例中的嵌套模式,因此您不必再次手动编写它。

就是这样。上面的代码,你只写一次。现在让我们实现DoSomething函数:

public Try<int> DoSomethingA(int inputInt) => () =>
   inputInt;
public Try<int> DoSomethingB(int inputInt) => () =>
{
    throw new Exception();
};

注意它们是如何被定义为lambda的。如果你使用c# 5或更低版本,它们看起来像这样:

public Try<int> DoSomethingA(int inputInt)
{
    return () => inputInt;
}

如果你想看到更完整的实现,请查看我的c#语言-ext函数库,那里有一个Try实现。它有许多有用的扩展方法,允许您编写功能代码,而无需使用try/catch的模板。

有时候遵循良好设计的代码可能比单一函数更难理解,但重用某些部分代码并避免代码重复要容易得多……

显然,通过正确地命名对象和函数,代码应该更容易理解。

在上面的代码中,最好忽略函数1和函数2中的异常,并在调用者处处理它们。这样,操作就简化了。例如,下面的代码要简单得多,但等效:
public int? CalculateSomething(int input)
{
    try
    {
        var result1 = SomeMath(input);
        var result2 = SomeOtherMath(result1);
        return result2;
    }
    catch
    {
        return null;
    }
}

这段代码可以更容易地推广到处理一个操作列表,其中一个函数的结果是下一个函数的输入。