是否有某些SOLID原则与清洁代码相反的OOP风格?


26

我最近与我的一个朋友讨论了有关视频游戏开发中OOP的问题。

我正在解释我的一款游戏的体系结构,令我的朋友惊讶的是,它包含许多小类和几个抽象层。我认为这是我专注于赋予一切单一职责并放松组件之间的耦合的结果。

他担心的是,大量的课程将转化为维护的噩梦。我的看法是,它将产生完全相反的效果。我们进行了长达数个世纪的讨论,最终同意了不同意,说也许在某些情况下SOLID原则和适当的OOP实际上并不能很好地融合在一起。

甚至Wikipedia关于SOLID原则的条目都指出,它们是有助于编写可维护代码的指南,并且它们是敏捷和自适应编程的整体策略的一部分。

所以,我的问题是:

在OOP中是否存在某些或全部SOLID原则不适合清理代码的情况?

我可以马上想象一下,《李斯科夫替代原则》可能与另一种安全继承形式发生冲突。也就是说,如果有人设计了通过继承实现的另一种有用模式,则LSP很可能与其直接冲突。

还有其他吗?也许某些类型的项目或某些目标平台可以使用较少的SOLID方法更好地工作?

编辑:

我只想说明我不是在问如何改善代码;)我在这个问题中提到一个项目的唯一原因是提供一些上下文。我的问题是关于OOP和一般的设计原则。

如果您对我的项目感到好奇,请参阅this

编辑2:

我以为可以用以下三种方式之一回答这个问题:

  1. 是的,存在与SOLID部分冲突的OOP设计原则
  2. 是的,存在与SOLID完全冲突的OOP设计原则
  3. 不,SOLID是蜜蜂的膝盖,OOP将永远更好。但是,与所有内容一样,它不是万能药。负责任地喝酒。

选项1和2可能会产生很长且有趣的答案。另一方面,选项3是一个简短的,无趣的,但总体上令人放心的答案。

我们似乎正在选择方案3。


您如何定义“干净代码”?
GrandmasterB 2014年

在这个问题的范围内,干净的代码是符合OOP目标和一组选定的设计原则的代码结构。因此,就OOP + SOLID设定的目标而言,我熟悉简洁的代码。我想知道是否存在另一套适用于OOP但与SOLID完全或部分不兼容的设计原则。
MetaFight 2014年

1
我比其他任何事情都更担心您的游戏的性能。除非我们谈论基于文本的游戏。
欣快2014年

1
该游戏本质上是一个实验。我正在尝试不同的开发方法,并且还在尝试查看编写游戏时是否可以使用企业级代码。我有点希望性能成为一个问题,但是还没有。如果/何时这样做,那么接下来我将尝试的是如何将我的代码改编成高性能代码。这么多的学习!
MetaFight 2014年

1
冒着一些低票的风险,我想回到“为什么”的问题:游戏开发中的 OOP 。由于游戏开发的性质,“传统的” OOP似乎不再流行,而是倾向于各种风格的实体/组件系统,例如,此处是一个有趣的变体。这让我认为问题不应该是“ SOLID + OOP”,而应该是“ OOP +游戏开发”。当您查看游戏开发人员的对象交互图时。与说N层应用程序相比,差异可能意味着一个新的范例是好的。
J特拉纳2014年

Answers:


20

在OOP中是否存在某些或全部SOLID原则不适合清理代码的情况?

一般来说,没有。历史表明SOLID原则在很大程度上促进了去耦,而这反过来又被证明可以增加代码的灵活性,从而提高您适应变更的能力,并使代码更易于推理,测试和重用。简而言之,使您的代码更整洁。

现在,在某些情况下,SOLID原则会与DRY(不要重复自己),KISS(保持简单愚蠢)或其他好的OO设计原则相冲突。当然,它们可能与需求的现实,人类的局限性,我们的编程语言的局限性以及其他障碍发生冲突。

简而言之,SOLID原则将始终使自己适用于干净的代码,但是在某些情况下,它们的适用性要比冲突的替代方案少。他们总是很好,但是有时候其他事情会更好


