返回博客

SOLID:面向对象设计的前 5 大原则?

SOLID:面向对象设计的前 5 大原则?

介绍

SOLID 是面向对象设计的五个原则的首字母缩写,这些原则是由 Robert C. Martin 提出的,他通常被称为 Uncle Bob。这些原则旨在帮助软件设计师、架构师、工程师和开发人员创建更灵活、更易于维护和更具扩展性的软件。通过遵循这些原则,您可以设计出更易于测试、重构、重用和扩展的类。

SOLID 首字母缩写代表:

S – 单一职责原则

O – 开闭原则

L – 里氏替换原则

I – 接口隔离原则

D – 依赖倒置原则

在本文中,我们将逐一解释每个原则,以了解它们如何帮助您编写更好的代码。此外,我们将为每个原则添加代码片段,向您展示如何在编码过程中应用它们,以及在整洁代码架构中应该避免什么。为了演示这些概念,我们将使用由 JetBrains 和开源贡献者开发的 Kotlin 编程 语言。

单一职责原则

单一职责原则(SRP)是一项软件设计原则,它规定程序中的每个类或模块都应该有一个单一且明确定义的职责。这意味着一个类应该只有一个引起它变化的原因,并且它应该只负责程序功能的一个单一部分。

考虑以下代码片段,作为如何在 Kotlin:

Kotlin 中的 data 关键字表示该类是一个数据类,这意味着它旨在保存数据,并且没有任何复杂的行为。使用这个数据类,我们可以创建一个 UserService  类来管理用户,如下所示:

在这个例子中, UserService 类具有单一职责:在数据库中管理用户。每个用户都由前面共享的 data class User 代码表示。 UserService 类中的所有方法都与此职责相关,因此具有内聚性。这使得该类更容易理解和维护,因为很明显,类中的所有方法都与一个单一且明确定义的任务相关。

UserService 类中,违反单一职责原则(SRP)的方法将是与该类的主要职责(即在数据库中管理用户)无关的方法。例如,考虑以下 UserService 类的变体:

在这个例子中, sendEmail  方法与 UserService 类的主要职责无关,该类的主要职责是在数据库中管理用户。此方法负责发送电子邮件,这与管理用户是不同的关注点。因此,该方法违反了单一职责原则(SRP),因为它为 UserService 类的更改引入了第二个原因。

为了遵守单一职责原则,最好将发送电子邮件的功能分离到一个单独的类中,例如 EmailService  类。这将允许 UserService 类专注于其管理用户的主要职责,而 EmailService 类专注于其发送电子邮件的职责。

您应该注意,单一职责原则并不是指一个类拥有的方法数量,而是指方法的内聚性以及类内部职责的清晰分离。

开闭原则

开闭原则(OCP)是一项软件设计原则,它指出软件实体(如类、模块或函数)应该对扩展开放,对修改关闭。这意味着应该可以在不更改现有代码的情况下,向类或模块添加新功能。

考虑前一个例子中的 UserService 类。假设我们想为 UserService 类添加一个新功能,允许我们通过电子邮件地址搜索用户。一种方法是向 UserService 类添加一个新方法,如下面代码中高亮显示的那样:

这种方法可行,但它违反了开闭原则,因为我们必须修改现有的 UserService 类才能添加新功能。更好的方法是使用继承或组合来扩展 UserService 类的功能,而无需修改其代码。

为了实现这一点,我们可以创建一个名为 UserSearchService  的新类,该类继承自 UserService 类并添加电子邮件搜索功能:

在这个例子中, UserSearchService 类对扩展是开放的,因为它提供了超出 UserService 类。同时, UserService 类仍然对修改关闭,因为我们不需要修改其代码来添加电子邮件搜索功能。

里氏替换原则

里氏替换原则(LSP)是一项软件设计原则,它指出父类的对象应该能够被子类的对象替换,而不会影响程序的正确性。这意味着子类应该是其父类的有效替代品,并且在相同的上下文中使用时,其行为应与父类相同。

我们将继续使用前面的示例中的 UserUserService 类进行演示。为了允许扩展 User 类,我们在 Kotlin 中使用 open  关键字:

这是原始的 UserService 类,它使用了上面的 User 数据类:

假设我们想创建一个 User 的子类,名为 AdminUser,用于表示具有管理员权限的用户。我们可以像这样实现:

