SOLID原则和代码结构


149

在最近的一次工作面试中,除了提供各种原理的基本含义之外,我无法回答有关SOLID的问题。这真的让我很烦。我做了几天值得深入研究的工作,但还没有得出令人满意的总结。

面试问题是:

如果您要看一个我严格遵循SOLID原则的.Net项目,那么您期望在项目和代码结构方面看到什么?

我挣扎了一下,没有真正回答问题,然后轰炸了。

我如何更好地处理这个问题?


3
我想知道什么是不明确的固体wiki页面
BЈовић

可扩展的抽象构建基块。
rwong

通过遵循面向对象设计的SOLID原理,您的类自然会趋于小巧,结构合理且易于测试。来源:docs.asp.net/en/latest/fundamentals/...
WhileTrueSleep

Answers:


187

S =单一责任原则

因此,我希望看到一个组织良好的文件夹/文件结构和对象层次结构。每个功能类/每个部分都应命名为其功能非常明显,并且应仅包含执行该任务的逻辑。

如果您看到包含成千上万行代码的大型经理类,则表明没有遵循单一职责。

O =开/关原理

这基本上是一个想法,即应通过对现有功能影响最小/需要修改的新类添加新功能。

我希望看到大量使用对象继承,子类型,接口和抽象类来将功能设计与实际实现分离开来,从而允许其他人一起实现其他版本而不影响其功能。原版的。

L = Liskov替代原理

这与将子类型视为其父类型的能力有关。如果要实现适当的继承对象层次结构,这在C#中是现成的。

我希望看到将普通对象视为其基本类型的代码,并在基类/抽象类上调用方法,而不是实例化和处理子类型本身。

I =接口隔离原理

这类似于SRP。基本上,您将较小的功能子集定义为接口,并与这些子集一起使用,以使系统保持解耦状态(例如,a FileManager可能具有处理文件I / O的单一职责,但是可以实现IFileReaderIFileWriter并且包含用于读取的特定方法定义)和文件写入)。

D =依赖反转原理。

同样,这涉及保持系统解耦。也许您会注意使用.NET依赖注入库,并将其用于解决方案中,例如UnityNinject或ServiceLocator系统,例如AutoFacServiceLocator


36
我已经在C#中看到很多违反LSP的行为,每当有人确定他们的特定子类型是专用的,因此不需要实现一部分接口,而只需在该部分上抛出异常即可。这是一种常见的初级方法解决误解接口实现和设计问题的方法
Jimmy Hoffa 2013年

2
@JimmyHoffa这是我坚持使用代码合同的主要原因之一;通过设计合同的思考过程,可以帮助人们摆脱这种不良习惯。
安迪

12
我不喜欢“ LSP在C#中开箱即用”,并且不将DIP等同于依赖注入实践。
欣快的

3
+1,但依赖倒置<>依赖注入。它们在一起可以很好地发挥作用,但是依赖倒置不仅仅是依赖注入。参考:野外DIP
Marjan Venema 2013年

3
@Andy:同样有用的是在接口上定义的单元测试,所有实现者(可以/被实例化的任何类)都针对该接口进行测试。
Marjan Venema 2013年

17

许多小类和接口到处都有依赖项注入。可能在大型项目中,您还将使用IoC框架来帮助您构造和管理所有这些小对象的生存期。参见https://stackoverflow.com/questions/21288/which-net-dependency-injection-frameworks-are-worth-look-into

请注意,一个严格遵循SOLID原则的大型.NET项目不一定意味着每个人都可以使用良好的代码库。根据面试官的身份,他/她可能希望您表明您了解SOLID的含义和/或检查您如何遵循设计原则。

您要成为SOLID,需要遵循以下步骤:

小号英格尔责任原则,让你将有很多小班他们每个人做一两件事只

O型封闭原则,在.NET中通常通过依赖项注入来实现,这也需要下面的I和D ...

使用 isliner在c#中解释L iskov替换原理可能是不可能的。幸运的是,还有其他问题可以解决,例如https://stackoverflow.com/questions/4428725/can-you-explain-liskov-substitution-principle-with-a-good-c-sharp-example

