【问题标题】:Avoid if and switch statement and more generic approach避免 if 和 switch 语句以及更通用的方法
【发布时间】:2021-04-22 10:33:08
【问题描述】:

我想避免使用处理特定情况的“if”和“switch”语句,而是采用更通用的方法。

要求: 根据下面详述的一些要求,订单的状态可能被确定为“已确认”、“已关闭”或“需要授权”。

输入:

IsRushOrder: bool
OrderType: Enum (Repair, Hire)
IsNewCustomer: bool
IsLargeOrder: bool

输出:

OrderStatus: Enum (Confirmed, Closed, AuthorisationRequired)

要求(按从上到下的优先顺序):

-Large repair orders for new customers should be closed
-Large rush hire orders should always be closed
-Large repair orders always require authorisation
-All rush orders for new customers always require authorisation
-All other orders should be confirmed

================================================ =============================

用 IF 语句避免明显的答案

这违反了SOLID原则的O。我相信有比添加大量 IF 语句更好的方法来做到这一点

        public IOrder GetOrder(OrderInput orderInput)
        {

            // Apply rules based on the priority 
            if (orderInput.IsLargeOrder == true && orderInput.OrderType == OrderTypeEnum.Repair && orderInput.IsNewCustomer == true)
            {
                return new LargeRepairNewCustomerOrder();
            }

            if (orderInput.IsLargeOrder == true && orderInput.OrderType == OrderTypeEnum.Hire && orderInput.IsRushOrder == true)
            {
                return new LargeRushHireOrder();
            }

            if (orderInput.IsLargeOrder == true && orderInput.OrderType == OrderTypeEnum.Repair)
            {
                return new LargeRepairOrder();
            }

            if (orderInput.IsRushOrder == true && orderInput.IsNewCustomer == true)
            {
                return new AllRushNewCustomerOrder();
            }

            return new AllOtherOrders();
        }

我可以想到的一种不使用 IF 实现的方法是在集合中填充 OrderStatus 并使用 LINQ 进行查询 随着我们添加更多状态,这个实现会很快变得复杂。

public class OrderStatusAnalyzer
{
    protected static List<OrderStatus> OrderStatuses;

    public OrderStatusAnalyzer()
    {
        OrderStatuses = OrderStatusCollection.Get();
    }

    public string GetOrderStatusTypeByOrderInput(OrderInput orderInput)
    {
        var orderStatusTypes = from orderStatus in OrderStatuses
                               where orderStatus.IsNewCustomer == orderInput.IsNewCustomer
                                     && orderStatus.IsLargeOrder == orderInput.IsLargeOrder
                                     && orderStatus.IsRushOrder == orderInput.IsRushOrder
                                     && orderStatus.OrderType == orderInput.OrderType
                               orderby orderStatus.Priority ascending
                               select orderStatus.OrderStatusType;

        var statusTypesList = orderStatusTypes.ToList();
        var orderStatusType = !statusTypesList.Any() ? "VehicleRepairOrder.Order.AllOtherOrders" : statusTypesList[0];

        return orderStatusType;
    }
}
public static class OrderStatusCollection
{
    public static List<OrderStatus> Get()
    {
        // Note:
        // 1) If we have to avoid referencing null, then we can populate the data with probabilities. Populating data with probabilities will become hard Maintain as we add more status   
        // 2) this is also violating O(Open for extension and closed for modification) of SOLID principle 
        // 3) instead of passing null, if you pass actual unit test will fail
        var orderStatuses = new List<OrderStatus>
        {
            new OrderStatus
            {
                IsLargeOrder = true, IsNewCustomer = true, OrderType = OrderTypeEnum.Repair,
                IsRushOrder = null,
                Priority = 1, OrderStatusType = "VehicleRepairOrder.Order.LargeRepairNewCustomerOrder"
            },

            new OrderStatus
            {
                IsLargeOrder = true, IsNewCustomer = null, OrderType = OrderTypeEnum.Hire, IsRushOrder = true,
                Priority = 2, OrderStatusType = "VehicleRepairOrder.Order.LargeRushHireOrder"
            },

            new OrderStatus
            {
                IsLargeOrder = true, IsNewCustomer = null, OrderType = OrderTypeEnum.Repair,
                IsRushOrder = null,
                Priority = 3, OrderStatusType = "VehicleRepairOrder.Order.LargeRepairOrder"
            },

            new OrderStatus
            {
                IsLargeOrder = null, IsNewCustomer = true, OrderType = OrderTypeEnum.Any, IsRushOrder = true,
                Priority = 4, OrderStatusType = "VehicleRepairOrder.Order.AllRushNewCustomerOrder"
            },

        };

        return orderStatuses;
    }
}

