实用类是好还是坏?

14

我读到过在代码中使用静态类/单例模式来创建依赖关系是不好的做法,并会引起紧密耦合和单元测试等问题。

我有一种情况,其中有一组与状态无关、仅使用方法的输入参数执行操作的url解析方法。我相信你也熟悉这种方法。

以前,我通常会创建一个类,并将这些方法添加到其中,然后直接从我的代码中调用它们,例如:

UrlParser.ParseUrl(url);

但是请稍等一下,这将引入对另一个类的依赖。我不确定这些“实用程序”类是否不好,因为它们是无状态的,这最大程度地减少了静态类和单例模式的某些问题。能否有人澄清一下这个问题?

如果只有调用类会使用方法,那么我应该将方法移到调用类中吗?这可能会违反“单一责任原则”。


1
我有一个情况,其中我有一组没有状态关联的URL解析方法,并且仅使用方法的输入参数执行操作。我相信您熟悉这种方法。这种方法被称为纯函数。 - morphles
9个回答

9
从理论上来看,我认为应尽可能避免使用实用程序类。它们基本上与静态类没有区别(虽然稍微好一些,因为它们没有状态)。
然而,从实际角度来看,我确实会创建这些类,并在适当的时候鼓励使用它们。试图避免使用实用程序类通常很麻烦,导致代码难以维护。然而,我尽量鼓励我的开发人员在公共API中尽可能避免使用它们。
例如,在您的情况下,我认为UrlParser.ParseUrl(...)最好作为一个类处理。请参考BCL中的System.Uri - 这提供了一个干净、易于使用的统一资源标识符接口,工作得很好,并保持实际状态。我更喜欢这种方法,而不是在字符串上工作的实用程序方法,并强制用户传递字符串,记住验证等。

好的观点。System.Uri是将像这样的静态实用程序方法转换为对象的很好的例子。 - Jamie Penney
考虑到他们的示例,这也与他们具体的问题非常相关 ;) - Reed Copsey
System.Uri是一个相当糟糕的例子,因为它众所周知存在与原始字符串表示和转换为/从绝对相对路径相关的问题。这就是在每个抽象设计和API制定中发生的事情,你永远不可能一开始就完全正确,而且对于MSFT框架设计师来说,要花费很长时间才能做到这一点,而且通常会比已知具有功能性的静态方法更加复杂。但这是“框架”和“设计指南”狂热主义方法的普遍问题。 - rama-jka toti
有人可以解释一下,“它们基本上和静态类没什么区别(尽管稍微好一些,因为它们没有状态)”是什么意思吗?也许我不明白“实用”类应该如何声明。根据示例,它看起来像是一个静态类。Reed指的“状态”是什么? - Michael Petito
通常它只是一个静态类,没有字段或属性,因此没有保存“状态”,而只是一系列用作实用工具的方法。 - Reed Copsey

3

只要不违反设计原则,实用类是可以使用的。像核心框架类一样愉快地使用它们。

这些类应该命名得很好,逻辑清晰。实际上,它们并不是那么"实用",而是新兴框架的一部分,本地类没有提供。

使用扩展方法也可以很有用,将功能对齐到“正确”的类上。但是,它们可能会导致一些混淆,因为扩展通常不与扩展的类一起打包,这并不理想,但仍然可以非常有用并产生更干净的代码。


2

你可以创建一个接口,并使用依赖注入来使用实现该接口的类的实例,而不是使用静态类。

问题在于,这是否值得花费精力?在某些系统中,答案是肯定的,但在其他系统中,尤其是较小的系统中,答案可能是否定的。


这也是我建议的。如果实用方法很复杂或计算成本很高,我建议使用依赖注入将其注入为某种服务对象。否则,我不会担心它。 - Jamie Penney
注入并不会移除依赖关系,它只是让处理变得更加容易。 - OscarRyz
唯一的去除依赖项的方法就是将其删除。我不明白你的观点。 - Matthew Scharley

2
这真的取决于上下文以及我们如何使用它。
实用类本身并不是坏的,但如果我们使用不当,就会变得糟糕。每个设计模式(尤其是单例模式)都很容易变成反模式,同样适用于实用类。
在软件设计中,我们需要在灵活性和简单性之间取得平衡。如果我们要创建一个仅负责字符串操作的StringUtils:
- 它是否违反SRP(单一职责原则)?-> 不是,是开发人员将太多的职责放入实用类中违反了SRP。 - “它无法使用DI框架进行注入” -> StringUtils实现是否会有所不同?我们是否会在运行时切换其实现?我们是否会对其进行模拟?当然不会。
=> 实用类本身并不是坏的,是开发人员的过错使其变得糟糕。
这一切都取决于上下文。如果您只是要创建一个仅包含单一职责并且仅在模块或层内部私下使用的实用程序类,则仍然可以使用它。

