SOLID与静态方法


11

这是我经常遇到的一个问题:假设有一个具有Product类的网上商店项目。我想添加一个功能,允许用户对产品发表评论。所以我有一个Review类,它引用一个产品。现在,我需要一种列出所有产品评论的方法。有两种可能性:

(一种)

public class Product {
  ...
  public Collection<Review> getReviews() {...}
}

(B)

public class Review {
  ...
  static public Collection<Review> forProduct( Product product ) {...}
}

通过查看代码,我将选择(A):它不是静态的,并且不需要参数。但是,我认为(A)违反了单一责任原则(SRP)和开放式封闭原则(OCP),而(B)没有违反:

  • (SRP)当我想更改收集产品评论的方式时,我必须更改产品类。但是更改产品类的原因应该只有一个。那当然不是评论。如果我在产品中打包了与产品有关的所有功能,它将很快崩溃。

  • (OCP)我必须更改Product类以使用此功能对其进行扩展。我认为这违反了原则的“为变革而封闭”部分。在我收到客户的要求以实施评论之前,我认为产品已完成,然后“关闭”。

更重要的是:遵循SOLID原则,还是拥有更简单的界面?

还是我在这里做错了什么?

结果

哇,谢谢您的所有精彩回答!很难选择一个作为官方答案。

让我总结一下答案中的主要论点:

  • 赞成(A):OCP不是法律,代码的可读性也很重要。
  • 亲(A):实体关系应该是可导航的。两个类都可能知道这种关系。
  • pro(A)+(B):同时执行并将(A)中的任务委派给(B),这样就不太可能再次更改产品。
  • pro(C):将finder方法放在非静态的第三类(服务)中。
  • 反对(B):阻止测试中的嘲笑。

我的大学在工作中还提供了一些其他功能:

  • 亲(B):我们的ORM框架可以自动生成(B)的代码。
  • 赞成(A):由于我们ORM框架的技术原因,在某些情况下,有必要独立于发现者去向而更改“封闭”实体。因此,无论如何,我将始终无法坚持使用SOLID。
  • 对比(C):大惊小怪;-)

结论

我在当前项目中同时使用(A)+(B)和委派。但是,在面向服务的环境中,我将选择(C)。


2
只要它不是静态变量,一切都会很酷。静态方法易于测试且易于跟踪。
编码员2012年

3
为什么不只具有ProductsReviews类?然后产品和评论保持不变。也许我误会了。
ElGringoGrande 2012年

2
@Coder“静态方法很容易测试”,真的吗?无法嘲笑它们,请参阅:googletesting.blogspot.com/2008/12/…了解更多详细信息。
StuperUser 2012年

1
@StuperUser:没有什么可嘲笑的。Assert(5 = Math.Abs(-5));
编码员

2
测试Abs()不是问题,测试依赖它的东西是问题。您没有接缝可用于隔离依赖的代码下测试(CUT)以使用模拟。这意味着您不能将其作为原子单元进行测试,并且所有测试都将成为测试单元逻辑的集成测试。测试失败可能发生在CUT或Abs()(或其相关代码)中,并且消除了单元测试的诊断优势。
StuperUser 2012年

Answers:


7

更重要的是:遵循SOLID原则,还是拥有更简单的界面?

相对于SOLID的界面

这些不是互斥的。该界面应以业务模型的形式理想地表达您的业务模型的性质。SOLID原则是Koan的一种,用于最大化面向对象的代码可维护性(我的意思是广义上的“可维护性”)。前者支持使用和操纵您的业务模型,而后者则可以优化代码维护。

开/关原则

“别碰那个!” 过于简单的解释。并假设我们的意思是“阶级”是任意的,不一定是正确的。相反,OCP意味着您已经设计了代码,因此修改(不应该)不要求您直接修改现有的工作代码。此外,首先不要触摸代码是保持现有接口完整性的理想方法。我认为这是OCP的重要推论。

