65.9K
CodeProject 正在变化。 阅读更多。
Home

SOLID 和 DRY

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.17/5 (16投票s)

2009年5月25日

CPOL

7分钟阅读

viewsIcon

94651

本系列文章共两部分,介绍 SOLID 和 DRY 首字母缩略词。本部分将介绍:Don't Repeat Yourself (不要重复自己)、Single Responsibility (单一职责)、Open/Closed (开放/封闭) 和 Liskov Substitution (里氏替换)。

不,这不是止汗露的广告。

许多开发人员深陷于自己的公司和项目,很少 venturing outside 去了解信息技术领域更广阔的世界,这真是一种有趣的现象。我在面试中经常遇到这种情况:一位高级开发人员在一家公司工作多年,迅速爬升到顶层,成为“骨干”,现在准备成为“架构师”。唯一的问题是,他们只知道如何在他们的 Visual Basic 6.0 系统上工作,并认为最好的架构是提取所有内容作为 ADO XML,并使用样式表转换来驱动应用程序(我之所以这样说,是因为这是我过去曾使用并实施过的框架)。

我可以举出两个很好的例子。第一个是测试驱动开发 (TDD)。你可以在面试中问一个关于 TDD 的问题,虽然很多人从未听说过它,但你会得到一些人说:“是的,当然,我遵循 TDD。”“具体怎么做?”“哦,我们编写单元测试。”如果你认为编写单元测试意味着你在实践 TDD,你可能需要做一些进一步的研究。第二个例子是泛型——大多数人都熟悉 C# 中的泛型。它们是用来做什么的?当然是用来处理列表的!(同样,如果你认为泛型仅用于类型化列表以防止装箱,你可能需要更深入地研究一下)。哦,我还想到第三个:委托只是为了事件,对吧?

我的观点是,有时我们会固守于对我们的系统有效并且运行良好的方法,但它并不总是正确或最佳的解决方案,作为软件工程师(或者我们今天称呼自己为什么)我们需要不断挑战自己,以保持对新事物的了解。

我决定在我的 C#er : IMage 博客上发布这篇文章,因为我认为 SOLID 和 DRY 是非常健全的软件开发原则,但我却经常看到人们在这两个概念上 stumbling。

我们先来处理 DRY

不要重复自己

这其实是一个简单的原则。

我正在开发一个应用程序,需要验证一个字段是否符合 IP 地址的特定模式。所以我插入了一个文本框,附加了一个自定义的正则表达式验证器控件,然后就可以开始了。一年后,我们有一个庞大的应用程序,到处都是 IP 地址,我们发现我们用 Google 搜索到的用于验证 IP 地址的正则表达式有一个 bug!现在我们需要进行搜索和替换,以及大量的单元测试。

我第二次添加正则表达式验证器时就应该意识到这一点。很容易想:“我写的是好代码,因为我不是手工编写正则表达式验证,我使用的是验证器。”但即使是这样的事情,也可以用来创建一个“IP 文本框控件”,并将验证器嵌入其中。然后,我就可以插入该控件,而不是重复自己。如果它需要更改,我只需更改一次。

你可以通过用字符串字面量装饰代码而不是将它们折叠成常量,或者拥有一些小的实用方法而不对自己说“哇,我刚才写了这个方法两次……是时候把它移到一个类里了”来重复自己。这是一个强大的原则,可以帮助你编写可扩展、可维护的代码。你不需要给它起一个花哨的名字,比如“单一真相来源”,来认识到如果你发现自己重复了什么,任何东西,它都是一个很好的候选者,可以将其分解成一个单独的、可重用的类。

这就引出了 SOLID,它本身就是一个研究领域,所以我会在这里简要介绍。我的目的是仅仅向你介绍这个原则,以便你可以自己研究并从中学习,因为我看到很多开发者可以从理解它中获益。

SOLID 是一个缩写,代表

  • Single Responsibility Principle (单一职责原则)
  • Open/Closed Principle (开放/封闭原则)
  • Liskov Substitution Principle (里氏替换原则)
  • Interface Segregation Principle (接口隔离原则)
  • Dependency Inversion (依赖倒置)

单一职责原则

一个类应该只有一个,并且只有一个改变的原因。

