如何重构cascade if语句

时间:2018-02-18 15:34:14

标签: refactoring

我在https://github.com/arialdomartini/Back-End-Developer-Interview-Questions#snippets

上找到了这个问题

我很好奇你的意见,我只是找不到这个重构的合适解决方案,以及在这种非常常见的情况下适用的模式。

function()
{
    HRESULT error = S_OK;

    if(SUCCEEDED(Operation1()))
    {
        if(SUCCEEDED(Operation2()))
        {
            if(SUCCEEDED(Operation3()))
            {
                if(SUCCEEDED(Operation4()))
                {
                }
                else
                {
                    error = OPERATION4FAILED;
                }
            }
            else
            {
                error = OPERATION3FAILED;
            }
        }
        else
        {
            error = OPERATION2FAILED;
        }
    }
    else
    {
        error = OPERATION1FAILED;
    }

    return error;
}

你知道如何重构这个吗?

2 个答案:

答案 0 :(得分:2)

实际上,我觉得重构的空间比Sergio Tulentsev所建议的要多。

您所链接的回购中的问题更多的是关于代码对话,而不是封闭式问题。因此,我认为有必要讨论该代码的气味和设计缺陷,以建立重构目标。

闻起来

我看到了这些问题:

  • 该代码违反了某些SOLID principles。它肯定违反了Open Closed Principle,因为如果不更改其代码就无法扩展它。例如,添加新操作将需要添加新的if / else分支;
  • 它也违反了Single Responsibility Principle。它只是做太多了。它执行错误检查,负责执行所有4个操作,包含其实现,负责检查其结果并按正确的顺序链接其执行;
  • 它违反了Dependency Inversion Principle,因为高级组件和低级组件之间存在依赖性;
  • 它有一个可怕的Cyclomatic complexity
  • 它表现出高耦合度和低内聚性,与what is recommended恰好相反;
  • 它包含许多代码重复:函数Succeeded()在每个分支中重复; if / else的结构一遍又一遍地复制; error的分配重复。
  • 它可能具有纯粹的功能性质,但它依赖状态突变,这使得对其进行推理并不容易。
  • 有一个空的if语句主体,这可能会造成混淆。

重构

让我们看看该怎么做。
在这里,我使用的是C#实现,但是无论使用哪种语言,都可以执行类似的步骤。
我重命名了某些元素,因为我相信遵守命名约定是重构的一部分。

  internal class TestClass
    {
        HResult SomeFunction()
        {
            var error = HResult.Ok;

            if(Succeeded(Operation1()))
            {
                if(Succeeded(Operation2()))
                {
                    if(Succeeded(Operation3()))
                    {
                        if(Succeeded(Operation4()))
                        {
                        }
                        else
                        {
                            error = HResult.Operation4Failed;
                        }
                    }
                    else
                    {
                        error = HResult.Operation3Failed;
                    }
                }
                else
                {
                    error = HResult.Operation2Failed;
                }
            }
            else
            {
                error = HResult.Operation1Failed;
            }

            return error;
        }

        private string Operation1()
        {
            // some operations
            return "operation1 result";
        }
        private string Operation2()
        {
            // some operations
            return "operation2 result";
        }
        private string Operation3()
        {
            // some operations
            return "operation3 result";
        }
        private string Operation4()
        {
            // some operations
            return "operation4 result";
        }

        private bool Succeeded(string operationResult) =>
            operationResult == "some condition";
    }

    internal enum HResult
    {
        Ok,
        Operation1Failed,
        Operation2Failed,
        Operation3Failed,
        Operation4Failed,
    }
}

为了简单起见,我假设每个操作都返回一个字符串,并且成功或失败是基于对字符串的相等性检查,但是当然可以是任何东西。在接下来的步骤中,如果代码独立于结果验证逻辑,那就太好了。

步骤1

最好在一些测试工具的支持下开始重构。

public class TestCase
{
    [Theory]
    [InlineData("operation1 result", HResult.Operation1Failed)]
    [InlineData("operation2 result", HResult.Operation2Failed)]
    [InlineData("operation3 result", HResult.Operation3Failed)]
    [InlineData("operation4 result", HResult.Operation4Failed)]
    [InlineData("never", HResult.Ok)]
    void acceptance_test(string failWhen, HResult expectedResult)
    {
        var sut = new SomeClass {FailWhen = failWhen};

        var result = sut.SomeFunction();

        result.Should().Be(expectedResult);
    }
}

我们的案子微不足道,但是作为测验应该是求职面试的问题,我不会忽略它。

步骤2

第一个重构可能是摆脱可变状态:每个if分支都只能返回值,而不是突变变量error。另外,名称error具有误导性,因为它包含成功案例。让我们摆脱它:

HResult SomeFunction()
{
    if(Succeeded(Operation1()))
    {
        if(Succeeded(Operation2()))
        {
            if(Succeeded(Operation3()))
            {
                if(Succeeded(Operation4()))
                    return HResult.Ok;
                else
                    return HResult.Operation4Failed;
            }
            else
                return HResult.Operation3Failed;
        }
        else
            return HResult.Operation2Failed;
    }
    else
        return HResult.Operation1Failed;
}

我们摆脱了空的if主体,与此同时使代码更易于推理。

步骤3

如果现在我们反转每个if语句(Sergio建议的步骤)