最后,我将OCP视为现有设计质量的指标。如果我发现自己经常破解开放类(或方法),并且/或者没有足够扎实的理由(哈哈),那么这可能是在告诉我我的设计不好(和/或我没有这样做)知道如何编码OO)。

全力以赴,我们有一支由医生组成的团队

如果您的需求分析告诉您您需要从两个角度表达产品-审阅关系,则可以这样做。

因此,沃尔夫冈,您可能有充分的理由来修改那些现有的类。给定新的要求,如果“审核”现在是产品的基本组成部分,则“产品”的每个扩展都需要审核,如果这样做会使客户代码具有适当的表达能力,则将其集成到“产品”中。


1
+1表示OCP可以更好地指示质量,而不是任何代码的快速规则。如果发现很难或不可能正确地遵循OCP,则表明您的模型需要重构以允许更灵活的重用。
CodexArcanum 2012年

我选择了Product and Review的示例来指出,Product是项目的核心实体,而Review只是一个附加组件。因此,产品已经存在,已经完成,并且审核将在以后推出,因此应在不打开现有代码(包括产品)的情况下进行介绍。
沃尔夫冈

10

SOLID是指导原则,因此影响决策而不是命令决策。

使用静态方法时要注意的一件事是它们对可测试性的影响。


测试forProduct(Product product)不会有问题。

测试依赖它的东西。

您没有接缝可以隔离要测试的依赖代码测试(CUT),因为当应用程序运行时,静态方法必定存在。

让我们叫的方法CUT()是调用forProduct()

如果forProduct()static,则不能CUT()作为原子单元进行测试,并且所有测试都将成为测试单元逻辑的集成测试。

在用于切割的试验失败了,可以通过在一个问题引起的CUT()或在forProduct()(或任何其相关代码),该去除单元测试的诊断好处。

有关更多详细信息,请参见这篇出色的博客文章:http : //googletesting.blogspot.com/2008/12/static-methods-are-death-to-testability.html


这可能会导致失败的测试失败,并放弃良好实践和周围的好处。


1
这是非常好的一点。总的来说,虽然不适合我。;-)对于编写涵盖多个类的单元测试,我并不那么严格。他们都进入数据库,对我来说这没关系。因此,我不会嘲笑业务对象或它的发现者。
沃尔夫冈

+1,感谢您为测试设置了红旗!我同意@Wolfgang的观点,通常我也不太严格,但是当我需要那种测试时,我真的很讨厌静态方法。通常,如果静态方法与其参数交互过多,或者与任何其他静态方法交互,我都希望将其设为实例方法。
阿德里亚诺·雷佩蒂

这不完全取决于所使用的语言吗?OP使用Java示例,但未在问题中提及语言,也未在标记中指定语言。
Izkata 2012年

1
@StuperUser- If forProduct() is static you can't test CUT() as an atomic unit and all of your tests become integration tests that test unit logic.我相信Javascript和Python都允许重写/ 模拟静态方法。不过,我不确定100%。
Izkata 2012年

1
@Izkata JS是动态类型的,因此没有static,您可以使用闭包和单例模式对其进行仿真。在Python上阅读(尤其是stackoverflow.com/questions/893015/…),您必须继承并扩展。覆盖不是嘲笑;看起来您仍然没有接缝可以将代码作为原子单元进行测试。
StuperUser 2012年

4

如果您认为该产品是找到该产品的评论的正确位置,则可以随时为该产品提供帮助类,以帮助其完成工作。(您可以说出来,因为您的企业永远不会谈论评论,除非涉及产品)。

例如,我很想注入一些扮演评论检索者角色的东西。我可能会给它接口IRetrieveReviews。您可以将其放在产品的构造函数中(依赖注入)。如果您想更改检索评论的方式,则可以通过注入其他合作者(a TwitterReviewRetriever或an AmazonReviewRetrieverMultipleSourceReviewRetriever您需要的其他任何东西)来轻松实现。

