在不违反SOLID原则的情况下在接口之间移动数据?

时间:2013-07-18 21:56:01

标签: c# .net design-patterns solid-principles

TL; DR:在不违反SOLID原则的情况下,在接口之间移动数据的最佳方法是什么?


我可能过分思考这个问题而且我并不打算在SOLID原则方面表现出教条;但我想得到一些意见。我一直在重构一个购物车,以便更加“坚固”,我写的方法对我来说似乎是“代码味道”(也许不是)。

我有一个CartHelper类看起来像这样(为了简洁,我简化了一下):

public class CartHelper
{
    IEnumerable Products;
    IEnumerable Subscriptions;

    // ...Other Class Methods...

    [HttpPost]
    public void AddItem(int productVariantID)
    {
        var product = ProductService.GetByVariantID(productVariantID);

        if (product != null)
        {
            if (product.Type == (int)Constants.ProductTypes.Subscription)
                Subscriptions = Subscriptions.Concat(new [] { product });

            Products = Products.Concat(new [] { product });
            CartService.AddItem(productVariantID);
        }
    }

    [HttpPost]
    public void RemoveItem(int productVariantID)
    {
        Subscriptions = Subscriptions.Where(s => s.VariantID != productVariantID);
        Products = Products.Where(p => p.VariantID != productVariantID);

        CartService.RemoveItem(productVariantID);
    }

    public decimal GetCartTotalBeforeDiscount()
    {
        return Products.Sum(p => p.Price);
    }

    public IEnumerable GetCartItems()
    {
        var products = (from p in Products
                        select new CartSummaryItem
                        {
                           ProductID = p.ProductID,
                           Title = p.Title,
                           Description = p.Description,
                           Price = p.Price,
                           // ...Assign other applicable properties here...
                        } 
                        as ICartSummaryItem);

        return products;
    }

    // ...Other Class Methods...
}

对我来说似乎是“代码嗅觉”的部分(这里可能还有更多不好)是GetCartItems()方法。关于它的一些东西对我来说似乎很时髦,但我想不出一个更好的选择。

我正在转换为ICartItem,因为添加了一些需要传递给视图的属性,但它们对IStoreProductIStoreSubscription没有意义(接口)隔离原则)。

我考虑过向ConvertProductToCartItem()添加ConvertSubscriptionToCartItem()CartHelper方法,但这似乎违反了单一责任原则。拥有一个接受IStoreProductIStoreSubscription s并转换的CartItemFactory会有意义吗?对于这种简单的转换来说,这似乎是一个不必要的开销。

我提出的解决方案之一是定义一个显式的强制转换方法:

public class StoreProduct : IStoreProduct
{
    public decimal Price { get; set; }
    public decimal Discount { get; set; }
    // ...Properties...

    public ICartItem ToCartItem()
    {
        // Will call explicit cast implementation
        return (CartItem) this;
    }

    // Explicit cast conversion for Product as CartItem
    public static explicit operator CartItem(StoreProduct product)
    {
        return new CartItem()
        {
            Price = product.Price,
            Discount = product.Price,
            SalePrice = Helper.CalculateSalePriceForProduct(product),
            // ...Assign other applicable properties here...
        };
    }
}

让我将我的GetCartItems方法更改为更清晰的实现:

public IEnumerable GetCartItems()
{
    return Products.Select(p => p.ToCartSummaryItem());
}

但这种方法的问题在于,这也违反了单一责任原则,将ICartItemCartItemStoreProduct类联系起来。我也考虑过扩展方法而不是强制转换,但这并不是更好或不同。

我应该让我的具体StoreProduct类实现ICartItem接口并将特定于购物车的属性放在那里吗?也许我应该只重写CartHelper以便它只有ICartItems(即删除IProduct s)?这两种选择似乎都违反了单一责任原则。也许在我睡了之后解决方案会变得明显......

所以,我想我的问题归结为:在不违反SOLID原则的情况下,在接口之间移动数据的最佳方法是什么?

有什么建议吗?也许我应该继续前进而不用担心它(即,不要对SOLID说教)?我回答了自己的问题吗?也许这属于programmers.stackexchange,我希望这不是太主观。


另外,如果它有用,这就是我的界面:

public interface IProductBase
{
    int ProductID { get; set; }
    decimal Price { get; set; }
    string Title { get; set; }
    string Description { get; set; }
    // ... Other Properties...
}


public interface IStoreProduct : IProductBase
{
    int VariantID { get; set; }
    decimal Discount { get; set; }
    // ... Other Properties...

    ICartItem ToCartItem();
}


public interface ISubscription : IProductBase
{
    SubscriptionType SubscriptionType { get; set; }
    // ... Other Properties...

    ICartItem ToCartItem();
}


public interface ICartItem : IProductBase
{
    decimal SalePrice { get; set; }
    // ... Other Properties...
}