覆盖整个院落隔离原则工作亦随开闭原则。如果按字面意思执行,那就意味着倾向于使用大量非常小的接口,而不是少数几个“较大”的接口

D倾向反转原理高级类不应依赖于低级类,而两者都应依赖于抽象。


SRP并不意味着“只做一件事”。
罗伯特·哈维

13

我希望在一家商店的代码库中看到一些基本的东西,这些东西在日常工作中特别重视SOLID:

  • 许多小型代码文件-.NET中的最佳做法是每个文件一个类,并且“单一职责原则”鼓励小型模块化类结构,我希望看到很多文件,每个文件都包含一个小而集中的类。
  • 大量的Adapter和Composite模式-我期望使用大量的Adapter模式(通过“传递”到另一个接口的功能来实现一个接口的类)可以简化为一个目的而开发的依赖项的插入在不同地方也需要其功能。如果接口被更新以提供一种指定使用文件名的方式,那么像用文件记录器替换控制台记录器这样简单的更新将违反LSP / ISP / DIP。取而代之的是,文件记录器类将公开其他成员,然后适配器通过隐藏新内容使文件记录器看起来像控制台记录器,因此只有将所有这些都捕捉到的对象才需要知道它们之间的区别。

    类似地,当一个类需要添加与现有接口相似的依赖关系时,为了避免更改对象(OCP),通常的答案是实现Composite / Strategy模式(一个类实现了依赖关系接口并使用了多个其他接口接口的实现,逻辑量的变化允许类将调用传递给一个,一些或所有实现。

  • 许多接口和ABC-DIP必须要求存在抽象,而ISP鼓励对它们进行狭义范围的研究。因此,接口和抽象基类是规则,并且您将需要很多接口来覆盖代码库的共享依赖项功能。虽然严格的SOLID必须注入所有内容,但是很明显,您必须在某个地方创建,因此,如果仅通过对所述父表单执行某些操作将GUI表单仅作为一个父表单的子表单来创建,那么我就不必担心会更新该子表单直接从父代码中获取代码。我通常只是将该代码用作其自己的方法,因此,如果相同形式的两个动作打开了窗口,则只调用该方法。
  • 许多项目-所有这些的目的是限制变更范围。变更包括需要重新编译(不再是相对琐碎的练习,但是在许多处理器和带宽关键型操作中仍然很重要,例如将更新部署到移动环境中)。如果必须重建项目中的一个文件,则所有文件都必须重建。这意味着,如果将接口与其实现放置在相同的库中,则会遗漏要点;如果更改接口的实现,则必须重新编译所有用法,因为您还将重新编译接口定义本身,要求用法指向生成的二进制文件中的新位置。因此,请将接口与用法分开, 实施,同时按一般使用领域将它们分开,是一种典型的最佳实践。
  • 1994年“ 设计模式”一书中确定的设计模式强调了SOLID试图创建的小规模模块化代码设计,这引起了人们的广泛关注。例如,依赖反转原则和开放/封闭原则是该书中大多数已识别模式的核心。因此,我希望一家严格遵循SOLID原则的商店也采用“四人帮”一书中的术语,并根据它们的功能命名类,例如“ AbcFactory”,“ XyzRepository”,“ DefToXyzAdapter” ”,“ A1Command”等。
  • 通用存储库-与通常理解的ISP,DIP和SRP保持一致,存储库在SOLID设计中几乎无处不在,因为它允许使用代码以抽象的方式请求数据类,而无需对检索/持久性机制有特定的了解,并且它将执行此操作的代码放置在与DAO模式相对的位置(例如,如果您拥有Invoice数据类,那么您还将拥有一个InvoiceDAO来生成该类型的水合对象,依此类推)代码库/架构中的所有数据对象/表)。
  • 一个IoC容器-我不愿意添加此容器,因为我实际上不使用IoC框架来进行大部分依赖注入。它很快成为上帝对象的反模式,将所有东西扔进容器,摇晃它,并通过注入工厂方法倾倒了您需要的垂直水合依赖性。听起来不错,直到您意识到结构变得非常单一,并且带有注册信息的项目(如果“熟练”)现在必须了解解决方案中所有内容。这是很多改变的原因。如果它不流利(使用配置文件进行后期绑定注册),则程序的关键部分取决于“魔术字符串”,这完全不同于蠕虫。

