单一责任原则。 并不像看起来那么简单

单一责任原则。 并不像看起来那么简单 单一责任原则,又称单一责任原则,
又名统一可变性原则 - 这是一个非常难以理解的家伙,也是程序员面试中如此紧张的问题。

我第一次认真认识这个原理是在第一学年伊始,当时幼小的和绿色的幼虫被带到森林里,使幼虫成为学生——真正的学生。

在森林里,我们被分成8-9人一组,进行了一场比赛——哪一组最快喝完一瓶伏特加,前提是第一个人把伏特加倒进杯子里,第二个人喝掉,第三个有零食。 已完成操作的单元移至组队列的末尾。

队列大小为三的倍数的情况是 SRP 的良好实现。

定义 1. 单一职责。

单一责任原则(SRP)的官方定义指出,每个实体都有自己的责任和存在的理由,并且它只有一种责任。

考虑对象“Drinker”(提普勒).
为了落实SRP原则,我们将职责分为三部分:

  • 一倒(浇注操作)
  • 一杯饮料(饮酒行动)
  • 一个人吃点零食(咬住行动)

该流程中的每个参与者都负责该流程的一个组成部分,即具有一个原子责任 - 喝水、倒酒或吃零食。

反过来,饮水孔是这些操作的门面:

сlass Tippler {
    //...
    void Act(){
        _pourOperation.Do() // налить
        _drinkUpOperation.Do() // выпить
        _takeBiteOperation.Do() // закусить
    }
}

单一责任原则。 并不像看起来那么简单

Зачем?

人类程序员为猿人编写代码,而猿人不专心、愚蠢且总是匆忙。 他可以一次掌握并理解大约 3 - 7 个术语。
对于醉汉来说,有以下三个术语。 然而,如果我们用一张纸来写代码,那么它就会包含手、眼镜、打斗和无休止的政治争论。 所有这些都将在一个方法的主体中。 我相信您在实践中见过这样的代码。 这不是最人道的心理测试。

另一方面,猿人被设计为在他的头脑中模拟现实世界的物体。 在他的想象中,他可以将它们推到一起,组装新的物体,并以同样的方式拆卸它们。 想象一辆旧模型汽车。 在你的想象中,你可以打开车门,拧开门饰,看到车窗升降机构,里面有齿轮。 但您无法在一个“列表”中同时看到机器的所有组件。 至少“猴人”不能。

因此,人类程序员将复杂的机制分解为一组不太复杂的工作元素。 然而,它可以通过不同的方式分解:在许多旧汽车中,空气管道进入车门,而在现代汽车中,电子锁故障会导致发动机无法启动,这可能是维修过程中的一个问题。

的,所以 SRP是一个原则,解释了如何分解,即在哪里划分界线.

他说,要按照“责任”划分的原则,即按照某些对象的任务来分解。

单一责任原则。 并不像看起来那么简单

让我们回到饮酒以及猴人在分解过程中获得的好处:

  • 代码在各个层面都变得极其清晰
  • 该代码可以由多个程序员同时编写(每个程序员编写一个单独的元素)
  • 自动化测试被简化——元素越简单,测试就越容易
  • 代码的组合性出现 - 您可以替换 饮酒行动 酒鬼将液体倒在桌子底下的行动。 或者用混合葡萄酒和水或伏特加和啤酒的操作来代替倾倒操作。 根据业务需求,无需触及方法代码即可完成所有操作 酒鬼法案.
  • 通过这些操作,您可以折叠贪食者(仅使用 采取比特操作)、酒精(仅使用 饮酒行动 直接从瓶子里取出)并满足许多其他业务要求。

(哦,看来这已经是OCP原则了,我违反了这个帖子的责任)

当然,还有缺点:

  • 我们必须创建更多类型。
  • 醉汉第一次喝酒比他原本应该喝酒的时间要晚几个小时。

定义 2. 统一变异性。

请允许我,先生们! 饮酒类也有一个单一的职责——它喝酒! 总的来说,“责任”这个词是一个极其模糊的概念。 有人要对人类的命运负责,有人要对养育在极点翻倒的企鹅负责。

让我们考虑一下饮水器的两种实现。 上面提到的第一个包含三个类 - 倒酒、饮料和零食。

第二个是通过“Forward and Only Forward”方法编写的,包含该方法中的所有逻辑 法案:

//Не тратьте время  на изучение этого класса. Лучше съешьте печеньку
сlass BrutTippler {
   //...
   void Act(){
        // наливаем
    if(!_hand.TryDischarge(from:_bottle, to:_glass, size:_glass.Capacity))
        throw new OverdrunkException();

    // выпиваем
    if(!_hand.TryDrink(from: _glass,  size: _glass.Capacity))
        throw new OverdrunkException();

    //Закусываем
    for(int i = 0; i< 3; i++){
        var food = _foodStore.TakeOrDefault();
        if(food==null)
            throw new FoodIsOverException();

        _hand.TryEat(food);
    }
   }
}