这和 DRY 很接近,不是吗?我们说:保持简单。一种常见的模式是模型-视图-控制器 (MVC)。你不是将表示层、数据持久化和业务逻辑全部放在同一个类中,而是将它们分开,使每个类都专注于自己的事情。为什么这如此重要?因为应用程序的每一个操作不仅是一个潜在的故障点,也是一个变化点。换句话说,“做某事”可能在未来会变成“做不同的事”。

一个常见的例子是编写一个下拉列表类。它调用数据库,执行“order by”,然后渲染控件。然后你发现另一个页面需要相同的控件,但作为一个具有“动态搜索”功能的文本框。所以你复制了这个控件,使用了一些很酷的 AJAX,看起来都不错,直到你被告知州信息将不再存储在数据库中,而是必须通过 XML 进行管理。现在,由于数据存储的一次更改,你有两个控件需要更改。

如果你遵循了单一职责原则,你可能已经有了四个类:一个用于获取数据(持久化管理),一个用于排序(业务逻辑),一个用于将数据绑定到下拉列表,还有一个用于那个花哨的“前瞻性”控件。当发生更改时,你只需在一个地方进行更改。

这个原则也使得扩展开发团队更容易。你听说过“九个女人不能一个月生一个孩子”这句谚语吗?通过这个原则,你可以通过将应用程序分解成更小、更易于维护的工作单元来扩展应用程序的交付速度。

开放/封闭原则

对扩展开放,对修改关闭。

这简单地说明,你应该能够在不修改类核心行为的情况下扩展类的功能。它对扩展开放,对修改关闭。同样,让我们举个例子。你可能有一个表示系统中用户的类,并且你认为用户基本由登录名、密码和电子邮件地址组成。之后,你发现有些用户可以有联系信息。最终,你了解到有些用户是管理员,他们有额外的,比如他们可以管理应用程序的哪些部分的信息。

一个违反这些原则的类将要求你拥有一个臃肿的类,其中有大量的标志来指示用户是什么/谁。每次你需要更改或添加东西时,你都需要更改那个原始类。

一个更稳定的方法是将你的基础知识放入一个基类中。现在你可以有一个 ContactUser : BaseUser ,并通过扩展用户来包含联系信息。然后你可以有一个 AdminUser : BaseUser ,或者一个 AdminUser : ContactUser。你是在扩展,而不是修改基类。我之前提到了泛型:这是一个完美的例子,泛型可以用来定义一些基础功能(例如,在你的数据访问层中,打开和关闭连接),而你的扩展则为特定类提供强类型行为。

里氏替换原则

派生类必须能够替换其基类。

为什么这如此重要?想想上面关于不同联系人的例子。如果我有一个验证登录的函数,那么我应该能够简单地检查类的用户名和密码。要做到这一点,我不需要知道这个类是基类用户、联系用户还是管理员用户。所有这些类都扩展了基类用户,所以我将针对基类用户编写登录逻辑。联系用户像基类用户一样运行。

如果我决定编写另一个显示用户信息类呢?我把它做成 UserInfo<T>,但通过这样做打破了这个原则

if (typeof(T) == typeof(BaseUser) { return ((BaseUser)T).Email; }
else if (typeof(T) == typeof(ContactUser) { return ((ContactUser)T).Phone; }
...
else { throw new Exception("Could not determine the type."); }

发生了什么?它表面上可能有效,但突然间我得到了一个非常困难的类。这个类现在需要理解 T 的所有可能的派生类型才能完成它的工作。如果我必须不断地回到同一个地方进行更改和跟上进度,应用程序如何能够扩展?我可能会聘请一位新开发人员,他去构建一个 AccountingUser ……然后导致系统崩溃,因为他们没有更新 UserInfo

更好的方法是重写 ToString 方法。每个类都可以实现自己的“显示信息”版本。任何其他对象都可以处理 BaseUser (而不关心派生类型是什么),只需调用 ToString() 即可显示有用信息。

现在事情开始变得有趣了,因为我们要讨论一个今天最受欢迎的“流行词”:依赖注入/控制反转……敬请关注,因为我们还有两个原则要讲!

Jeremy Likness
© . All rights reserved.