14
我喜欢,但是有时候其他事情更好。它具有“动物农场”的感觉。;)
FrustratedWithFormsDesigner 2014年

12
+1当我只看SOLID原则时,我看到5个“精打细算”。但是他们都不是DRY,KISS和“表明您的意图”的最佳选择。使用SOLID,但要谨慎谨慎。
user949300 2014年

我同意。我认为要遵循的正确原则显然取决于具体情况,但是除了硬性性能要求(始终排在其他一切之上(尤其是在游戏开发中))之外,我倾向于认为DRY和KISS通常都更重要比SOLID 当然,代码越干净越好,因此,如果可以遵循所有原则而没有冲突,那就更好了。
本李

集成隔离与聚合的有效设计又如何呢?如果基本的“序列”接口[例如IEnumerable]包含诸如Count和的方法asImmutable,以及诸如getAbilities[其返回将表明诸如此类的东西是否Count将是“有效的”]之类的属性,则可以采用一种静态方法,该方法采用多个序列并将其汇总,因此它们将表现为更长的单个序列。如果这些能力存在于基本序列类型中,即使它们仅链接到默认实现,那么聚合将能够公开这些能力……
supercat

...并与可以使用的基类一起有效地实现它们。如果一个人汇总了一个知道其计数的一千万个项目列表和一个只能按顺序检索项目的五个项目列表,那么请求第10,000,003个条目的请求应从第一个列表中读取计数,然后从第二个列表中读取三个项目。厨房接收器接口可能会违反ISP,但是它们可以在某些组合/聚合方案中大大提高其性能。
超级猫

13

我认为我在金融和游戏领域都有过不同寻常的见解。

我在游戏中遇到的许多程序员在软件工程界都很糟糕-但是他们不需要像SOLID这样的实践。传统上,他们将游戏放在盒子里-然后就完成了。

在金融领域,我发现开发人员确实很马虎且不受纪律,因为他们不需要游戏中的性能。

上面的两个陈述当然都是过分概括,但是实际上,在两种情况下,干净的代码都是至关重要的。对于优化,清晰易懂的代码至关重要。为了可维护性,您需要相同的东西。

这并不是说SOLID并非没有批评。它们被称为原则,但是应该像指导原则一样被遵循吗?那么我什么时候应该关注他们呢?我什么时候应该违反规则?

您可能会看两段代码,并说哪一段最符合SOLID。但是孤立地,没有客观的方法可以将代码转换为SOLID。绝对是对开发人员的解释。

说“大量的课程可能导致维护噩梦”是不言而喻的。但是,如果未正确解释SRP,则很容易出现此问题。

我见过很多层几乎没有责任的代码。除了将状态从一类传递到另一类外,什么也不做的类。间接层过多的经典问题。

如果滥用SRP,最终可能会导致代码缺乏凝聚力。您可以在尝试添加功能时告诉您。如果始终必须同时更改多个位置,则代码缺乏内聚性。

开闭不是没有批评者的。例如,请参阅Jon Skeet对开放式封闭原则的评论。我不会在这里重述他的论点。

您对LSP的争论似乎是假设的,我真的不理解您所说的另一种安全继承形式是什么意思?

ISP似乎有点重复SRP。当然,必须对它们进行相同的解释,因为您永远无法预期接口的所有客户端将执行的操作。今天的凝聚力接口是明天的ISP违反者。唯一不合逻辑的结论是,每个接口最多只能有一个成员。

DIP可能导致无法使用的代码。我已经以DIP的名义看到了带有大量参数的类。这些类通常会“更新”它们的复合部分,但是我称之为“可测试性”,我们绝不能再使用new关键字。

仅靠SOLID不足以编写干净的代码。我看过罗伯特·C·马丁(Robert C. Martin)的书“ 干净的代码:敏捷软件技巧手册 ”。最大的问题是,阅读本书并将其理解为规则很容易。如果这样做,您将失去重点。它包含大量的气味,试探法和原理-不仅仅是SOLID中的五种。所有这些“原则”都没有原则,而是准则。鲍伯叔叔把他们自己形容为概念的“小洞”。他们是一种平衡的行为。盲目遵循任何准则都会导致问题。