从外部观察者的角度来看,这两个阶级看起来完全一样,并且承担着相同的“饮酒”责任。

困惑!

然后我们上网查了SRP的另一个定义——单一可变性原则。

SCP 指出“一个模块有且仅有一个改变的理由”。 也就是说,“责任是改变的理由”。

(看来提出最初定义的家伙对猿人的心灵感应能力很有信心)

现在一切都已就绪。 单独地,我们可以改变倒酒、饮用和吃零食的程序,但在饮酒器本身中,我们只能改变操作的顺序和组成,例如,在饮用前移动零食或添加吐司的阅读。

在“向前且仅向前”的方法中,所有可以改变的东西都只在方法中改变 法案。 当逻辑很少且很少改变时,这可能是可读且有效的,但通常会以每行 500 行的可怕方法结束,其中的 if 语句比俄罗斯加入北约所需的还要多。

定义 3. 变更的本地化。

饮酒者常常不明白为什么他们在别人的公寓里醒来,或者他们的手机在哪里。 是时候添加详细的日志记录了。

让我们开始记录浇注过程:

class PourOperation: IOperation{
    PourOperation(ILogger log /*....*/){/*...*/}
    //...
    void Do(){
        _log.Log($"Before pour with {_hand} and {_bottle}");
        //Pour business logic ...
        _log.Log($"After pour with {_hand} and {_bottle}");
    }
}

通过将其封装在 浇注操作,我们从责任和封装的角度采取了明智的行动,但现在我们对可变性原则感到困惑。 除了操作本身可以改变之外,日志记录本身也可以改变。 您必须为浇注操作分离并创建一个特殊的记录器:

interface IPourLogger{
    void LogBefore(IHand, IBottle){}
    void LogAfter(IHand, IBottle){}
    void OnError(IHand, IBottle, Exception){}
}

class PourOperation: IOperation{
    PourOperation(IPourLogger log /*....*/){/*...*/}
    //...
    void Do(){
        _log.LogBefore(_hand, _bottle);
        try{
             //... business logic
             _log.LogAfter(_hand, _bottle");
        }
        catch(exception e){
            _log.OnError(_hand, _bottle, e)
        }
    }
}

细心的读者会注意到 日志后, 之前记录 и 错误 也可以单独更改,类比前面的步骤,会创建三个类: 倒入记录器之前, 倒入记录器之后 и 浇注错误记录器.

记住饮酒者有 14 个操作,我们有 XNUMX 个记录类。 结果,整个饮酒圈由XNUMX(!!!)个等级组成。

双曲线? 几乎不! 一个拿着分解手榴弹的猴人将把“倒酒器”分成一个醒酒器、一个玻璃杯、倒酒操作员、一个供水服务、一个分子碰撞的物理模型,在下个季度,他将尝试在不发生任何变化的情况下理清这些依赖关系。全局变量。 相信我,他不会停止。

正是在这一点上,许多人得出结论,SRP是来自粉红王国的童话故事,走开去玩面条......

...没有了解过 Srp 第三个定义的存在:

“单一责任原则指出 类似变化的东西应该存放在一个地方”。 或者 ”一起更改的内容应保留在一个地方=

也就是说,如果我们更改操作的日志记录,那么我们必须在一处更改它。

这是非常重要的一点——因为上面所有对SRP的解释都说有必要在粉碎类型的同时粉碎它们,也就是说,他们对对象的大小施加了“来自上方的限制”,并且现在我们谈论的是“来自下面的约束”。 换句话说, SRP不仅要求“压碎时压碎”,而且也不能过度——“不要压碎环环相扣的东西”。 这就是奥卡姆剃刀与猿人之间的伟大之战!

单一责任原则。 并不像看起来那么简单

现在饮酒者应该感觉好多了。 除了不需要将 IPourLogger 记录器分为三个类之外,我们还可以将所有记录器合并为一种类型:

class OperationLogger{
    public OperationLogger(string operationName){/*..*/}
    public void LogBefore(object[] args){/*...*/}       
    public void LogAfter(object[] args){/*..*/}
    public void LogError(object[] args, exception e){/*..*/}
}

如果我们添加第四种操作,那么它的日志记录就已经准备好了。 并且操作本身的代码是干净的并且没有基础设施噪音。

因此,我们有 5 个课程来解决饮酒问题:

  • 浇注作业
  • 饮酒操作
  • 干扰操作
  • 记录器
  • 饮水器立面

他们每个人都严格负责一项功能,并且有一个变更原因。 所有与更改类似的规则都位于附近。

现实生活中的例子

我们曾经编写过一个自动注册 B2B 客户端的服务。 对于200行类似的内容,出现了一个GOD方法:

  • 前往1C并创建一个帐户
  • 使用此帐户,转到支付模块并在那里创建它
  • 检查主服务器上没有创建该账户的账户
  • 创建一个新账户
  • 将支付模块中的注册结果和1c号添加到注册结果服务中
  • 将帐户信息添加到此表中
  • 在积分服务中为此客户端创建一个积分编号。 将您的 1c 帐号传递给此服务。

这份名单上还有大约 10 个连接性很差的企业。 几乎每个人都需要帐户对象。 一半的呼叫需要点 ID 和客户端名称。

经过一个小时的重构,我们能够将基础设施代码和使用帐户的一些细微差别分离到单独的方法/类中。 上帝的方法让事情变得更简单,但是剩下的 100 行代码就是不想理清。

几天后才明白,这种“轻量级”方法的本质是商业算法。 而且技术规格的原始描述相当复杂。 尝试将此方法分解为多个部分就会违反 SRP,反之亦然。

形式主义。

是时候让我们的醉汉一个人呆着了。 擦干你的眼泪——总有一天我们一定会回来的。 现在让我们形式化本文中的知识。

形式主义一、SRP的定义

  1. 将元素分开,使每个元素负责一件事。
  2. 责任代表“改变的理由”。 也就是说,就业务逻辑而言,每个元素只有一个更改原因。
  3. 业务逻辑的潜在变化。 必须本地化。 同步更改的元素必须位于附近。

形式主义2.必要的自检标准。

我还没有看到满足 SRP 的足够标准。 但有必要条件:

1)问问自己这个类/方法/模块/服务的作用。 你必须用一个简单的定义来回答它。 ( 谢谢 布赖托里 )

说明

然而,有时很难找到一个简单的定义

2) 修复错误或添加新功能会影响最小数量的文件/类。 理想情况下 - 一个。

说明

由于责任(针对某个功能或错误)被封装在一个文件/类中,因此您确切地知道在哪里查看以及编辑什么内容。 例如:更改日志记录操作的输出的功能将只需要更改记录器。 无需运行其余代码。

另一个示例是添加新的 UI 控件,与之前的控件类似。 如果这迫使您添加 10 个不同的实体和 15 个不同的转换器,那么看起来您做得太过分了。

3)如果多个开发人员正在开发项目的不同功能,那么发生合并冲突的可能性(即多个开发人员同时更改同一文件/类的可能性)是最小的。

说明

如果在添加一个新操作“在桌子底下倒伏特加”时,需要影响记录器,即喝和倒酒的操作,那么看起来职责划分歪了。 当然,这并不总是可能的,但我们应该尽力减少这个数字。

4)当(来自开发人员或经理)询问有关业务逻辑的澄清问题时,您严格进入一个类/文件并仅从那里接收信息。

说明

特征、规则或算法都被紧凑地编写在一个地方,并且不会在整个代码空间中散布着标志。

5)命名清晰。

说明

我们的类或方法负责一件事,而责任就体现在它的名字中

AllManagersManagerService - 很可能是上帝类
LocalPayment - 可能不是

形式主义3.奥卡姆优先开发方法论。

在设计之初,猴人不知道也感受不到所解决问题的所有微妙之处,并且可能会犯错误。 你可能会以不同的方式犯错误:

  • 通过合并不同的职责使对象变得太大
  • 通过将单一职责划分为许多不同类型来重新构建
  • 错误界定责任边界

记住这条规则很重要:“最好犯一个大错误”,或者“如果你不确定,就不要把它分开”。 例如,如果您的类包含两个职责,那么它仍然是可以理解的,并且可以在对客户端代码进行最小更改的情况下将其拆分为两个。 由于上下文分布在多个文件中并且客户端代码中缺乏必要的依赖关系,因此用玻璃碎片组装玻璃通常更加困难。

是时候到此为止了

SRP的范围不限于OOP和SOLID。 它适用于方法、函数、类、模块、微服务和服务。 它适用于“figax-figax-and-prod”和“火箭科学”开发,让世界各地变得更美好。 如果你仔细想想,这几乎是所有工程的基本原则。 机械工程、控制系统以及事实上所有复杂的系统都是由组件构建的,“碎片化不足”剥夺了设计者的灵活性,“过度碎片化”剥夺了设计者的效率,不正确的边界剥夺了他们的理性和内心的平静。

单一责任原则。 并不像看起来那么简单

SRP 不是自然发明的,也不属于精确科学的一部分。 它突破了我们生物和心理的限制,只是利用猿人大脑来控制和开发复杂系统的一种方式。 他告诉我们如何分解一个系统。 最初的表述需要大量的心灵感应,但我希望这篇文章能够消除一些烟幕弹。

来源: habr.com

添加评论