两者现在都负有单一责任(分别负责与产品相关的所有事情,并分别获取评论),并且将来可以在不实际更改产品的情况下修改产品相对于评论的行为(您可以扩展它例如,ProductWithReviews如果您真的想对SOLID原则be之以鼻,但这对我来说已经足够了。


听起来像DAO模式,在面向服务/组件的软件中非常常见。我喜欢这个想法,因为它表示检索对象不是这些对象的责任。但是,由于我倾向于面向对象的方式而不是面向服务的方式。
沃尔夫冈

1
使用像这样的界面就IRetrieveReviews可以使其停止面向服务-它无法确定获得评论的内容,方式或时间。也许这是一种具有很多类似方法的服务。也许这是一类要做的事情。可能是存储库,或者是向服务器发出HTTP请求。你不知道 你不知道 这才是重点。
鲁尼武尔2012年

是的,所以这将是策略模式的一种实现。这将是第三类的争论。(A)和(B)不支持这一点。查找程序肯定会使用ORM,因此没有理由替换算法。抱歉,如果我不清楚我的问题。
沃尔夫冈

3

我将有一个ProductsReview类。您说评论仍然是新的。这并不意味着它可以是任何东西。它仍然只有一个改变的理由。如果出于任何原因更改了获取评论的方式,则必须更改Review类。

那是不对的。

您将静态方法放在Review类中是因为...为什么?那不是你在努力吗?这不是整个问题吗?

那不要 安排一个班级负责获取产品评论。然后,您可以将其子类化为ProductReviewsByStartRating。或对其进行子类化以获得有关某类产品的评论。


我不同意在Review上使用方法会违反SRP。在产品上,它将在但不进行审查。我的问题是它是静态的。如果我将方法移到某些第三类,它将仍然是静态的并具有product参数。
沃尔夫冈

因此,您说的是,对Review类的唯一责任,对其进行更改的唯一原因是是否需要更改forProduct静态方法?Review类中没有其他功能吗?
ElGringoGrande 2012年

评论上有很多东西。但是我认为forProduct(Product)非常适合它。查找评论很大程度上取决于评论的属性和结构(哪些唯一标识符,哪些范围会更改属性?)。但是,该产品不了解也不应该了解评论。
沃尔夫冈

3

我不会在Product类或Review类中都使用“获取产品评论”功能...

您有一个检索产品的地方,对吗?与某物有关GetProductById(int productId),也许GetProductsByCategory(int categoryId)等等。

同样,您应该有一个检索您的评论的位置,并带有GetReviewbyId(int reviewId),可能还有一个GetReviewsForProduct(int productId)

当我想更改收集产品评论的方式时,我必须更改产品类。

如果您将数据访问权限与域类分开,则在更改评论的收集方式时无需更改任何一个域类。


这就是我的处理方式,直到您发帖之前都没有这个答案,我感到困惑(并担心自己的想法是否正确)。显然,代表报告的类别或产品的代表都不应该真正负责检索另一个类别。某种数据提供服务应该在处理这个问题。
CodexArcanum 2012年

@CodexArcanum好吧,您并不孤单。:-)
埃里克·金

2

模式和原则是准则,而不是一成不变的规则。我认为问题不是遵循SOLID原则还是保持简单的界面是更好的选择。你应该问自己什么是更可读易懂的大部分人。通常,这意味着它必须尽可能靠近域。

在这种情况下,我希望使用解决方案(B),因为对我而言 ,出发点是产品,而不是评论,而是想象您正在编写用于管理评论的软件。在那种情况下,中心是“评论”,因此解决方案(A)可能更可取。

当我有很多这样的方法(类之间的“连接”)时,我将它们全部剥离,然后创建一个(或多个)新的静态类来组织它们。通常,您可以将它们视为查询或某种存储库。