internal HResult SomeFunction()
{
    if (!Succeeded(Operation1()))
        return HResult.Operation1Failed;

    if (!Succeeded(Operation2()))
        return HResult.Operation2Failed;

    if (!Succeeded(Operation3()))
        return HResult.Operation3Failed;

    if (!Succeeded(Operation4()))
        return HResult.Operation4Failed;

    return HResult.Ok;
}

我们很明显地看到代码执行了一系列的执行:如果一个操作成功,则调用下一个操作;否则,链条会中断,并出现错误。 GOF Chain of Responsibility Pattern浮现在脑海。

步骤4

我们可以将每个操作移到一个单独的类,并让我们的函数接收一系列操作以一次执行。每个班级都将处理其特定的操作逻辑(遵守单一责任原则)。

internal HResult SomeFunction()
{
    var operations = new List<IOperation>
    {
        new Operation1(),
        new Operation2(),
        new Operation3(),
        new Operation4()
    };

    foreach (var operation in operations)
    {
        if (!_check.Succeeded(operation.DoJob()))
            return operation.ErrorCode;
    }

    return HResult.Ok;
}

我们完全摆脱了if(只有一个)。

注意如何:

  • 引入了IOperation接口,这是使函数与操作脱钩的一项初步举措,符合Dependency Inversion Principle;
  • 可以使用Dependency Injection将操作列表轻松地注入到类中。
  • 结果验证逻辑已移到单独的类Check中,并注入到主类中(满足了依赖倒置和单一职责)。
internal class SimpleStringCheck : IResultCheck
{
    private readonly string _failWhen;

    public Check(string failWhen)
    {
        _failWhen = failWhen;
    }

    internal bool Succeeded(string operationResult) =>
        operationResult != _failWhen;
}

我们获得了在不修改主类的情况下切换检查逻辑的能力(开闭原理)。

每个操作已移至一个单独的类,例如:

internal class Operation1 : IOperation {
    public string DoJob()
    {
        return "operation1 result";
    }

    public HResult ErrorCode => HResult.Operation1Failed;
}

每个操作都知道其自己的错误代码。该功能本身变得独立于此。

步骤5

还有更多需要重构的代码

foreach (var operation in operations)
{
    if (!_check.Succeeded(operation.DoJob()))
        return operation.ErrorCode;
    }

    return HResult.Ok;
}
  • 首先,不清楚为什么将案例return HResult.Ok;视为特殊情况:链中可能包含一个永不终止并返回该值的终止操作。这将使我们摆脱最后的if

  • 第二,我们的职能仍然有两个责任:访问链并检查结果。

一个想法可能是将操作封装到一个真实的链中,因此我们的功能可以简化为:

return operations.ChainTogether(_check).Execute();

我们有2个选项:

  • 每个操作都知道下一个操作,因此从operation1开始,我们可以通过一次调用执行整个链;
  • 运营始终不被视为链条的一部分;单独的封装结构为操作增加了按顺序执行的功能。

我要继续讨论后者,但这是有争议的。我要介绍一个类,它对链中的环进行建模,使代码远离我们的类:

internal class OperationRing : IRing
{
    private readonly Check _check;
    private readonly IOperation _operation;
    internal IRing Next { private get; set; }

    public OperationRing(Check check, IOperation operation)
    {
        _check = check;
        _operation = operation;
    }

    public HResult Execute()
    {
        var operationResult = _operation.DoJob();

        if (_check.Succeeded(operationResult))
            return Next.Execute();

        return _operation.ErrorCode;
    }
}

此类负责执行操作,并在下一个环成功执行时处理该环,或者中断返回正确错误代码的链。

该链将由永不失败的元素终止:

internal class AlwaysSucceeds : IRing
{
    public HResult Execute() => HResult.Ok;
}

我们原来的课程减少为:

internal class SomeClass
{
    private readonly Check _check;
    private readonly List<IOperation> _operations;

    public SomeClass(Check check, List<IOperation> operations)
    {
        _check = check;
        _operations = operations;
    }

    internal HResult SomeFunction()
    {
        return _operations.ChainTogether(_check).Execute();
    }
}

在这种情况下,ChainTogether()是实现为List<IOperation>的扩展的函数,因为我不认为链接逻辑是我们课程的责任。

那不是正确答案

将职责划分为最适当的类别是绝对有争议的。例如:

  • 链接操作是我们功能的任务吗?还是应该直接接受链式结构?
  • 为什么使用枚举?正如罗伯特·马丁(Robert Martin)在“重构:改善现有代码的设计”中写道:枚举是代码的味道,应重构为多态类;
  • 多少钱太多了?最终的设计是否太复杂?整个应用程序的复杂性是否需要这种级别的模块化?

因此,我确定还有其他几种方法可以重构原始功能。在工作面试中,或在一对编程会议中,我希望会进行很多讨论和评估。

答案 1 :(得分:1)

你可以在这里使用早期回报。

function() {
  if(!SUCCEEDED(Operation1())) {
    return OPERATION1FAILED;
  }
  if(!SUCCEEDED(Operation2())) {
    return OPERATION2FAILED;
  }
  if(!SUCCEEDED(Operation3())) {
    return OPERATION3FAILED;
  }
  if(!SUCCEEDED(Operation4())) {
    return OPERATION4FAILED;
  }

  # everything succeeded, do your thing

  return S_OK;
}