在这个示例中, AdminUser 类是 User 类的有效替代品,因为它的行为与 User 类相同,并且可以在任何需要 User 对象的地方使用。例如,我们可以将 AdminUser 类与 UserService  类一起使用,如下所示:

这段代码是正确的,因为 AdminUser 类是 User 类的有效替代品,并且可以像 User 对象一样使用。

需要注意的是,里氏替换原则不仅仅关乎继承。它旨在确保子类的对象与父类的对象具有相同的行为方式,而不管子类是如何实现的。例如,如果 AdminUser 类的行为在某些方面与 User 类不同,它就会违反里氏替换原则,因为它不是 User 类的有效替代品。

接口隔离原则

接口隔离原则(ISP)是一项软件设计原则,它指出不应强迫客户端依赖它们不使用的 接口。这意味着,通常最好创建小而专一的 接口,让它们做好一件事,而不是创建大而通用的 接口,试图做很多事情。

以下是一个通过重写前面示例中的 User  和 UserService  类来应用该原则的示例:

在这个例子中, UserService 接口定义了四个与在数据库中管理用户相关的方法。 DatabaseUserService  类实现了这个接口,并为这些方法提供了具体的实现。

假设我们想为 UserService 接口添加一个新功能,允许我们通过电子邮件地址搜索用户。一种方法是在 UserService 接口中添加一个新方法:

您的代码将无法运行,除非您还在所有实现 UserService 接口的类中实现此方法:

虽然这种方法可行,但它违反了接口隔离原则,因为它强迫 DatabaseUserService  类去实现一个方法( searchUsersByEmail ),而该类可能并不需要或使用它。

更好的方法是创建一个单独的 接口 用于电子邮件搜索功能:

现在我们有了独立、小巧且专一的接口,即 UserService 和 UserSearchService,它们具有单一职责。需要这些接口所有功能的类可以实现它们,如下面的代码片段所示:

这符合接口隔离原则,因为它确保了客户端(例如 DatabaseUserService 类)不会被迫依赖它们不使用的接口。

为了更好地理解这个概念,假设我们有另一个名为 MemoryUserService  的类,它实现了 UserService  接口,但不需要电子邮件搜索功能,我们可以这样编写代码:

在这个例子中, MemoryUserService 类只需要实现定义在 UserService 接口中的方法,而不需要担心电子邮件搜索功能。这使得 MemoryUserService 类能够专注于其在内存中管理用户的首要职责,而不是被迫实现无关的功能。

依赖倒置原则

依赖倒置原则(DIP)是一项软件设计原则,它指出高层模块不应该依赖于低层模块,二者都应该依赖于抽象。这意味着,在设计软件时,通常最好使高层组件不与低层组件的具体实现绑定,而是依赖于可以以不同方式实现的抽象(例如接口或抽象类)。

让我们来看一个如何将该原则应用于前述代码片段中使用的 User 和 UserService 类的示例:

在这个例子中, UserService 类依赖于一个名为 UserRepository 接口的抽象,而不是依赖于用户存储库的具体实现。这使我们能够以不同的方式实现 UserRepository 接口,例如使用数据库或内存存储,而不会影响 UserService 类。

例如,这里是 UserRepository 接口的使用数据库的实现:

这里是 UserRepository 接口的使用内存存储的另一个实现:

这使得系统更加灵活和可维护,因为它允许我们在不影响系统其余部分的情况下更改存储库实现。它还使测试 UserService 类变得更加容易,因为我们可以在测试中模拟 UserRepository 依赖项。

结论

在本文中,我们讨论了 SOLID 的五个原则  代码和共享代码片段满足每个原则。遵循这些原则可以帮助您设计出更灵活、更易于维护和更具扩展性的软件系统。然而,请务必记住,这些原则是指导方针,而不是硬性规则,开发人员需要根据具体项目的背景来决定何时以及如何应用它们。继续通过查看我们的博客来学习更深入、最新的文章和 教程 关于 云计算 和 DevOps、软件设计与开发、需要关注的技术趋势等。

祝您编程愉快!

author

Preslav Dobrev

作者 · CloudSigma

Preslav Dobrev 是 CloudSigma 的创意设计师,专注于通过传统和创新营销渠道打造一致的企业形象。他擅长将艺术愿景与战略营销相融合,创造具有影响力的品牌叙事。

评论

暂无评论。发表第一条评论吧。