0

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


我可能想多了,我不打算在 SOLID 原则方面教条;但我想得到一些意见。我一直在将购物车重构为更加“可靠”,并且我编写的一种方法对我来说似乎是“代码味道”(也许不是)。

我有一个CartHelper看起来像这样的类(为简洁起见,我对其进行了一些简化):

公共类 CartHelper
{
    IEnumerable 产品;
    IEnumerable 订阅;

    // ...其他类方法...

    [HttpPost]
    公共无效 AddItem(int productVariantID)
    {
        var product = ProductService.GetByVariantID(productVariantID);

        如果(产品!= null)
        {
            if (product.Type == (int)Constants.ProductTypes.Subscription)
                订阅 = 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);
    }

    公共小数 GetCartTotalBeforeDiscount()
    {
        返回 Products.Sum(p => p.Price);
    }

    公共 IEnumerable GetCartItems()
    {
        var products =(来自 Products 中的 p
                        选择新的 CartSummaryItem
                        {
                           ProductID = p.ProductID,
                           标题 = p.标题,
                           描述 = p.描述,
                           价格 = p.Price,
                           // ...在此处分配其他适用的属性...
                        }
                        作为 ICartSummaryItem);

        退货;
    }

    // ...其他类方法...
}

对我来说似乎是“代码气味”的部分(这里可能还有更多不好的部分)是GetCartItems()方法。关于它的某些东西对我来说似乎很时髦,但我想不出更好的替代方案。

我正在转换为,ICartItem因为添加了一些需要传递给视图的属性,但它们在 an IStoreProductor an IStoreSubscription(Interface Segregation Principle) 上没有意义。

我考虑过在 中添加ConvertProductToCartItem()ConvertSubscriptionToCartItem()方法CartHelper,但这似乎违反了单一责任原则。IStoreProduct拥有一个接受s 和IStoreSubscriptions 并进行转换的 CartItemFactory 是否有意义?对于如此简单的转换,这似乎是很多不必要的开销。

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

公共类 StoreProduct : IStoreProduct
{
    公共小数价格 { 得到;放; }
    公共小数折扣 { 得到;放; }
    // ...特性...

    公共 ICartItem ToCartItem()
    {
        // 将调用显式转换实现
        返回(CartItem)这个;
    }

    // 将 Product 显式转换为 CartItem
    公共静态显式运算符 CartItem(StoreProduct 产品)
    {
        返回新的 CartItem()
        {
            价格 = 产品.价格,
            折扣 = 产品.价格,
            SalePrice = Helper.CalculateSalePriceForProduct(product),
            // ...在此处分配其他适用的属性...
        };
    }
}

让我将我的GetCartItems方法更改为更简洁的实现:

公共 IEnumerable GetCartItems()
{
    返回 Products.Select(p => p.ToCartSummaryItem());
}

但是这种方法的问题在于,这也违反了单一职责原则,因为它与ICartItem类的耦合。我还考虑了一种扩展方法而不是强制转换,但这并没有更好或不同。CartItemStoreProduct

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

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

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


此外,如果它有帮助,这就是我的界面的样子:

公共接口 IProductBase
{
    int ProductID { 获取;放; }
    小数价格 { 得到;放; }
    字符串标题 { 获取;放; }
    字符串描述 { 获取;放; }
    // ... 其他属性...
}


公共接口 IStoreProduct : IProductBase
{
    int VariantID { 得到;放; }
    小数折扣 { 得到;放; }
    // ... 其他属性...

    ICartItem ToCartItem();
}


公共接口 ISubscription : IProductBase
{
    订阅类型订阅类型 { 获取;放; }
    // ... 其他属性...

    ICartItem ToCartItem();
}


公共接口 ICartItem : IProductBase
{
    小数销售价格 { 得到;放; }
    // ... 其他属性...
}

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

4

3 回答 3

1

什么是 SRP?

我想强调的是,SOLID 的 SRP、S 是单一责任原则。意味着一个类/对象只有一个责任,没有更多。如果班级发生变化,那么只有1个原因,仅此而已。

可数?

其次,你为什么使用IEnumerable(而不是List)和使用Concat?我找不到使用IEnumerableand Concatover的优点List

现在让我们看看您的CartHelper.

首先从命名上来说,它是Helper。它是什么/什么是助手?责任是什么?

添加项目

移动到AddItem,我可以看到逻辑是:

  1. 使用提供的产品 ID,使用服务获取产品。
  2. 如果产品存在且产品类型为Subscription,则添加到Subscription.
  3. 最后,添加到Product列表和CartService.

在我看来,方法做得太多了。我认为如果您将点 1 移到其他地方会更好,或者甚至客户也可以。它将使 AddItem 只接受IProductBase并添加到Product列表中。Subscription清单呢?废了,你不需要。使用 LINQ ,您可以使用其 type 属性where从列表中找到订阅的子集。Product

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

我不明白在CartService做什么。但是,我认为如果您想这样实现就可以了。

除去项目

与 相同AddItem,您可以取消Subscription列表。关于传递产品 id 或IProductBase类,它是相互可比的。

GetCartTotalBeforeDiscount

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

获取购物车项目

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

那么你想如何改进这个 GetCartItems 呢?换回of GetProducts,一切都在课堂上完成。然后为产品转换创建一些,例如:IEnumerableIProductBaseinterfaces

class CartItemConverter : IProductConverter<ICartItem>{
    public IEnumerable<ICartItem> GetConverted(IEnumerable<IProductBase> products){
        // logic here
    }
}

现在你有皈依的自由。

Decorator注意:对于这种情况,可能还有其他更好的模式。但恕我直言,这是最简单的一个,并且与您的代码具有最相似的结构。

于 2013-07-19T01:41:20.840 回答
1

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

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

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

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

于 2013-07-18T23:24:28.857 回答
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)来填充必要的数据。

于 2013-07-19T16:22:41.107 回答