更新:为了清晰起见,添加了帖子属性。

3 个答案:

答案 0 :(得分:1)

我的简单回答是StoreProduct不是购物车商品。您可以拥有一个实现ICartItem的ProductCartItem。此实现将包含对IStoreProduct的引用。您的购物车商品包含产品没有的商品,如数量,折扣价格,当前价格等。

通过这种方式,您不会违反SRP或其他SOLID原则。您的购物车商品可能会记录产品添加到购物车时的价格,即使货架商品的价格发生变化也是如此。购物车项目也可以使用StoreProduct作为其upc,而无需将其存储在购物车项目中。

我认为这是您应该采用的方向,因为设计将允许您添加不是产品的购物车项目(例如ServiceCartItem)。

最后,您可能需要一些其他类型,可能是ProductCartItem工厂,它从产品构造新的购物车项目。这取决于向购物车添加内容涉及多少逻辑。

答案 1 :(得分:1)

什么是SRP?

我想强调来自SOLID的SRP,S是单一责任原则。意味着一个类/对象只有1个责任而不是更多。如果班级被改变了,那么只有1个理由,不再有。

的IEnumerable?

其次,为什么要使用IEnumerable(而不是List)并使用Concat?我找不到使用IEnumerableConcat优于List的优点。

现在让我们看一下CartHelper

的实现

首先来自命名,它是Helper。它是什么/什么是帮手?责任是什么?

<强>的AddItem

转到AddItem,我可以看到逻辑是:

  1. 使用提供的产品ID,使用服务获取产品。
  2. 如果产品存在且产品类型为Subscription,请添加到Subscription
  3. 最后,添加到Product列表和CartService
  4. 在我看来,这种方法做得太多了。我认为如果你将点1转移到其他地方会更好,或者甚至客户端都可以。它将使AddItem仅接受IProductBase并添加到Product列表中。怎么样Subscription列表?报废,你不需要它。使用LINQ where,您可以使用其type属性从Product列表中找到订阅的子集。

    从客户的角度来看,在AddItem期间找不到指定的产品时,此方法可以为用户提供什么?您没有throw任何例外或任何事情。客户只需添加product,他们就不会知道产品是否已添加。他们只希望一切都已完成,并且没有错误处理。

    我不明白CartService在做什么。但是我认为如果你想这样实现它会很好。

    <强>的removeItem

    AddItem相同,您可以废弃Subscription列表。关于传递产品ID或IProductBase类,它们是相互比较的。

    <强> GetCartTotalBeforeDiscount

    等等,这个类可以添加项目,删除项目,现在得到总数?在SRP之后,如果要更改折扣之前获取总计的逻辑,则需要修改此类。现在它有两个改变班级的理由,第一个是你想要改变它如何管理项目收集(添加/删除/获取)和第二个如何计算折扣前的总数。把这个责任转移到其他班级。如果您认为可以转换为在CartHelper中添加方法,请使用外观模式。

    <强> GetCartItems

    现在您想获得购物车商品。但是等等,你把它转换成一个特定的类而不是只返回它?当您将每个产品转换为特定类时,IProductBase接口的用途是什么?此外,现在改变班级有3个理由,现在以转换为原因。

    那么你想如何改进这个GetCartItems?更改为GetProducts并返回IEnumerable的{​​{1}},所有内容都在课程中完成。然后为产品转化创建一些IProductBase,例如:

    interfaces

    现在你可以自由转换。

    注意:对于这种情况,可能还有其他更好的模式,例如class CartItemConverter : IProductConverter<ICartItem>{ public IEnumerable<ICartItem> GetConverted(IEnumerable<IProductBase> products){ // logic here } } 。但恕我直言,这是最简单的一个,并且与你的代码具有最相似的结构。

答案 2 :(得分:0)

好的,感谢您的建议(Fendy,Andy,MrDosu),这里是更清洁的Facade实施:

public class Cart
{
    private List<ICartItem> CartItems;

    // ...Other Class Methods...

    [HttpPost]
    public void AddItem(int productVariantID)
    {
        var product = ProductService.GetByVariantID(productVariantID);

        if (product != null)
        {
            CartItems.Add(CartItemConverter.Convert(product));
            CartService.AddItem(productVariantID);
        }
    }

    [HttpPost]
    public void RemoveItem(int productVariantID)
    {
        CartItems.RemoveAll(c => c.VariantID == productVariantID);
        CartService.RemoveItem(productVariantID);
    }

    public IEnumerable<ICartItem> GetCartItems()
    {
        return CartItems;
    }

    // ...Other Class Methods...
}

由于在将商品添加到购物车后我只关心与购物车相关的属性,因此我可以删除对IStoreProduct和ISubscription的引用。如果我最终需要提取更多信息(这里适用YAGNI),我可以使用适配器模式(通过ProductService)填充必要的数据。