我也在考虑这种关于两端语义的想法,哪一个是“更强的”,所以它会要求发现者。这就是为什么我想到了(B)。这也将有助于创建业务类“抽象”的层次结构。产品只是一个基本对象,而评论是较高的对象。因此,“评论”可以引用“产品”,但不能反过来。这样,可以避免业​​务类之间的引用循环。这将是我列表中另一个已解决的问题。
沃尔夫冈

我认为对于一小类类,即使同时提供方法(A)和(B)也可能很好。在编写此逻辑时,UI尚不为人所知。
阿德里亚诺·雷佩蒂

1

您的产品只能委托给您的静态Review方法,在这种情况下,您将在自然位置(Product.getReviews)提供方便的界面,但是实现细节在Review.getForProduct中。

SOLID是指导原则,应形成一个简单而合理的界面。或者,您可以从简单,合理的界面派生SOLID。一切都与代码中的依赖管理有关。目标是最大程度地减少造成摩擦并为不可避免的变更创造障碍的依赖性。


我不确定我是否想要这个。这样,我在两个地方都有一个很好的方法,因为其他开发人员不需要在两个类中都去看我放在哪里。另一方面,我既有静态方法的缺点,也有“封闭的” Product类的更改的缺点。我不确定代表团是否会解决摩擦问题。如果有理由更换取景器,则肯定会导致签名的更改,从而又导致产品的更改。
沃尔夫冈

OCP的关键是您可以在不更改代码的情况下替换实现。实际上,这与Liskov换人紧密相关。一个实现必须可以替代另一个实现。您可能将DatabaseReviews和SoapReviews作为实现IGetReviews.getReviews和getReviewsForProducts的不同类。然后,您的系统将打开以进行扩展(更改评论的获取方式),并关闭以进行修改(不中断对IGetReviews的依赖)。这就是我所说的依赖管理。在您的情况下,您将必须修改代码以更改其行为。
普里斯

这不是您所描述的依赖倒置原则(DIP)吗?
沃尔夫冈

它们是相关的原则。您的替代点是通过DIP实现的。
普里斯

1

与大多数其他答案相比,我的看法略有不同。我认为,ProductReview基本数据传输对象(DTO的)。在我的代码中,我尝试使DTO /实体避免出现行为。它们只是一个很好的API,用于存储模型的当前状态。

当您谈论OO和SOLID时,通常是在谈论一个“对象”,它不(必须)代表状态,而是代表某种为您解答问题或可以委派某些工作的服务。例如:

interface IProductRepository
{
    void SaveNewProduct(IProduct product);
    IProduct GetProductById(ProductId productId);
    bool TryGetProductByName(string name, out IProduct product);
}

interface IProduct
{
    ProductId Id { get; }
    string Name { get; }
}

class ExistingProduct : IProduct
{
    public ProductId Id { get; private set; }
    public string Name { get; private set; }
}

那么你的实际ProductRepository将返回ExistingProductGetProductByProductId方法等。

现在,您遵循单一责任原则(继承自的IProduct内容只是坚持状态,而继承自的内容IProductRepository则是负责了解如何持久化和补充数据模型的责任)。

如果更改数据库架构,则可以更改存储库实现,而无需更改DTO等。

简而言之,我想我不会选择您的任何一个。:)


0

静态方法可能更难测试,但这并不意味着您不能使用它们-您只需要设置它们就无需测试它们。

将product.GetReviews和Review.ForProduct都做成一行方法,就像

new ReviewService().GetReviews(productID);

ReviewService包含所有更复杂的代码,并具有专为可测试性设计的接口,但并不直接向用户公开。

如果您必须具有100%的覆盖率,请进行集成测试以调用product / review类方法。

如果您考虑使用公共API设计而不是类设计,那么可能会有所帮助-在这种情况下,具有逻辑分组的简单接口就很重要-实际的代码结构仅对开发人员重要。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.