【问题讨论】:

  • 您需要一个字典或哈希方法来为不同的组合创建一个键。
  • 我并不完全理解这个问题。你说输出应该是Enum (Confirmed, Closed, AuthorisationRequired),但是你的代码输出了一个字符串比如"VehicleRepairOrder.Order.AllRushNewCustomerOrder"
  • 为什么没有ifs?如果您有 4 个 if,就像您在问题中陈述了您的要求,那么您将拥有一个易于阅读且易于使用调试器逐步完成的代码,以防出现问题。
  • @Canton7 我没有把所有的代码都放在那里我拿那个字符串并使用反射来识别类
  • @someBody 这违反了 SOLID 的 O 原则。我相信有比添加大量 IF 语句更好的方法

标签: c# design-patterns coding-style factory-pattern strategy-pattern


【解决方案1】:

我将根据您提出的要求而不是您的示例代码来回答,因为两者似乎相互冲突。

将这样的需求转化为代码最重要的是确保代码清晰明确地实现了需求。

不要试图变得聪明,只是显而易见。这意味着您更有可能是正确的,并且随着需求的变化,您肯定更易于维护。

根据你的情况...

// Large repair orders for new customers should be closed
if (order.IsLargeOrder && order.OrderType == OrderType.Repair && order.IsNewCustomer)
{
    return OrderStatus.Closed;
}

// Large rush hire orders should always be closed
if (order.IsLargeOrder && order.IsRushOrder && order.OrderType == OrderType.Hire)
{
    return OrderStatus.Closed;
}

// Large repair orders always require authorisation
if (order.IsLargeOrder && order.OrderType == OrderType.Repair)
{
    return OrderStatus.AuthorisationRequired;
}

// All rush orders for new customers always require authorisation
if (order.IsRushOrder && order.IsNewCustomer)
{
    return OrderStatus.AuthorisationRequired;
}

// All other orders should be confirmed
return OrderType.Confirmed;

查看代码与需求的匹配程度,以及在需求发生变化时找到要更改的代码的难易程度?


我想说您的问题根本与开放/封闭原则无关。带有硬编码条件列表的版本并不比包含一些 if 语句的方法更容易修改:这是一种不太明显的方式来做同样的事情。

如果您考虑 Martin 对开放/封闭原则的解释,其核心是确保如果您需要对单个需求进行修改,则将它们放在一个地方,而您不需要寻找并修改大量不同的代码。

例如,您使用 OrderStatus 枚举本身可能违反了打开/关闭原则,具体取决于它的使用方式。如果您有大量单独的代码,它们都可以:

switch (orderStatus)
{
    case OrderStatus.Closed:
        // Do stuff
        break;
    ...
}

...那么添加一个新的订单状态类型将意味着去查找所有这些 switch 语句并添加新的条件,这将违反打开/关闭原则。

值得一提的是,您确实需要对开/关原则持怀疑态度。 Meyer 的版本绝对不适用于像 C# 这样的语言,在这些语言中添加字段/方法不是二进制的重大更改。 (即使确实如此,要将 Meyer 的开/关原则应用到您的代码中,您需要将 GetOrderStatusTypeByOrderInput 方法设为虚拟,如果后来需求发生变化,您将继承 OrderStatusAnalyzer 并在那里实现新的一组需求。)

多态解释也不适用于您的情况:这表示接口是不可变的(“禁止修改”),而应该使用抽象基类。这曾经适用于 C#,在其中向接口添加方法是一个二进制破坏性更改(这就是为什么像 SqlConnection 这样的东西是基类而不是接口),但对于 DIM,这不再适用。