1
如果您有大量的构造函数参数,则表明您违反了SRP。但是,DI容器仍然消除了与大量构造函数参数相关的痛苦,因此我不同意您在DIP上的声明。
斯蒂芬

所有这些“原则”都没有原则,而是准则。他们是一种平衡的行为。正如我在SRP之后的帖子中所说的那样,极端本身也可能会带来问题。
戴夫·希利尔

好的答案,+ 1。我想补充一件事:我读过Skeet的评论,更多是对OCP的标准教科书定义的批评,而不是对OCP背后的核心思想的批评。据我了解,OCP从一开始就应该是受保护的变化思想。
布朗

5

这是我的意见:

尽管SOLID原则旨在实现非冗余和灵活的代码库,但是如果类和层太多,这可能会在可读性和维护性方面进行权衡。

特别是关于抽象的数量。如果大量的类基于很少的抽象,则可能是可以的。由于我们不知道项目的大小和细节,所以很难说,但是除了大量的类之外,您还提到几层,这有点令人担忧。

我猜想SOLID原则并不反对OOP,但是仍然有可能编写粘附于其上的不干净的代码。


3
+1顾名思义,高度灵活的系统必须比灵活性较差的系统更难以维护。由于灵活性带来了额外的复杂性,因此付出了代价。
安迪

@安迪不一定。在我目前的工作中,代码的许多最糟糕的部分都是乱七八糟的,因为它们只是通过“做一些简单的事情,将它们砍在一起并使其正常工作而不会变得如此复杂”而混杂在一起的。结果,系统的许多部分都是100%刚性的。您根本无法修改它们,而不必重写它们的大部分。真的很难维护。可维护性来自高内聚性和低耦合性,而不是系统的灵活性。
萨拉

3

在开发初期,我开始用C ++编写Roguelike。当我想运用从我的教育中学到的优秀的面向对象方法时,我立即将游戏项目视为C ++对象。PotionSwordsFoodAxeJavelins等都是从一个基本的核心衍生Item代码,处理名称,图标,重量,这样的东西。

然后,我编写了包逻辑,然后遇到了问题。我可以放进Items书包,但是,如果我选择一个,我怎么知道它是a Potion还是a Sword?我在互联网上查询了如何下达项目。我砍死编译器选项启用运行时信息,所以我想知道我的抽象的Item真实类型,并开始切换逻辑上种知道我会允许什么样的操作(如避免饮用Swords并把Potions我的脚)

从本质上讲,它把代码变成了一个半复制的泥浆大球。随着项目的进行,我开始了解什么是设计失败。发生的事情是我尝试使用类来表示值。我的班级层次结构不是有意义的抽象。我应该做的正在Item实施一系列的功能,如Equip ()Apply ()Throw (),并根据每个值的行为。使用枚举表示装备插槽。使用枚举代表不同的武器种类。使用更多的值而减少分类,因为我的子分类除了填充这些最终值外没有其他目的。

由于您面对的是抽象层下的对象乘法,因此我认为我的见解在这里很有价值。如果您似乎有太多的对象类型,则可能是您混淆了应为类型和应为值。


2
问题在于将类型与值的类混淆(注意:我没有使用“类”一词来指代类的OOP / C ++概念。)在笛卡尔平面中(矩形坐标,极坐标)表示点有多种方法坐标),但它们都是同一类型的一部分。但是,主流的OOP语言自然不支持对值进行分类。与之最接近的是C / C ++的并集,但是您需要添加一个额外的字段,以便知道您在其中放置了什么。
2014年

4
您的经验可以用作反例。通过不使用SOLID或任何类型的建模方法,您创建了无法维护和不可扩展的代码。
欣快2014年

1
@Euphoric我很努力。我有面向对象的方法。不过,拥有一种方法与成功实施它之间是有区别的:)
Arthur Havlicek 2014年

2
这是一个与OOP中的纯净代码相反的SOLID原理示例吗?似乎更像是一个错误设计的示例-这与OOP正交!
Andres F.

