在处理SOLID时,我经常碰到一个事实,就是不遵循这些原则会导致问题。 问题是已知的,但形式化较差。 本文旨在规范化在编写代码的过程中出现的典型情况,可能的解决方案以及由此产生的后果。 我们将讨论为什么我们要面对错误的代码,以及随着程序的增长问题将如何发展。
不幸的是,在大多数情况下,评估归结为``很多''和``一个''选项,这暗示了O标记的破产能力,但是即使进行这样的分析,也可以更好地了解哪些代码对于系统的进一步开发确实是危险的,而哪些代码是可以容忍的。
定义
我们说如果程序员在逻辑上不超过f(n)项更改以将更改准确地实现到恒定因子,则程序中的更改需要f(n)个操作中的“ O”,其中n表示程序的大小。
指标
考虑罗伯特·马丁(Robert Martin)的一些设计特征,并根据O标记对其进行评估。
如果单个更改导致从属模块中的其他更改级联,则设计将很困难。 您必须更改的模块越多,设计就越僵硬。
显着差异是O(1)和O(n)的变化。 即 我们的设计允许不变的更改数量,或者随着程序的增长,更改的数量将会增加。 接下来,我们应该考虑这些变化本身-它们也可能会变得僵化,并具有某些渐近行为。 因此,刚度可能高达O(nm)。 参数m将被称为刚度深度。 在设计中甚至允许刚度O(n)的刚度深度的粗略估计,对于一个人来说也太复杂了,因为必须检查每个变化以获得更深的变化。
易碎性是程序的属性,一旦进行单个更改,它就会在许多地方被破坏。 在与被更改的零件没有概念联系的零件中,经常会出现新的问题。
我们不会考虑发生更改的模块的逻辑连接问题。 因此,从符号的角度来看,脆性和刚度之间没有区别,并且对刚度有效的参数适用于脆性。
如果设计中包含在其他系统中可能有用的部件,则它是惰性的,但是与尝试将这些部件与原始系统分离相关的工作和风险太大。
该定义中的风险和努力可以解释为,随着模块大小的增加,当尝试从原始系统中将其抽象为非恒定时,模块中发生的更改数量。 但是,正如实践所示,它仍然值得抽象,因为这可以使模块本身具有顺序,并可以将其转移到其他项目中。 通常,在第一次需要将模块转移到另一个项目之后,会出现其他类似的项目。
黏度
面对进行更改的需求,开发人员通常会找到几种方法来进行更改。 有些保留了设计,而有些则没有(也就是说,它们本质上是“黑客”)。 如果保留设计的方法比破解更难实现,那么设计的粘性就很高。 解决问题是容易的,但是正确是困难的。 我们希望设计程序,以便可以轻松进行更改以保留设计。
遵循粘度的O记号可以称为近视。 是的,起初开发人员确实有机会在O(1)而不是O(n)上进行更改(由于刚度或易碎性),但是这种更改通常会导致更大的刚度和易碎性,即 增加刚性的深度。 如果您忽略这样的“响铃”,那么以后的更改可能不再可能通过“ hack”解决,您将不得不更改刚性条件(可能比“响铃”之前更多)或使系统处于良好状态。 也就是说,当检测到粘度时,最好立即正确重写系统。
事情是这样的:Ivan需要编写一些使他的小脚翘起来的代码。 爬到程序的不同部分,如他所怀疑的那样,烤肉已经被抽了不止一次,他找到了合适的片段。 它复制它,粘贴到其模块中并进行必要的更改。
但是Ivan不知道他用鼠标提取的代码将Peter放在了Sveta编写的模块中。 Sveta是第一个抽一点路边石的人,但是她知道这个过程与土拨鼠的吸烟非常相似。 她在某处找到了一个代码,将代码复制到她的模块中并进行了修改。 当相同的代码一遍又一遍地以略有不同的形式出现时,开发人员就会失去抽象的想法。
在这种情况下,很明显,当需要更改挖掘动作的依据时,应在n个位置进行更改。 考虑到每个副本粘贴中可能进行唯一修改的可能性,这也可能导致逻辑上不相关的更改。 在这种情况下,由于在编译阶段没有保护,因此有机会简单地忘记另一个地方的更改。 因此,这也可以转化为O(n)个测试迭代。
关于SOLID表示法的应用
SRP(单一责任原则)。 一个软件实体应该只有一个改变的原因(责任)。 换句话说,例如,该类不应遵循业务逻辑和映射,因为 更改一项责任时,我们必须确保我们没有损害另一项责任。 即,与SRP原理的不一致导致刚性和脆弱性。 遵循这一原理还有助于摆脱惯性,将模块从一个程序转移到另一个程序,而依赖项的数量可能会更少。
变化的渐近行为基本保持与不遵循原理的相同,但是常数因子显着减小。 在这两种情况下,我们都应检查类的全部内容,并在更改实体的接口的情况下检查它们与该实体交互的位置。 仅遵循SRP有助于减少接口及其更改的可能性以及内部实现的数量,这些内部更改在更改后可能会出现故障。 对于接口的讨论,类似的推理被删节了,这对于ISP(接口隔离原理)是有效的。
OCP(开闭原理)。 软件实体(类,模块,功能等)必须打开才能进行扩展,而必须关闭才能进行修改。 应该以这样一种方式来理解这一点,即我们应该能够在不影响其源代码的情况下修改模块的行为。 通常,这是通过继承和多态性实现的。 由于违反LSP原则违反了OCP,而DIP是维护OCP的手段,因此以下内容可以同时应用于LSP和DIP。 不遵守开放性-紧密性原则会迫使所有未关闭的实体进行此更改。
例如,一个相当琐碎的情况是,存在一系列ifs,这些ifs确定子类列表中的变量类型。 这样的结构可能会多次出现在程序中。 每当添加新的子类时,都应在每个此类链中进行适当的更改。 类似的情况不仅发生在子类上,而且在没有考虑所有可能的特殊情况的情况下也可能发生[这是指不是在撰写本文时而是一般情况下的情况。 案件可能稍后出现]。
现在考虑当我们进行m个相同类型的更改时的情况,由于与开放性-封闭性原理的差异,需要我们执行n次操作。 然后,如果我们将所有内容保持原样,支持体系结构以考虑特殊情况,而不是一概而论,我们将获得所有更改O(mn)的总体复杂度。 如果我们针对此更改关闭所有m个位置,则后续更改将采用O(1)而不是O(m)。 因此,总体复杂度降低为O(m + n)。 这意味着启动OCP永远不会太晚。
马丁(Martin)谈到了这种情况,您不应该猜测(当然,如果您不确定,是否会知道)如何从第一个更改关闭,但是在第一个更改之后值得关闭,因为第一个更改是系统不一定保持当前状态的标志。 这是合乎逻辑的,因为由于非亲密关系,我们执行O(1 * n)个操作,然后执行O(m)个操作来使自己免受后续更改的影响。 总的来说,我们获得了总体复杂度O(n + m),但是与此同时,我们仅在必要时准确地执行所有操作,并且在不知道是否需要的情况下事先不执行任何操作。
模式和O标记
在计算理论中的O表示与设计中的O表示之间可以得出另一种类比。 这是因为我们减少了使用算法和数据结构(例如搜索树和堆)的计算量,这些算法和数据结构比“直接”解决方案更快地解决了典型问题,并且设计精良的程序员的操作数量也得到了提高,在这种情况下,他也可以使用好的典型解决方案称为设计模式。 您可以在SOLID原理的上下文中以及因此在O标记的上下文中评估图案的效果。
例如,Mediator模板消除了更改两个对象之间的交互逻辑时破坏程序中某些内容的可能性,因为它完全封装了模板并保证了这种更改的持续复杂性。
Adapter模板允许我们使用具有不同接口的(读添加)实体,这些接口将用于常见目的。 使用此模板,您可以将具有不兼容接口的新对象嵌入到系统中,以获得与系统大小无关的操作数量。
由于数据结构可以支持某些具有良好渐进性的操作,而某些具有不良渐进性的操作,因此模式对于某些更改具有灵活的行为,而对于其他更改则具有严格的行为。
合理的限制
在处理O符号时,处理优化问题时,我们必须记住,渐近性最好的算法并不总是最适合解决问题的。 应该理解的是,尽管金字塔排序具有更好的渐近性,但通过气泡对3个元素的数组进行排序将比金字塔排序更快。 对于较小的n值,常量因子起着重要的作用,O标记隐藏了该常量。 设计中的O标记以相同的方式工作。 对于小型项目,围堵许多模板是没有意义的,因为其实施成本超过了“不良设计”应进行的更改数量。