1
为什么要下票?
KeithS 2013年

我认为这是一个很好的答案。您没有列出许多与这些术语相似的博客文章,而是列出了示例和解释来展示其用法和价值
Crowie 2013年

10

乔恩·斯凯特(Jon Skeet)对 SOLID中的“ O”如何“无助且理解不清” 的讨论分散了他们的注意力,让他们谈论阿利斯泰尔·考克本(Alistair Cockburn)的“受保护的变体”和乔什·布洛赫(Josh Bloch)的“继承设计,或禁止继承”。

Skeet文章的简短摘要(尽管我不建议您在不阅读原始博客文章的情况下就删除他的名字!):

  • 大多数人不知道“开放式封闭原则”中的“开放”和“封闭”是什么意思,即使他们认为确实如此。
  • 常见的解释包括:
    • 应该始终通过实现继承来扩展模块,或者
    • 原始模块的源代码永远无法更改。
  • OCP的基本意图以及Bertrand Meyer最初的表达是好的:
    • 该模块应具有其客户端可以依赖的定义明确的接口(不一定从技术上讲是“接口”),但是
    • 应该有可能在不破坏这些接口的情况下扩展其功能。
  • 但是“ open”和“ closed”这两个词只会使这个问题感到困惑,即使它们确实是一个很好的缩写。

OP问:“我如何更好地解决这个问题?” 作为一名进行面试的高级工程师,我对候选人能够产生聪明的想法谈论各种代码设计风格的利弊比对那些能够从项目符号要点中脱颖而出的人更加感兴趣。

另一个很好的答案是:“嗯,这取决于他们对它的理解程度。如果他们所知道的只是SOLID流行语,我希望滥用继承,过度使用依赖项注入框架,一百万个小型接口都不是。反映用于与产品管理进行沟通的领域词汇。”


6

可能有很多方法可以在不同的时间量内回答。但是,我认为这更像是“您知道SOLID意味着什么吗?”。因此,回答这个问题可能只是归结为要点并按照项目进行解释。

因此,您希望看到以下内容:

  • 类具有单一职责(例如,针对客户的数据访问类将仅从客户数据库中获取客户数据)。
  • 类很容易扩展而不会影响现有行为。我不必修改属性或其他方法即可添加其他功能。
  • 派生类可以替代基类,使用这些基类的函数不必将基类解包装为更特定的类型即可处理它们。
  • 界面小巧且易于理解。如果类使用接口,则不需要依赖多种方法来完成任务。
  • 代码足够抽象,因此高级实现不具体依赖于特定的低级实现。我应该能够在不影响高级代码的情况下切换低级实现。例如,我可以将我的SQL数据访问层切换为基于Web服务的层,而不会影响其余的应用程序。

4

这是一个很好的问题,尽管我认为这是一个艰难的面试问题。

SOLID原则确实支配着类和接口以及它们之间的相互关系。

这个问题实际上与文件而不是类有关。

我要给出的简短观察或答案是,通常您会看到仅包含接口的文件,并且通常约定是它们以大写字母I开头。除此之外,我还要提到文件中没有重复的代码(尤其是在模块,应用程序或库中),并且代码将在模块,应用程序或库之间的特定边界之间谨慎共享。

Robert Martin在使用Booch方法设计面向对象的C ++应用程序的C ++领域(请参阅有关内聚性,封闭性和可重用性的部分)和“ 干净代码”中讨论了这个主题。


.NET编码器IME通常遵循“每个文件1类”规则,并且还镜像文件夹/命名空间结构;Visual Studio IDE鼓励两种做法,ReSharper等各种插件都可以实施。因此,我希望看到一个反映类/接口结构的项目/文件结构。
KeithS13年
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.