1

我真的很努力地避免使用它们,但我们都知道... 它们会渗入每个系统。尽管如此,在给定的示例中,我将使用一个URL对象,该对象将公开URL的各种属性(协议、域、路径和查询字符串参数)。几乎每次我想创建一个静态实用程序类时,通过创建执行此类工作的对象,我可以获得更多的价值。

以类似的方式,我创建了许多自定义控件,这些控件具有内置的验证功能,例如百分比、货币、电话号码等。在这之前,我有一个解析器实用程序类,其中包含所有这些规则,但是只需在页面上放置一个已知基本规则的控件(因此仅需要添加业务逻辑验证),就可以使其变得更加清晰。

我仍然保留解析器实用程序类和这些控件隐藏该静态类,但广泛使用它(将所有解析放在一个易于找到的位置)。在这方面,我认为拥有实用程序类是可以接受的,因为它允许我应用“不要重复自己”的原则,同时我可以获得使用实用程序的控件或其他对象的实例化类的好处。


1

我同意其他回答中的一些观点,即应该避免经典的单例模式,它维护了一个有状态对象的单个实例,而不是没有状态的实用程序类。我也同意Reed的观点,如果可能的话,将这些实用方法放在一个合适的类中,并且在逻辑上可以怀疑这些方法会驻留在其中。我还想补充说,通常这些静态实用程序方法可能是扩展方法的好候选。


1

在这种使用方式中,实用程序类基本上是纯顶级函数的命名空间。

从架构角度来看,如果您使用纯顶级“全局”函数或基本(*)纯静态方法没有区别。任何一个的优缺点都同样适用于另一个。

静态方法 vs 全局函数

使用实用程序类而不是全局(“浮动”)函数的主要论点是代码组织、文件和目录结构以及命名:

  • 您可能已经有一个按命名空间在目录中构造类文件的约定,但您可能没有一个好的对顶级函数的约定。
  • 对于版本控制(例如 git),每个函数单独存储在一个文件中可能更可取,但出于其他原因,将它们放在同一个文件中可能更可取。
  • 您的语言可能具有自动加载类的机制,但没有自动加载函数的机制。(我认为这主要适用于 PHP)
  • 您可能更喜欢编写import Acme:::Url; Url::parse(url)而不是import function Acme:::parse_url; parse_url();。或者您可能更喜欢后者。
  • 您应该检查您的语言是否允许将静态方法和/或顶级函数作为值传递。也许有些语言只允许其中一个。

因此,它在很大程度上取决于您使用的语言以及项目、框架或软件生态系统中的约定。

(*) 您可以在实用程序类中拥有私有或受保护的方法,甚至使用继承-这是您无法使用顶级函数的。但大多数时候,这不是您想要的。

静态方法/函数 vs 对象方法

对象方法的主要好处是您可以注入该对象,并稍后用具有不同行为的不同实现替换它。如果您永远不需要替换它,则直接调用静态方法效果很好。通常情况下,如果:

  • 该函数是纯的(没有副作用,不受内部或外部状态的影响)
  • 任何替代行为都将被视为错误或高度奇怪。例如,1 + 1始终应该是2。没有理由使用1 + 1 = 3的替代实现。

您还可以决定静态调用“现在足够好”。

即使您从静态方法开始,也可以稍后使它们可注入/可插入。通过使用函数/可调用的值,或者通过具有内部调用静态方法的对象方法的小包装器类。


0

只要你设计得好,它们就很好用(也就是说,你不需要经常更改它们的签名)。

这些实用方法不经常更改,因为它们只做一件事。问题出现在当你想把一个更复杂的对象与另一个紧密耦合时。如果其中一个需要更改或替换,如果它们高度耦合,则更难做到。

由于这些实用方法不会经常更改,我认为这不是什么大问题。

我认为重复复制/粘贴同一实用方法会更糟糕。

这个视频如何设计一个好的 API 以及为什么它很重要由 Joshua Bloch 讲解了设计 API(也就是你的实用程序库)时需要记住的几个概念。虽然他是一位著名的 Java 架构师,但这些内容适用于所有编程语言。


0

要谨慎使用它们,您希望尽可能将逻辑放入类中,以便它们不仅成为数据容器。

但是,同时您无法真正避免使用工具,有时候它们是必需的。

在这种情况下,我认为可以使用。

顺便提一下,有一个system.web.httputility类,其中包含许多常见的http实用程序,您可能会发现它们很有用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接