1
您的基本ITEM类应具有许多“ canXXXX”布尔方法,所有方法均默认为FALSE。然后,您可以在Javelin类中将“ canThrow()”设置为true,而在Postion类中将canEat()设置为true。
James Anderson

3

他担心的是,大量的课程将转化为维护的噩梦。我的看法是,它将产生完全相反的效果。

我绝对站在您朋友的一边,但这可能取决于我们的领域以及我们要解决的问题和设计的类型,尤其是将来可能需要更改的类型。不同的问题,不同的解决方案。我不相信对与错,只是程序员试图寻找能够最好地解决其特定设计问题的最佳方法。我使用的是VFX,与游戏引擎不太相似。

但是,我所苦苦挣扎的问题可能至少可以归结为“太多的类”或“太多的功能”,因为我至少在某种程度上被称为符合SOLID的体系结构(基于COM)。您的朋友可能会形容。我要特别指出的是,“互动过多,太多的地方可能会导致行为不端,太多的地方可能会导致副作用,太多的地方可能需要更改,太多的地方可能无法实现我们认为的目标”。

我们有很多抽象(纯的)接口是由大量子类型实现的,就像这样(在谈论ECS好处的背景下制作此图,而无视左下角的注释):

在此处输入图片说明

运动接口或场景节点接口可能由数百个子类型实现:灯光,相机,网格,物理求解器,着色器,纹理,骨骼,基本形状,曲线等(每种类型通常都有多种类型) )。最终的问题确实是这些设计不是那么稳定。我们的需求在不断变化,有时接口本身也必须在变化,而当您想更改由200个子类型实现的抽象接口时,这是一笔极其昂贵的更改。我们开始通过使用抽象基类来减轻这种负担,在它们之间减少了此类设计更改的成本,但它们仍然很昂贵。

因此,我也开始探索游戏行业中相当普遍使用的实体组件系统架构。那改变了一切,就像这样:

在此处输入图片说明

哇!就可维护性而言,是如此的不同。依赖关系不再流向抽象,而是流向数据(组件)。至少就我而言,尽管需求不断变化(尽管我们对相同数据的处理能力会随着需求的变化而不断变化),但从设计的角度来讲,数据要稳定得多并且更容易获得正确的结果。

另外,由于ECS中的实体使用组合而不是继承,因此它们实际上不需要包含功能。它们只是类比的“组件容器”。这就使得实现运动接口的类比200子类型变成200个实体实例(不是具有单独代码的单独类型),它们仅存储运动分量(除了与运动相关的数据外,什么也没有)。A PointLight不再是单独的类/子类型。这根本不是一堂课。它是一个实体的实例,该实体仅组合了与其在空间中的位置(运动)和点光源的特定属性有关的某些组件(数据)。与它们关联的唯一功能是在系统内部,例如RenderSystem,它会在场景中寻找灯光组件以确定如何渲染场景。

随着ECS方法下需求的变化,通常只需要更改在该数据上运行的一个或两个系统,或者只是在侧面引入新系统,或者在需要新数据时引入新组件。

因此,至少对于我的领域,而且我几乎可以肯定,并不是每个人都这样,这使事情变得如此容易,因为依赖关系正在朝着稳定的方向发展(不需要经常更改的事物)。当依赖关系统一流向抽象时,在COM体系结构中情况并非如此。以我为例,要弄清楚运动的前期需要什么数据要容易得多,而不是您可以使用它做所有可能的事情,随着新要求的出现,这种变化通常会在几个月或几年内有所变化。

在此处输入图片说明

在OOP中是否存在某些或全部SOLID原则不适合清理代码的情况?

好吧,我不能说干净的代码,因为有些人将干净的代码等同于SOLID,但是肯定在某些情况下,像ECS一样,将数据与功能分开,并且将依赖关系从抽象重定向到数据肯定可以使事情变得容易得多。出于明显的耦合原因,如果数据比抽象要稳定得多,请进行更改。当然,对数据的依赖性可能会使保持不变性变得困难,但是ECS倾向于通过系统组织将这种情况减少到最小,这可以最小化访问任何给定类型的组件的系统数量。

