要避免的类(代码完成)

Classes to avoid (code complete)

提问人:adrianm 提问时间:1/19/2010 最后编辑:adrianm 更新时间:2/16/2010 访问量:1615

问:

我对代码完整书中的一段话有些困惑。

在“要避免的类”一节中,它写道:

“避免以动词命名的类 只有行为而没有数据的类通常不是真正的类。考虑将像 DatabaseInitialization() 或 StringBuilder() 这样的类转换为其他类的例程”

我的代码主要由没有数据的动词类组成。有发票阅读器、价格计算器、消息生成器等。我这样做是为了将每个类集中在一个任务上。然后,我将依赖项添加到其他类中以实现其他功能。

如果我正确理解了该段落,我应该使用如下代码

class Webservice : IInvoiceReader, IArticleReader {
    public IList<Invoice> GetInvoices();
    public IList<Article> GetArticles();
}

而不是

class InvoiceReader : IInvoiceReader {
    public InvoiceReader(IDataProvider dataProvider);
    public IList<Invoice> GetInvoices();
}

class ArticleReader : IArticleReader {
    public ArticleReader(IDataProvider dataProvider);
    public IList<Article> GetArticles();
}

编辑感谢您的所有回复。

我的结论是,我目前的代码比 OO 更像是 SRP,但它也受到“贫血领域模型”的影响。

我相信这些见解将来会对我有所帮助。

类设计 代码完成

评论

1赞 kdgregory 1/20/2010
我同意Thorarin的观点。类的存在是为了做某事。否则,他们只是自命不凡。struct
2赞 vgru 1/20/2010
实际上,在这个具体的例子中,确实有一些状态(私有字段)。InvoiceReaderArticleReaderdataProvider
1赞 jk. 1/20/2010
当然,这确实假设了一种纯粹的 OO 方法,您更喜欢动词的函子(因为它们正在伪造第一类函数),可能还有一些静态类
2赞 Michael Stum 1/20/2010
尽管我很喜欢 Code Complete,但我主要不同意他的例子。 是一个很好的类名。我认为他应该作为一个反面的例子。StringBuilderBuildString()
0赞 Michael Damatov 1/23/2010
这应该是一个社区维基。

答:

0赞 Ewan Todd 1/19/2010 #1

少关注名称。关于名称的规则只是不良做法的经验法则指标。重要的一点是:

只有行为而没有数据的类通常不是真正的类

在您的例子中,看起来您的类同时具有数据和行为,并且它们也可以称为“Invoice”和“Article”。

评论

0赞 adrianm 1/19/2010
没有。InvoiceReader 只分析数据流并创建一个 Invoice 对象。
0赞 Thorarin 1/20/2010
@Jeff:除了那是错误的方法。数据访问对象通常适用于特定的提供程序。应该有某种抽象工厂,它基于提供者实例实例化 DAO。
0赞 Jeff Sternal 1/20/2010
@Thorain - 呵呵,对不起,我在将其升级为答案的过程中删除了我的评论。是的,我同意 - 我并不是要暗示任何关于提供程序实现的特别信息。抽象工厂就可以了。
0赞 phkahler 1/20/2010
@adrianm:如果 InvoiceReader 只是处理数据,为什么它是一个类而不仅仅是一个函数?Invoice.CreateFromDataStream()有什么问题?
3赞 Stefano Borini 1/20/2010
你可以有纯粹的行为类(就像策略类一样)
0赞 iandisme 1/19/2010 #2

这要视情况而定。存在许多以 Read 和 Write 谓词命名的类,因为这些类还会创建、维护和表示与它们正在读取或写入的数据源的连接。如果你的班级正在这样做,最好将它们分开。

如果 Reader 对象只包含解析逻辑,那么将类转换为实用工具方法是要走的路。不过,我会使用比 Webservice 更具描述性的名称。

3赞 Mongus Pong 1/19/2010 #3

从本质上讲,这本书所说的是,面向对象设计是关于提取对象(名词)并识别发生在这些对象上和对象之间的操作(动词)。

名词成为宾语,动词成为对这些对象进行操作的方法。

这个想法是,

更接近程序模型的真实 世界问题,程序越好 会的。

在实践中,对象的有用之处在于它可以表示特定的状态。然后,您可以让此类的几个不同实例,每个实例都持有不同的状态来表示问题的某些方面。

对于 InvoiceReader 类

  • 您只会创建一个实例
  • 它表示的唯一状态是包含 dataProvider 的状态
  • 它只包含一种方法

将其放置在对象中没有任何优势。

0赞 Ariel 1/19/2010 #4

我认为这本书提出了一个这样的设计:

class Article : IIArticleReader
{
    // Article data...

    public IList<Article> GetArticles(); 
}

评论

0赞 Thorarin 1/20/2010
虽然这更符合引用段落所建议的结构,但我不认为这是该建议的预期结果:)
0赞 Ariel 1/20/2010
可能是也可能不是,这是可以解释的。
12赞 Aaronaught 1/20/2010 #5

就我个人而言,我忽略了这个“规则”。.NET Framework 本身充满了“动词”类:、、、、如果你认为框架设计得很糟糕,那么无论如何,不要使用这样的名称。TextReaderBinaryWriterXmlSerializerCodeGeneratorStringEnumeratorHttpListenerTraceListenerConfigurationManagerTypeConverterRoleProvider

