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
,因为添加了一些需要传递给视图的属性,但它们对IStoreProduct
或IStoreSubscription
没有意义(接口)隔离原则)。
我考虑过向ConvertProductToCartItem()
添加ConvertSubscriptionToCartItem()
和CartHelper
方法,但这似乎违反了单一责任原则。拥有一个接受IStoreProduct
和IStoreSubscription
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()); }
但这种方法的问题在于,这也违反了单一责任原则,将ICartItem
和CartItem
与StoreProduct
类联系起来。我也考虑过扩展方法而不是强制转换,但这并不是更好或不同。
我应该让我的具体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... }
更新:为了清晰起见,添加了帖子属性。
答案 0 :(得分:1)
我的简单回答是StoreProduct不是购物车商品。您可以拥有一个实现ICartItem的ProductCartItem。此实现将包含对IStoreProduct的引用。您的购物车商品包含产品没有的商品,如数量,折扣价格,当前价格等。
通过这种方式,您不会违反SRP或其他SOLID原则。您的购物车商品可能会记录产品添加到购物车时的价格,即使货架商品的价格发生变化也是如此。购物车项目也可以使用StoreProduct作为其upc,而无需将其存储在购物车项目中。
我认为这是您应该采用的方向,因为设计将允许您添加不是产品的购物车项目(例如ServiceCartItem)。
最后,您可能需要一些其他类型,可能是ProductCartItem工厂,它从产品构造新的购物车项目。这取决于向购物车添加内容涉及多少逻辑。
答案 1 :(得分:1)
我想强调来自SOLID的SRP,S是单一责任原则。意味着一个类/对象只有1个责任而不是更多。如果班级被改变了,那么只有1个理由,不再有。
其次,为什么要使用IEnumerable
(而不是List
)并使用Concat
?我找不到使用IEnumerable
和Concat
优于List
的优点。
CartHelper
。首先来自命名,它是Helper
。它是什么/什么是帮手?责任是什么?
<强>的AddItem 强>
转到AddItem
,我可以看到逻辑是:
Subscription
,请添加到Subscription
。Product
列表和CartService
。在我看来,这种方法做得太多了。我认为如果你将点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)填充必要的数据。