不一定像DIP所建议的那样,依赖关系应该流向抽象。依赖关系应该流向不太可能需要将来更改的事物。在所有情况下都可能是抽象,也可能不是抽象(当然这不是我的意思)。

  1. 是的,存在与SOLID部分冲突的OOP设计原则
  2. 是的,存在与SOLID完全冲突的OOP设计原则。

我不确定ECS是否真的是OOP的味道。有人用这种方式定义它,但我认为它在本质上与耦合特性,数据(组件)与功能(系统)的分离以及缺乏数据封装的本质不同。如果将其视为OOP的一种形式,我认为它与SOLID(至少是SRP,开放式/封闭式,liskov替代和DIP的最严格的观念)有很大冲突。但是我希望这是一个案例和领域的合理示例,在该案例和领域中,SOLID的最基本方面(至少人们通常会在更易于理解的OOP上下文中对其进行解释)可能不太适用。

小班

我正在解释我的一款游戏的体系结构,令我的朋友惊讶的是,它包含许多小类和几个抽象层。我认为这是我专注于赋予一切单一职责并放松组件之间的耦合的结果。

ECS挑战并改变了我的观点。像您一样,我以前一直认为可维护性的思想是对可能的事物进行最简单的实现,这意味着许多事物,此外还包含许多相互依赖的事物(即使相互依赖是抽象之间的)。如果您仅放大一个类或函数以查看最直接,最简单的实现,那么这是最有意义的;如果看不到,请对其进行重构,甚至进一步分解。但是很容易错过结果,因为外部世界每次将相对复杂的事物分解为2个或更多事物时,这2个或更多事物必然不可避免地在某些情况下相互交互*(见下文)方式,否则外界必须与所有人互动。

这些天来,我发现在某种事物的简单性与多少事物之间以及在需要多少互动之间存在一种平衡的行为。ECS中的系统往往非常繁重,并且采用非平凡的实现来对数据进行操作,例如PhysicsSystemRenderSystemGuiLayoutSystem。但是,一个复杂的产品只需要很少的一个这一事实,往往会使后退变得容易,并且可以对整个代码库的整体行为进行推理。那里的东西可能暗示着,依靠更少,更庞大的类(仍然履行可以说是单一的责任)的观点可能不是一个坏主意,如果这意味着要维护和推理的类更少,并且整个过程中的交互更少系统。

互动互动

我说的是“交互”而不是“耦合”(尽管减少交互意味着减少两者),因为您可以使用抽象来解耦两个具体的对象,但是它们仍然可以相互通信。在这种间接交流的过程中,它们仍可能导致副作用。通常,我发现推理系统正确性的能力与这些“相互作用”有关,而与“耦合”无关。尽量减少交互作用,往往会使我更容易从鸟瞰图上推理出一切。这意味着事情根本不会互相交谈,从这个意义上讲,ECS还趋向于将“交互”真正地最小化,而不仅仅是耦合到最小的栏(至少我没有)。

也就是说,这可能至少部分是我和我的个人弱点。我发现创建最大规模系统的最大障碍是,仍然自信地对它们进行推理,浏览它们,并觉得我可以以可预测的方式在任何地方进行任何潜在的所需更改,包括状态和资源管理以及副作用。当我从成千上万的LOC到成千上万的LOC到数百万的LOC时,这是最大的障碍,即使对于我自己编写的代码也是如此。如果有什么事情会使我无所适从,那就是从某种意义上说,我不再能够理解应用程序状态,数据和副作用方面的情况。它' 进行变更所需的时间不是机械时间,它使我的工作减慢了很多,而如果系统超出了我的思维能力,那么就无法理解变更的全部影响。对我来说,减少交互是最有效的方法,它可以使产品具有更大的功能而变得更大,而我个人不会被这些事情淹没,因为将交互减少到最低程度同样会减少可以甚至可能会更改应用程序状态并引起副作用。

它可以变成这样(图中的所有功能都可以正常运行,显然,在现实世界中场景中对象的数量是很多倍,这是一个“交互”图,而不是耦合图)。一个介于两者之间的抽象):

在此处输入图片说明

...仅在系统具有功能的情况下(蓝色组件现在仅是数据,现在是耦合图):