史蒂夫的意图是可以理解的。如果你发现自己创建了几十个类只是为了执行一个特定的任务,这可能是一个贫血领域模型的迹象,在这个模型中,应该能够自己做这些事情的对象却没有。但在某些时候,你必须在“纯”OOP 和 SRP 之间做出选择。

我的建议是这样的:如果你发现自己创建了一个作用于单个“名词”类的“动词”类,请诚实地考虑“名词”类是否可以自行执行该动作。但是,不要为了避免动词类而开始创造上帝对象或想出无意义/误导性的名称。

评论

11赞 1/20/2010
NET框架就是这样——一个框架。大多数人不是在编写框架,他们的类名称应该反映这一点。
4赞 Coincoin 1/20/2010 #6

请注意使用“避免”一词。如果你曾经使用过它们,它不会消除、根除或在地狱中燃烧。

作者的意思是,如果你发现自己有一堆以动词命名的类,而你碰巧做的只是静态地创建这些类,调用一个函数并忘记它们,这可能表明你分离了太多的类关注点。

但是,在某些情况下,创建类来实现操作是一件好事,例如,当您对同一操作具有不同的策略时。一个很好的例子是 IComparer<>。它所做的只是比较两件事,但有几种比较事物的方法。

正如作者所建议的,在这些情况下,一个很好的方法是创建一个接口并实现它。IComparer<>再次浮现在脑海中。

另一种常见情况是操作具有繁重状态,例如加载文件。将状态封装在一个类中可能是合理的。

21赞 Ash 1/20/2010 #7

InvoiceReader、PriceCalculator、MessageBuilder、ArticleReader、InvoiceReader 等类名实际上不是动词名称。它们实际上是“名词代理-名词”类名。请参阅代理名词

动词类名类似于 Validate、Operate、Manage 等。显然,这些更好地用作方法,并且作为类名会非常尴尬。

“名词代理-名词”类名的最大问题是,它们对类的实际作用几乎没有意义(例如UserManager,DataProcessor等)。因此,他们更容易臃肿并失去内部凝聚力。(见单一责任原则)。

因此,具有 IInvoiceReader 和 IArticleReader 接口的 WebService 类可能是更清晰、更有意义的 OO 设计。

这为您提供了简单、明显的名词类名称“WebService”,以及“名词代理-名词”接口名称,这些接口名称清楚地宣传了 WebService 类可以为调用者做什么。

您可能还可以通过在另一个名词前面添加前缀来为实际类赋予更多含义,例如 PaymentWebService。

但是,在更具体地描述类可以为调用者做什么方面,接口总是比单个类名更好。随着类变得越来越复杂,还可以使用有意义的名称添加新接口。

评论

2赞 Aaronaught 1/20/2010
我同意你在这里所说的一切,尽管书中的例子确实提到了代理名词,如(具有讽刺意味的是,.NET Framework 实际上有这个确切的类)。 告诉我更多关于这门课的目的。StringBuilderInvoiceReaderWebService
3赞 Ash 1/20/2010
@Aaronaught,是的,我可能会使用比 WebService 更具体的名称,例如 PaymentWebService 来提供更多的 meining。但是,接口是描述类功能的关键部分。它们可以更准确地、更精细地执行此操作,随着类大小的增长(即可以添加更多接口),甚至更是如此。
1赞 Ash 1/20/2010
@Jeff,是的,最重要的可能是在命名方法上达成共识,然后坚持下去。即使用哪种命名样式的一致性。
5赞 Daniel Daranas 1/20/2010 #8

不要盲目听从任何建议。这些只是指导方针。

也就是说,名词是非常好的类名,只要它们对逻辑对象进行建模。由于 “Person” 类是所有 “Person” 对象的蓝图,因此将其命名为 “Person” 非常方便,因为它允许您进行这样的推理:“我将根据用户的输入创建一个 Person,但首先我需要验证它......”

2赞 vgru 1/20/2010 #9

只有行为而没有数据的类通常不是真正的类。 这句话是完全错误的。

在重构中,将行为提取到单独的类中是一件好事且很常见的事情。它可以有状态,但也不需要有状态。你需要有干净的接口,并在你认为必要的时候实现它们。

此外,无状态类非常适合您只需要在短时间内进行的计算。你实例化它们(或者,请求某种工厂来获取它们),进行必要的计算,然后把它们扔进垃圾桶。您可以随时随地获得适当的行为“版本”。

通常我发现接口的不同实现具有某种状态(例如,在构造函数中设置),但有时类的类型可以完全确定其行为。

例如:

public interface IExporter
{
    /// <summary>
    /// Transforms the specified export data into a text stream.
    /// </summary>
    /// <param name="exportData">The export data.</param>
    /// <param name="outputFile">The output file.</param>
    void Transform(IExportData exportData, string outputFile);
}

可以作为

class TabDelimitedExporter : IExporter { ... }
class CsvExporter : IExporter { ... }
class ExcelExporter : IExporter { ... }

要实现从 CSV 文件导出(无论它是什么),您可能根本不需要任何状态。另一方面,可以具有导出选项的各种属性,但也可以是无状态的。IExportDataExcelExporter

[编辑]

移动并进入类意味着要将它们的实现绑定到 WebService 类型。将它们放在单独的类中将允许您对发票和物品进行不同的实现。总的来说,将它们分开似乎更好。GetInvoicesGetArticlesWebService

0赞 Daniel Roseman 1/21/2010 #10

这是OO中“动词与名词”的经典演绎:

http://steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html