对于像 C# 这样的语言来说,剩下的就是 Martin 的解释版本,它是关于确保如果您需要进行有针对性的更改,您不需要触及整个代码库中的几十个不同的代码位。您的需求实现代码本身根本不会违反这一点,因为任何需求更改都可以通过修改单个方法来进行。使用“开放”和“封闭”这两个词有点牵强,我认为人们只是热衷于继续使用“SOLID”这个词,即使“开放/封闭原则”的原始定义不再适用于新语言,因此他们不断制定新的规则,这些规则大致(但不是真的)符合相同的名称。

事实上,我什至会说您的带有OrderStatusCollection 的版本实际上比我回答中的版本更违反了开放/封闭原则!如果您对 OrderStatusCollection 进行任何更改(例如添加属性或更改属性类型),您还必须修改 OrderStatusAnalyzer 中的那个可怕的 linq 查询:您已经从拥有一点代码修改,有两个位!这是 Martin 的开放/封闭原则告诉您要避免的事情。


说了这么多,最重要的是使用常识。 SOLID 是指导原则,而不是严格的法律。如果你试图虔诚地遵循它们,从而使你的代码变得更糟,那么你就做错了。

【讨论】:

  • 这是我知道但我想避免的答案。这违反了SOLID原则的O。我确信有比添加大量 IF 语句更好的方法。
  • 如果需要另一个条件,只需添加另一个 if 语句?扩展是不是很容易?
  • @AnandNagaraja 我在长时间的讨论中编辑了开放/封闭原则的含义,以及为什么我认为if 版本没有违反它
【解决方案2】:

目前尚不完全清楚为什么要强制执行“没有如果”规则。甚至像 SOLID 这样的一套原则也旨在帮助解决特定问题。如果你没有这些问题,你就不需要 SOLID。如前所述,问题非常简单,您不需要 SOLID。

不过,我猜这个问题可以替代一个更大的问题,并且您希望能够独立更改规则,这让我想起了this article

根据实际限制,您可以通过多种方式解决这些问题。当我读到这个问题时,我很容易想到的是Chain of Responsibility 模式。

你可以从定义一个接口开始:

public interface IPolicy
{
    OrderStatus DecideStatus(
        bool isRushOrder,
        OrderType orderType,
        bool isNewCustomer,
        bool isLargeOrder);
}

然后你实施一套具体的政策,例如:

public class LargeRepairOrderForNewCustomersPolicy : IPolicy
{
    public LargeRepairOrderForNewCustomersPolicy(IPolicy next)
    {
        Next = next;
    }

    public IPolicy Next { get; }

    public OrderStatus DecideStatus(
        bool isRushOrder,
        OrderType orderType,
        bool isNewCustomer,
        bool isLargeOrder)
    {
        if (isLargeOrder && orderType == OrderType.Repair && isNewCustomer)
            return OrderStatus.Closed;

        return Next.DecideStatus(isRushOrder, orderType, isNewCustomer, isLargeOrder);
    }
}

或:

public class LargeRushHireOrderPolicy : IPolicy
{
    public LargeRushHireOrderPolicy(IPolicy next)
    {
        Next = next;
    }

    public IPolicy Next { get; }

    public OrderStatus DecideStatus(
        bool isRushOrder,
        OrderType orderType,
        bool isNewCustomer,
        bool isLargeOrder)
    {
        if (isLargeOrder && isRushOrder && orderType == OrderType.Hire)
            return OrderStatus.Closed;

        return Next.DecideStatus(isRushOrder, orderType, isNewCustomer, isLargeOrder);
    }
}

要使用它们,请组成链:

private IPolicy CreatePolicy()
{
    return new LargeRepairOrderForNewCustomersPolicy(
        new LargeRushHireOrderPolicy(
            new LargeRepairOrderPolicy(
                new RushOrderForNewCustomersPolicy(
                    new DefaultPolicy()))));
}

对象现在可以使用了:

IPolicy policy = CreatePolicy();
var status = policy.DecideStatus(isRushOrder, orderType, isNewCustomer, isLargeOrder);

如果您需要更改策略,可以编辑单个策略、添加新类或重新排序链。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-07-07
    • 1970-01-01
    • 1970-01-01
    • 2016-03-02
    • 1970-01-01
    相关资源
    最近更新 更多