在此处输入图片说明

关于这一切的想法不断涌现,也许是一种在与SOLID更兼容的更加一致的OOP上下文中构架这些好处的一种方法,但是我还没有找到设计和文字,我发现了因为我习惯于抛弃所有与OOP直接相关的术语,所以很难。我一直在努力弄清这里的人们的答案,并尽我最大的努力制定自己的答案,但是关于ECS的本质,有些事情非常有趣,但我无法完全将其付诸实践。甚至可能适用于不使用它的体系结构。我也希望这个答案不会作为ECS推广而来!我觉得这很有趣,因为设计ECS确实彻底改变了我的想法,


我喜欢交互和耦合之间的区别。尽管您的第一个交互图被混淆了。这是一个平面图,因此不需要交叉箭头。
D Drmmr

2

SOLID原则是好的,但是在发生冲突时,KISS和YAGNI会优先考虑。SOLID的目的是管理复杂性,但是如果应用SOLID本身会使代码变得更复杂,那么它就无法达到目的。举例来说,如果您的小型程序只有很少的类,那么应用DI,接口隔离等可能会大大增加程序的整体复杂性。

开放/封闭原则特别有争议。它基本上说您应该通过创建子类或同一接口的单独实现来向类中添加功能,但不要改变原始类。如果您要维护不协调的客户端使用的库或服务,这是有道理的,因为您不想破坏退出客户端可能依赖的行为。但是,如果您要修改仅在自己的应用程序中使用的类,则简单地修改该类通常会更简单,更简洁。


您对OCP的讨论忽略了通过组合来扩展类行为的可能性(例如,使用策略对象来允许类所使用的算法被更改),通常认为这是比子类更好的遵守主体的方法。
2015年

1
如果KISS和YAGNI始终优先,则您仍应使用1和0进行编码。汇编程序只是其他可能出错的内容。没有KISS和YAGNI指出设计工作的许多成本中的两个。该费用应始终与任何设计工作的利益相平衡。SOLID具有在某些情况下可以弥补成本的优势。没有简单的方法可以告诉您何时越过线。
candied_orange

@CandiedOrange:我不同意机器代码比高级语言中的代码更简单。由于缺乏抽象,机器代码最终会包含很多偶然的复杂性,因此绝对不能决定用机器代码编写典型的应用程序。
JacquesB

@Jules:是的,组合是更好的选择,但是它仍然需要您设计原始类,以便可以使用组合来对其进行自定义,因此您必须对未来需要定制哪些方面以及哪些方面做出假设。不可自定义。在某些情况下,这样做是有必要的(例如,您正在编写要分发的库),但是这样做有一定的成本,因此遵循SOLID 并不总是最佳的。
JacquesB

1
@JacquesB-是的。OCP的核心是反对YAGNI。但是,要实现它,就需要在扩展点进行设计以实现以后的改进。在两个理想之间找到合适的平衡点是很棘手的。
Jules '18

1

在我的经验中:-

大型类随着变得越来越大而难以维护。

小类易于维护,维护小类集合的难度在算术上增加了类的数量。

有时大班不可避免,一个大问题有时需要大班,但是,它们很难阅读和理解。对于命名良好的较小类,只需名称即可理解,您甚至无需查看代码,除非该类存在非常具体的问题。


0

我总是发现很难在实体的“ S”与(使对象具有自己的全部知识)(可能是过时的)需求之间划清界限。

15年前,我与Deplhi开发人员合作,他们的圣杯是包含所有内容的类。因此,对于抵押贷款,您可以添加保险,付款,收入,贷款和税收信息,并且可以计算应纳税额,应扣除额,并且可以序列化自身,将自身持久保存到磁盘等。

现在,我将同一对象分为许多小类,它们的优点是可以更轻松,更好地进行单元测试,但不能很好地概述整个业务模型。

是的,我知道您不想将您的对象绑定到数据库,但是您可以在大型抵押贷款类别中注入存储库来解决该问题。

除此之外,为类只做一点小事情的类很难找到好名字。

所以我不确定'S'总是一个好主意。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.