提问人:adrianm 提问时间:1/19/2010 最后编辑:adrianm 更新时间:2/16/2010 访问量:1615
要避免的类(代码完成)
Classes to avoid (code complete)
问:
我对代码完整书中的一段话有些困惑。
在“要避免的类”一节中,它写道:
“避免以动词命名的类 只有行为而没有数据的类通常不是真正的类。考虑将像 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,但它也受到“贫血领域模型”的影响。
我相信这些见解将来会对我有所帮助。
答:
少关注名称。关于名称的规则只是不良做法的经验法则指标。重要的一点是:
只有行为而没有数据的类通常不是真正的类
在您的例子中,看起来您的类同时具有数据和行为,并且它们也可以称为“Invoice”和“Article”。
评论
这要视情况而定。存在许多以 Read 和 Write 谓词命名的类,因为这些类还会创建、维护和表示与它们正在读取或写入的数据源的连接。如果你的班级正在这样做,最好将它们分开。
如果 Reader 对象只包含解析逻辑,那么将类转换为实用工具方法是要走的路。不过,我会使用比 Webservice 更具描述性的名称。
从本质上讲,这本书所说的是,面向对象设计是关于提取对象(名词)并识别发生在这些对象上和对象之间的操作(动词)。
名词成为宾语,动词成为对这些对象进行操作的方法。
这个想法是,
更接近程序模型的真实 世界问题,程序越好 会的。
在实践中,对象的有用之处在于它可以表示特定的状态。然后,您可以让此类的几个不同实例,每个实例都持有不同的状态来表示问题的某些方面。
对于 InvoiceReader 类
- 您只会创建一个实例
- 它表示的唯一状态是包含 dataProvider 的状态
- 它只包含一种方法
将其放置在对象中没有任何优势。
我认为这本书提出了一个这样的设计:
class Article : IIArticleReader
{
// Article data...
public IList<Article> GetArticles();
}
评论
就我个人而言,我忽略了这个“规则”。.NET Framework 本身充满了“动词”类:、、、、如果你认为框架设计得很糟糕,那么无论如何,不要使用这样的名称。TextReader
BinaryWriter
XmlSerializer
CodeGenerator
StringEnumerator
HttpListener
TraceListener
ConfigurationManager
TypeConverter
RoleProvider
史蒂夫的意图是可以理解的。如果你发现自己创建了几十个类只是为了执行一个特定的任务,这可能是一个贫血领域模型的迹象,在这个模型中,应该能够自己做这些事情的对象却没有。但在某些时候,你必须在“纯”OOP 和 SRP 之间做出选择。
我的建议是这样的:如果你发现自己创建了一个作用于单个“名词”类的“动词”类,请诚实地考虑“名词”类是否可以自行执行该动作。但是,不要为了避免动词类而开始创造上帝对象或想出无意义/误导性的名称。
评论
请注意使用“避免”一词。如果你曾经使用过它们,它不会消除、根除或在地狱中燃烧。
作者的意思是,如果你发现自己有一堆以动词命名的类,而你碰巧做的只是静态地创建这些类,调用一个函数并忘记它们,这可能表明你分离了太多的类关注点。
但是,在某些情况下,创建类来实现操作是一件好事,例如,当您对同一操作具有不同的策略时。一个很好的例子是 IComparer<>。它所做的只是比较两件事,但有几种比较事物的方法。
正如作者所建议的,在这些情况下,一个很好的方法是创建一个接口并实现它。IComparer<>再次浮现在脑海中。
另一种常见情况是操作具有繁重状态,例如加载文件。将状态封装在一个类中可能是合理的。
InvoiceReader、PriceCalculator、MessageBuilder、ArticleReader、InvoiceReader 等类名实际上不是动词名称。它们实际上是“名词代理-名词”类名。请参阅代理名词。
动词类名类似于 Validate、Operate、Manage 等。显然,这些更好地用作方法,并且作为类名会非常尴尬。
“名词代理-名词”类名的最大问题是,它们对类的实际作用几乎没有意义(例如UserManager,DataProcessor等)。因此,他们更容易臃肿并失去内部凝聚力。(见单一责任原则)。
因此,具有 IInvoiceReader 和 IArticleReader 接口的 WebService 类可能是更清晰、更有意义的 OO 设计。
这为您提供了简单、明显的名词类名称“WebService”,以及“名词代理-名词”接口名称,这些接口名称清楚地宣传了 WebService 类可以为调用者做什么。
您可能还可以通过在另一个名词前面添加前缀来为实际类赋予更多含义,例如 PaymentWebService。
但是,在更具体地描述类可以为调用者做什么方面,接口总是比单个类名更好。随着类变得越来越复杂,还可以使用有意义的名称添加新接口。
评论
StringBuilder
InvoiceReader
WebService
不要盲目听从任何建议。这些只是指导方针。
也就是说,名词是非常好的类名,只要它们对逻辑对象进行建模。由于 “Person” 类是所有 “Person” 对象的蓝图,因此将其命名为 “Person” 非常方便,因为它允许您进行这样的推理:“我将根据用户的输入创建一个 Person,但首先我需要验证它......”
只有行为而没有数据的类通常不是真正的类。 这句话是完全错误的。
在重构中,将行为提取到单独的类中是一件好事且很常见的事情。它可以有状态,但也不需要有状态。你需要有干净的接口,并在你认为必要的时候实现它们。
此外,无状态类非常适合您只需要在短时间内进行的计算。你实例化它们(或者,请求某种工厂来获取它们),进行必要的计算,然后把它们扔进垃圾桶。您可以随时随地获得适当的行为“版本”。
通常我发现接口的不同实现具有某种状态(例如,在构造函数中设置),但有时类的类型可以完全确定其行为。
例如:
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 文件导出(无论它是什么),您可能根本不需要任何状态。另一方面,可以具有导出选项的各种属性,但也可以是无状态的。IExportData
ExcelExporter
[编辑]
移动并进入类意味着要将它们的实现绑定到 WebService 类型。将它们放在单独的类中将允许您对发票和物品进行不同的实现。总的来说,将它们分开似乎更好。GetInvoices
GetArticles
WebService
这是OO中“动词与名词”的经典演绎:
http://steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html
评论
struct
InvoiceReader
ArticleReader
dataProvider
StringBuilder
BuildString()