【问题标题】:How do you determine how coarse or fine-grained a 'responsibility' should be when using the single responsibility principle?在使用单一职责原则时,您如何确定“职责”的粗粒度或细粒度?
【发布时间】:2010-03-16 15:31:56
【问题描述】:

在 SRP 中,“责任”通常被描述为“改变的理由”,因此每个类(或对象?)应该只有一个理由,有人应该去那里改变它。

但是,如果你把它带到极端的细粒度上,你可以说一个对象将两个数字加在一起是一种责任,也是改变的可能原因。因此对象不应该包含其他逻辑,因为它会产生另一个变化的原因。

我很好奇是否有任何人有任何“范围界定”策略,即单一职责原则稍微不那么客观?

【问题讨论】:

标签: oop single-responsibility-principle solid-principles


【解决方案1】:

这取决于您所建模的内容。我已经就 SOLID 原则进行了一些广泛的写作和演示,并在我对单一职责的讨论中专门解决了您的问题。

以下内容首次出现在 2010 年 1 月/2 月的 Code Magazine 杂志上,可在线获取 "S.O.L.I.D. Software Development, One Step at a Time"


单一职责原则 说一个类应该有一个,并且 只有一个,改变的理由。

这在 第一的。会不会更容易说 一个类应该只有一个 存在的理由?其实没人 存在的理由很容易 采取极端,会导致 弊大于利。如果你把它带到 那个极端和构建类 有一个存在的理由,你可能会结束 每个类只有一个方法。 这将导致大规模蔓延 即使是最简单的类 进程,导致系统 难以理解和困难 改变。

一个类应该有的原因 一个改变的理由,而不是一个 存在的理由,是企业 您正在构建的上下文 系统。即使有两个概念 逻辑上不同,业务 需要它们的上下文可能 使他们成为一个和 相同的。决定什么时候的关键点 类应该改变不是基于一个 概念的纯粹逻辑分离, 而是企业的看法 的概念。什么时候做生意 观念和背景发生了变化, 那么你有理由改变 班级。了解什么 单个班级应承担的责任 有,你需要先了解 应该封装什么概念 那个班级和你期望的地方 该概念的实施细节 改变。

考虑汽车中的发动机,因为 例子。你在乎内在吗 发动机的工作?你关心 你有一个特定的大小 活塞、凸轮轴、喷油器等? 或者,你只关心引擎 当您进入时按预期运行 车?答案当然是 完全取决于上下文 您需要使用引擎。

如果您是一名机械师,在 汽车店,你可能关心 发动机的内部工作。你需要 要知道具体型号, 各种零件尺寸和其他 发动机的规格。如果你 没有此信息, 您可能无法维修发动机 适当地。但是,如果您是 普通人只有 需要从 A 点运输到 B点,你可能不需要那个 信息水平。的概念 单个活塞、火花塞、 滑轮、皮带等,差不多 对你毫无意义。你只关心那个 你开的车有引擎 并且它执行正确。

引擎示例直接驱动到 单一职责的核心 原则。驾驶环境 汽车与维修发动机提供 关于应该做什么的两种不同的概念 并且不应该是一个单一的概念-a 变化的原因。在上下文中 维修发动机,每个人 部分需要分开。你需要 将它们编码为单个类并确保 他们都取决于他们的个人 规格。在上下文中 但是,驾驶汽车,发动机是 不需要的单一概念 被进一步分解。你会 可能有一个名为 引擎,在这种情况下。在任一情况下, 上下文决定了什么 适当的分离 职责是。

【讨论】:

  • 解释得很好。与软件设计中的所有事情一样,上下文是关键。
  • 责任也有不同的粒度级别(上下文)。例如。类很可能映射到领域对象,如Product、Order等。Product的类改变的原因只有一个,即Product的域对象发生了改变。可能需要为产品添加更多属性。
  • @Derick 非常漂亮的文章,它让我对 SOLID 更加清楚?非常感谢。
  • 我意识到这是一个老话题,但我最近问了类似的问题。这是否意味着引擎类将是每个内部对象的单个对象的组合?当您更改内部时,更改仅限于该部分的类,而当您使用引擎时,接口只是引擎的接口?
  • Ryan - 假设您需要对引擎的内部进行建模,是的。不过,真正的问题是您是否需要对引擎的内部进行建模。
【解决方案2】:

我倾向于考虑业务需求的“变化速度”而不是“变化的原因”。

问题确实是事物会一起改变的可能性有多大,而不是它们是否会改变。

区别很微妙,但对我有帮助。让我们考虑wikipedia 上关于报告引擎的示例:

  • 如果报告的内容和模板同时发生变化的可能性很高,则它可以是一个组件,因为它们显然是相关的。 (也可以是两个)

  • 但如果内容在没有模板的情况下发生变化的可能性很重要,那么它必须是两个组件,因为它们不相关。 (有一个会很危险)

但我知道这是对 SRP 的个人解释。

另外,我喜欢的第二种技巧是:“用一句话描述你的班级”。它通常可以帮助我确定是否有明确的责任。

