通过 SOLID 巩固您的软件设计理念






4.86/5 (64投票s)
在设计软件时,您应该始终以面向对象的方式进行思考。为了提高这种思维的准确性,您应该始终观察现实世界中对象的创建和管理方式。如果您了解设计原则,您将无所不能。
从现实世界开始
如果您想做一个娃娃屋,您可以轻松地自己完成。它不一定需要任何前期设计。您只需收集一些材料(例如纸张),然后根据您的想象开始建造。
把娃娃屋送给您的女儿。她会玩这个,过一段时间她就不喜欢了。然后您就把它扔掉,再做一个。
现在您的妻子要求您在她刚买的土地上建一栋房子。您能自己建房子吗?我知道这是一个愚蠢的问题。那么,如果您有 100 个人在您身边,您能开始建房子吗?答案是不能。如果我问为什么?您会立即给出显而易见的答案——“建房子涉及很多问题,在开始实际施工之前,您需要专家来处理这些问题。”
一些问题可能包括
1. 您妻子的房子梦想。
2. 最大可用土地面积。
3. 您的预算。
4. 核心建筑设计。
5. 平面布局。
6. 公用设施规划。
7. 停车位。
8. 等等。
所以,您需要开始工作的第一点是与您的妻子进行愉快的交谈,以便您确切地知道她想要什么。然后去建筑公司,他们将作为承包商建造您的房子。告诉他们您的期望、面积和预算。然后您将被介绍给土木工程师和建筑师。他们将详细了解您的需求并进行彻底的分析。之后,土木工程师和建筑师将提出设计和施工计划。他们将考虑所有问题,并有望以最高效率处理它们。他们做得越好,您的房子就会越好。另一方面,如果建筑师和土木工程师工作不力,您的房子将长期受苦。
因此,建造一栋伟大的房子不仅仅是您和工人的责任,更是建筑师和土木工程师能够实现这一目标,当然还有能够按照计划工作的熟练工人。如果您的设计和建筑出色,最困难的部分就已经完成了。
我相信,既然您正在阅读本文,您就是一名软件工程师。您能将这种房屋建造范式与软件开发世界联系起来吗?
让我们假设性地建造您的房子,将其视为一个软件,并尝试将房屋建造的设计策略与一些软件设计原则联系起来,以便我们能够认识到在设计软件时遵循标准原则的必要性。
首先,这栋房子的客户是谁?主要是您的妻子,她提供了您对房子的期望。然后您也是客户,将是客户方面的联系人。建筑公司将是正在建造您的房子的软件公司。因此,建立了一个敏捷的环境,可以确保最佳的生产,因为双方将始终保持紧密联系。现在呢?
现在,建筑师和土木工程师将开始分析您的规格,以提出一个好的设计。实际上,建造房子是一项非常艰巨的任务。所以,为了简单起见,我们不会涉及太多点。目标是,设计者应该以最好的方式设计您的建筑,我们将通过一些建筑范例来查看。
建筑物的地基
土木工程师将设计房子的地基。他可能会在设计中考虑以下几点:
1. 最大土地面积。
2. 最多可容纳的楼层数。
3. 您最初想建多少层。
4. 您未来扩建的计划。
然后可能,
5. 地基的深度是多少。
6. 需要多少个支撑墩以及它们的分布方式。
7. 等等。
在设计地基方案时,土木工程师需要与建筑师密切合作,以便支撑墩的分布不会影响建筑物的内部。
现在,一旦地基完成,就关闭了。支撑墩将是可扩展的,并在其上方,您的房子将逐层扩展。假设您最初建造您的房子有 2 层。几年后,世界经济衰退,您失业了。所以,您想在您的房子上面再建 2 层,使用您的积蓄,这样您就可以出租并获得一些持续的收入。假设工程师没有考虑到您可能想在您的房子上面再建几层。所以,他只为 2 层设计了地基,顶部的支撑墩被封死了。那么您现在该怎么办?不幸的是,您别无选择,只能拆除您的房子,重新做地基并重新建造。所以,工程师可能会杀了您。是不是?
另一方面,如果工程师很出色,他会做好地基,以便您可以上面再建几层,并且顶部楼层的支撑墩是可扩展的。所以,您的地基对任何修改都关闭了,但仍然对扩展开放。在这里,我们学到了一个非常重要的原则,它也适用于软件范式。那就是,您必须在您正在开发的模块的足够知识下构建软件的基类,然后从基类公开扩展点,以便子类可以扩展它。
用一句话来说,“您的基类将对任何修改都关闭,但仍然对扩展开放。”这个原则在软件界被称为开放-封闭原则。
开放-封闭原则的更多示例
查看下面的示例代码。如果您理解了开放-封闭原则,您就会轻松地看到第一个示例如何违反开放-封闭原则,以及第二个示例如何遵守它。
图片 – 违反开放-封闭原则
图片 – 遵守开放-封闭原则
室内设计——空调的安装位置
正如我之前所说,我们必须处理许多问题才能建造一栋房子。在所有这些问题中,像在卧室里安装空调这样的小问题同样重要。比如说,您的建筑师设计的房间可以在窗户里面安装空调。在他设计的时候,他想到了窗式空调,并在窗户里留了一个位置。现在,时代变了,您可能想安装一个更大的空调,或者您可能想安装一个分体式空调来替换旧的窗式空调。您需要做什么?
一种选择是您更换您的窗户,以便它允许您为空调腾出更大的空间。但您看,如果您这样做,窗户的尺寸可能会减小,最终可能不利于您房间的日光照射和空气流通。
所以,理想的解决方案可能是,如果空调的空间不是在您的窗户里面,而是在墙里面,您可以通过雇佣非常低技能的劳动力来改变墙壁的空间以安装您的新空调。
看看这里的好处
1. 您无需更换窗户。
2. 您的房间通风和采光不会有风险。
您是否看到了将空调安装在窗户里的设计的缺陷?我们都知道窗户的目的是为您的房间提供适当的光线和通风。所以,只有在您需要调整采光和通风时才应该更换窗户。如果您因为需要安装新的空调而不得不更换窗户,那就不是好事。
您建造设计中的这种缺陷与一个非常重要的软件设计原则相似。该原则规定,对象应该仅为一个原因而改变。换句话说,对象应该始终只有一个职责。这个原则在软件界被称为单一职责原则。
单一职责原则的更多示例
看看下面的简单示例。这里 `ApplicationSecurityContext` 类执行两个任务。一个是身份验证,另一个是登录。您知道在现代软件世界中,用户身份验证有许多方法,用户登录到您的应用程序也有许多方法。身份验证可以通过数据库、Windows 身份验证、单点登录等进行;登录可以通过表单身份验证、Windows 身份验证、基于 LDAP 的身份验证等进行。因此,您的应用程序的登录和身份验证机制很可能会发生变化。但是,如果您在同一个类中实现这两个功能,这个类就会承担您应用程序的两个非常大的责任。在这里,身份验证是系统的一个核心关注点,而登录与应用程序的客户端界面(桌面应用程序、智能客户端应用程序、Web 应用程序等)密切相关。在这种情况下,我们不应该在同一个类中实现这两个关注点,如果我们这样做,它将逐渐违反单一职责原则。如果我们继续将这些类型的关注点混入一个类中,它将逐渐变成一个巨大的混乱。
图片:违反单一职责原则
那么解决方案是什么?
![]()
图片:遵守单一职责原则
我们可以为身份验证创建一个单独的类。最好的方法是有一个像 `IAuthenticationProvider` 这样的接口,并实现您想要的身份验证。`FormsAuthenticationProvider` 类实现了您应用程序的表单身份验证。以后,如果需要,您可以轻松实现其他身份验证机制。因此,它既遵守单一职责原则,又遵守开放-封闭原则。
设计停车场
在现代建筑中,停车场是一个非常重要的区域。您的工程师和建筑师需要设计您建筑物的支撑墩,以便您的地下室或地面层能够容纳尽可能多的汽车。在设计过程中,他们应该考虑他们应该支持的汽车类型。通常在住宅中,吉普车可能是最大的车辆。所以,您的工程师应该设计停车场,以便每个空间都支持轻型车辆。现在,如果您的建筑师和工程师不仔细,他们可能会只为轿车设计空间。其后果是,当您需要容纳您的新普拉多时,您无法做到。
因此,设计原则应该是,每个停车位都应该支持一辆轻型车辆。这样,它也应该支持任何类型的轻型车辆,无论是小型轿车还是大型普拉多吉普车。
这个停车问题也可以描述另一个非常重要的软件设计原则。该原则规定,在系统中使用对象 X,如果对象 Y 是对象 X 的子类型,那么在该系统中使用对象 X 应该可以用对象 Y 替换。更正式地说,这个原则在软件界被称为里氏替换原则。
里氏替换原则的更多示例
里氏替换原则基本上意味着您在应用程序中创建的继承必须是正确的。这是里氏替换原则被违反的经典示例。示例中使用了 2 个类:`Rectangle` 和 `Square`。假设 `Rectangle` 对象在应用程序的某个地方被使用。我们扩展应用程序并添加 `Square` 类。`Square` 类基于某些条件由工厂模式返回,我们不知道确切返回的对象类型。但我们知道它是一个矩形。我们获取 `Rectangle` 对象,将宽度设置为 5,高度设置为 10,然后获取面积。对于宽度为 5、高度为 10 的矩形,面积应为 50。结果将是 100。(此示例摘自 http://www.oodesign.com/liskov-s-substitution-principle.html )
![]()
图片 – 违反里氏替换原则
电力分配线路
在您必须拥有的众多公用设施中,电力可能是最重要的一项。您的工程师和建筑师可能需要与电气工程师合作,设计您建筑物的电力分配线路。一种方法是,您建筑物的整个电流供应来自一个单一的根。但您会发现这将使整个供应链过于复杂。这将使其难以管理、扩展和故障排除。那么,解决方案应该是什么?
事实上,电力线路应该通过多个段进行接口。首先,每层可能有一个单独的根,然后可能是每个房间都有单独的根。如果您遵循这个设计原则,您将非常容易地管理和排除您的电力供应线路的故障。
同样,这与另一个重要的软件设计原则相似。该原则规定,一旦接口变得“过于臃肿”,就需要将其拆分为更小、更具体的接口,以便接口的任何客户端只知道与他们相关的方法。这个原则在软件界被称为接口隔离原则。
接口隔离原则的更多示例
接口隔离原则与我之前在这篇文章中解释的单一职责原则密切相关。这里,为 `IAuthenticationProvider` 创建一个不同的接口,将身份验证问题与登录机制隔离。同样,每当我们看到一个接口变得太重并且处理不止一个问题时,我们就应该隔离接口并为每个单独的任务创建接口。请参阅本文中的单一职责原则代码块以获取此示例。
整体建筑开发过程
到目前为止,我们已经讨论了一些关于您的建筑物建造的问题。现在让我们谈谈建筑物建造的过程。您已经聘请了土木工程师和建筑师,并且他们已经完成了设计。现在,实际施工将开始。大规模的施工工作将如何进行?肯定会有一个团队负责人。团队负责人最终会有不同的团队。例如,建筑工人、电工、采购团队等等。现在,责任将分配给每个团队,每个团队都将拥有自己的团队负责人。所以,整个施工工作的团队负责人只需要知道每个团队的individual 团队负责人。他不需要知道每个团队中有哪些成员在工作。这样,不同团队之间的依赖关系就被抽象到了 individual 团队负责人身上。因此,需求和设计的任何更改都不需要传达给项目中工作的 n 个人。n 个人所做的工作就是实现他们的团队负责人要求他们做的事情。
所以,原则上,团队负责人不依赖于不同的团队的 n 个人,而是通过 individual 团队负责人依赖于不同的团队。individual 员工依赖他们的团队负责人来知道他们需要实现什么。
这种建筑施工过程可以展示另一个非常重要的软件设计原则。该原则规定:
1. 高层模块不应依赖于低层模块。两者都应依赖于抽象。
2. 抽象不应依赖于细节。细节应依赖于抽象。
--维基百科
该原则被称为依赖倒置原则。
依赖倒置原则的更多示例
我们实际上已经看到了依赖倒置原则的代码示例。再次,如果您查看单一职责原则的代码块,您会看到 `IAutheticationProvider` 类型的参数被传递给 `LogIn` 方法。这里,`LogIn` 方法的客户端不需要直接知道 `IAutheticationProvider` 的实际实现,而是通过 `IAutheticationProvider` 接口的抽象层来获取身份验证。
图片 - 依赖倒置原则
在现代软件世界中,有相当多的依赖注入容器可以帮助我们编写遵循依赖倒置原则的代码。其中一些是 `Unity`、`NInject`、`Windsor, `StructureMap` 等。我计划稍后写一篇关于这个主题的文章。所以,请继续关注。
到目前为止,在这篇文章中,我试图解释 5 个非常重要的设计原则,我们在设计和编程软件时应该始终遵循。这些原则统称为 SOLID。
图片 - SOLID 来自维基百科
除此之外,还有一些其他原则我们也应该了解。但同样,我计划稍后写关于这些的。所以,请继续关注。