Attribute 与单一职责原则






4.92/5 (10投票s)
本文将解释 Attribute 如何违反单一职责原则,并提出一些避免该问题的方法。
背景
Attribute 是一种非常有用的资源,它允许开发人员为类型和成员添加额外的信息。
它们之所以优秀,是因为我们可以将所有需要的信息集中在一起,并且它们完全支持重构,当成员重命名时,其 Attribute 会保留下来。但有时(或者说很多时候),这种最初的好处最终会带来比解决的问题更多的麻烦。
单一职责原则
在 Wikipedia 上,我找到了这个定义:
在面向对象编程中,单一职责原则指出,每个类都应该只有一个职责,并且该职责应该完全由该类封装。它的所有服务都应该与其职责紧密对齐。
尽管这个理念很棒,但它并没有真正告诉我一个类(或者说,一个类型,因为我们也有 struct)何时拥有单一职责,何时没有。
让我们看看 int
(System.Int32
) 类型。
int
变量是一个简单的值持有者。但是这个 struct
包含方法来解析 string
并返回一个 int
,将该 int
转换为 string
,将一个 int
与另一个进行比较,执行数学运算等等。对我来说这没问题,但我已经可以指出,一些数学运算包含在 int
类型本身中,而另一些则在 Math
类中,这有点违反直觉。像 Math.Max
这样的方法难道不应该放在原始类型中(int.Max
、double.Max
等)吗?
嗯,也许吧,但这篇论文的目的不是讨论这一点。我想要讨论的点是 Attribute 以及它们如何违反单一职责原则。
所以,让我们来看一些 Attribute 的用法。
[Serializable]
[DisplayName("Non-Empty String")]
[Description("This struct simple holds a string value,
which must be null or have a non-empty content.")]
public struct NonEmptyString
{
public NonEmptyString(string value)
{
if (value.Length == 0)
throw new ArgumentException("value must be null or should have a non-empty content.",
"value");
_value = value;
}
private readonly string _value;
[Description("This property should be null or have a non-empty content.")]
public string Value
{
get
{
return _value;
}
}
}
该 struct
的创建初衷只有一个职责(存储一个非空 string
)。你可能不喜欢为如此简单的验证创建一个 struct
,但这不重要。重要的是它的职责被改变为:
- 使其
[Serializable]
,以便可以在 ASP.NET 外部会话中使用(或出于其他原因进行序列化); - 提供更好的名称和描述,以防它被某种编辑器使用。
看到问题了吗?
如果没有,我来告诉你。类型本身不应该关心为可能的编辑器使用的文本描述。它也不应该关心 Serializable
属性。序列化器(serializer)的责任是知道它是否可序列化。编辑器的责任是通过某种方式找到更好的名称和描述。
此外,我还可以说还有其他问题,比如,我们如何在这种模型下支持非英语描述?
为了使代码符合单一职责原则,它应该看起来像这样:
public struct NonEmptyString
{
public NonEmptyString(string value)
{
if (value.Length == 0)
throw new ArgumentException
("value must be null or should have a non-empty content.", "value");
_value = value;
}
private readonly string _value;
public string Value
{
get
{
return _value;
}
}
}
这符合单一职责原则的理念。但是,我们如何解决 [Serializable]
的问题?编辑器如何找到正确的 DisplayName
和 Description
?
嗯……用旧代码来说这可能有点棘手。当然,我们可以创建另一个类型,比如 SerializableNonEmptyString
,它包含一个 NonEmptyString
并添加序列化支持。但如果我们想序列化一个已经包含 NonEmptyString
的对象,最终我们会需要将该对象复制到一个具有可序列化类型的类似对象中。因此,在这些情况下,也许违反单一职责原则会更好。或者,如果我们有选择,我们可以重新创建序列化过程(或使用替代方案),允许我们为已存在的类型添加特定的序列化代码。
实际上,理念是:每个类型都有一个单一职责。因此,NonEmptyString
的单一职责是存储一个 string
,该 string
要么是 null
要么是非空的。然后,一个序列化器类型会知道如何序列化 NonEmptyString
。只需要告知序列化机制我们有该类型的序列化器即可。
事实上,使用这种模型,任何存在于第三方 DLL 中的类型都可以被序列化,只要能够访问存储它们并稍后重新构建(或找到)实例所需的所有属性。因此,数据类型可以存在于一个程序集中,而它的序列化器可以存在于另一个程序集中。我可以肯定地说,我用它来序列化 WPF 颜色,例如。
那么 DisplayName 和 Description 呢?
嗯……我可以肯定地说,一个 Dictionary<MemberInfo, string>
可以完成这个任务。当然应该有一些代码可用于加载 MemberInfo
的描述,但有了这样的字典,我们就可以将 DisplayName
或 Description
绑定到任何已存在的成员(事实上,我们要么创建两个结构相似但不同的类,要么需要创建另一个类型来存储 DisplayName
和 Description
)。
这种方法最好的地方在于,我们同样可以在不同的程序集中进行。因此,如果我们处理的是第三方类,我们仍然可以为那里找到的类型添加显示名称或描述。稍加努力,我们就可以支持不同的语言,而且所有这些都不需要改变原始类型。
示例代码
本文中的示例代码是一个小型的二进制序列化库,它遵循单一职责原则。ConfigurableBinarySerializer
必须被配置(也就是说,用户必须为每种项类型添加序列化器)。此外,还有一个非常小的示例,它只展示了如何注册序列化器,如何进行序列化和反序列化。如果一切正常,数据将被序列化到内存流并从中反序列化。控制台应用程序将退出。因此,仅用于查看代码。
我不期望人们仅仅将此序列化器用作普通序列化技术的替代品,但我希望它能帮助人们更好地理解单一职责原则。
一种方式 vs. 另一种方式
我已经收到了两条消息,有些人认为 Attribute 并不会违反单一职责原则。他们确实有合理的观点。但让我们从外部视角来看。
某人(用户、厨师、另一个程序员)要求一个组件,该组件:
- 有两个属性。
- 另外两个属性由前两个属性计算得出(P1+P2 和 P1*P2)。
程序员很容易这样做:
public class SomeClass
{
public int P1 { get; set; }
public int P2 { get; set; }
public int Sum
{
get
{
return P1 + P2;
}
}
public int Multiplication
{
get
{
return P1 * P2;
}
}
}
事实上,代码是可以运行的。但随后开发者会收到一个“警告”,说他的代码不正确。事实是:
- 他必须在他的类中放入
[Serializable]
属性,否则它将无法工作。 - 他必须在计算出的属性上添加 Description。
但对他来说,这并不是被要求做的。你可能会认为他懒惰,但他做了他被要求做的事情。
他真的错了?
我的答案是否定的。不是因为有人忘记说这个类是可序列化的,而是因为这个类的目的是存储数据。可序列化是这种类的另一种“特征”……但最好将这个特征放在别处,让负责查看什么可序列化的人来说:嘿,这是一个可以毫无问题地序列化的类!
不要把需求强加给编写类的程序员。因此,也不要把这个需求强加给类本身。如果后来有人能够弄清楚这一点,那就允许它。这意味着:允许在运行时进行(添加),而不是只允许在编译时进行。而这才是真正的问题所在:
你是在使用 Attribute 作为提示,还是在使用它作为需求?作为提示,这可能是一个小的违规。但作为需求,这就成了一个问题。
历史
- 2012 年 8 月 16 日:初始版本