【讨论】:

  • +1,有趣的答案,这就是我最初的解释,但校长的措辞似乎有点模糊。校长似乎经常要求用各种短语替换“责任”这个词。
  • 我添加了第二个我喜欢的技巧。我仍然完全同意这是模糊的。但任何设计原则不都是这样吗?这不是公式,也不是食谱。
  • 能否请你帮我解决这个问题,因为我在遵循 SRP 时遇到了一些问题:stackoverflow.com/questions/56017036/…
【解决方案3】:

我不认为执行将两个数字相加之类的任务是一种责任。职责有不同的形式和大小,但它们当然应该被视为比执行单一功能更大的事情。

为了更好地理解这一点,明确区分类的职责和方法的职责可能会有所帮助。一个方法应该“只做一件事”(例如,添加两个数字,尽管在大多数情况下,'+' 是一种已经这样做的方法),而一个类应该向它的消费者呈现一个明确的“责任”。它的责任远高于方法。

像 Repository 这样的类具有明确而独特的职责。它有多种方法,如 Save 和 Load,但明确的责任是为 Person 实体提供持久性支持。一个类还可以协调和/或抽象依赖类的职责,再次将其作为单一职责呈现给其他消费类。

底线是,如果 SRP 的应用导致单方法类的全部目的似乎只是将该方法的功能包装在一个类中,那么 SRP 没有被正确应用。

【讨论】:

  • 但它并没有真正回答“我如何确定一个类的责任?”这个问题。 (我的回答也不是,顺便说一句)
  • 嗯,我想这就是软件设计的本质,它确实需要一定程度的直觉和(不是)常识。如果我们可以应用一些任意规则来实现 SRP,那么它就不是真正的设计,并且可能会有 ReSharper SRP 捷径;P。
  • 另外,我认为重要的是要注意 SOLID 专门针对“原则”而​​不是“规则”。
  • +1 好点,但您所说的校长的职责似乎是含糊的,而不是规则。主体可能非常类似于规则,但仍然难以将其概念化为自动重构系统。尽管显然 SRP 永远不可能是一个完全自动化的重构工具。
  • 没错,我绝对不是说原则意味着模糊,但它通常比基本规则更全面。它旨在提供指导,但不一定要准确表达必须如何做某事。
【解决方案4】:

我使用的一个简单的经验法则是:责任的级别或粒度应该与所讨论的“实体”的级别或粒度相匹配。显然,方法的目的总是比类、服务或组件的目的更精确。

评估责任级别的一个好的策略是使用适当的比喻。如果您可以将您正在做的事情与现实世界中存在的事情联系起来,它可以帮助您从另一个角度了解您正在尝试解决的问题 - 包括能够识别适当的抽象级别和责任。

【讨论】:

    【解决方案5】:

    @Derick bailey:很好的解释
    一些补充:
    SRP 的应用是基于上下文的,这是完全可以接受的。
    问题仍然存在:是否有任何客观的方法来定义给定类是否违反 SRP?

    一些设计上下文非常明显(例如 Derick 的汽车示例),但在其他情况下,必须定义类的行为的上下文多次保持模糊。

    对于这种情况,如果通过将其职责拆分为不同的类,然后测量由于拆分而​​产生的新行为和结构关系的影响来分析模糊类行为,这可能会很有帮助。

    一旦拆分完成,保留拆分的职责或将它们反向合并为单一职责的原因立即变得显而易见。

    我已经应用了这种方法,并为我带来了良好的效果。

    但我仍在继续寻找“定义类责任的客观方法”。

    【讨论】:

      【解决方案6】:

      我不同意上面 Chris Nicola 所说的“一个班级应该向它的消费者呈现单一明确的“责任”

      我认为 SRP 是关于在课堂上拥有良好的设计,而不是课堂上的客户。

      对我来说,责任是什么并不是很清楚,证明这个概念出现的问题的数量。

      “改变的唯一理由”

      "如果描述中包含单词 "and" 那么就需要拆分"

      引出一个问题:极限在哪里?最后,任何具有 2 个公共方法的类都有 2 个改变的理由,不是吗?

      对我来说,真正的 SRP 会导致 Facade 模式,在这种模式下,您有一个简单地将调用委托给其他类的类

      例如:

      class Modem
        send()
        receive()
      
      Refactors to ==>
      
      class ModemSender
      class ModelReceiver
      
      +
      
      class Modem
        send() -> ModemSender.send()
        receive()  -> ModemReceiver.receive()
      

      欢迎意见

      【讨论】:

      • 我发现调制解调器的情况相当有趣,并且倾向于认为从用户端的角度来看,将其作为一组接口处理会更好:ITransmitStreamIReceiveStreamITransmitSocketIReceiveSocketITransmitReceiveSocket 等,因为在某些情况下,人们可能希望将设备的发送和接收部分分开(例如,可以拼接一条串行电缆以将条形码阅读器和打印机连接到同一个端口)但是在其他情况下,将设备的两半保持在一起很重要。
      • 这就是为什么 SOLID 不仅仅是 SRP。例如,“开放封闭”之类的内容与类和 API 的内部设计和外部设计更相关,但“责任”肯定说明了一个类对它的消费者代表什么我不明白你如何以其他方式解释它.尽管如此,我认为如果您试图将 SOLID 简化为简单的规则,您将遇到困难。例如,如果 SRP 只是关于外观类,那么哪些类实际上做了任何工作?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-01-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-03-16
      相关资源
      最近更新 更多