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

我如何向妻子解释面向对象设计

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (348投票s)

2010年7月12日

CPOL

19分钟阅读

viewsIcon

745087

downloadIcon

1

通过有趣的对话学习面向对象设计原则。

引言

我的妻子 Farhana 希望恢复她的软件开发职业生涯(她最初是一名软件开发者,但因为第一个孩子的出生没能继续深入),最近我一直在帮助她学习面向对象设计,因为我在软件设计和开发方面有一些经验。

从我早期从事软件开发开始,我一直观察到,无论技术问题看起来多么困难,如果能从现实生活的角度来解释并以对话的方式讨论,它总是会变得更容易。由于我们进行了一些关于面向对象设计的富有成效的对话,我想我可以分享出来,因为有人可能会觉得这是一种有趣的 OOD 学习方式。

以下是我们进行 OOD 对话的过程:

主题:介绍 OOD

Shubho:亲爱的,我们开始学习面向对象设计吧。你知道面向对象原则,对吧?

Farhana:你是说封装、继承和多态,对吧?是的,我知道这些原则。

Shubho:好的,我希望你已经知道如何使用类和对象了。今天我们来学习面向对象设计。

Farhana:等一下。面向对象原则不足以进行面向对象编程吗?我的意思是,我可以定义类,并将属性和方法封装起来。我还可以根据类之间的关系定义一个类层次结构。那么,还剩下什么呢?

Shubho:好问题。面向对象原则和面向对象设计实际上是两件不同的事情。我给你举个现实生活中的例子来帮助你理解。

你小时候先学会了字母,对吧?

Farhana:是的。

Shubho:好的。你也学会了单词,以及如何组合字母来构成有意义的单词。同时,你还学会了一些基本的语法来将单词组合成句子。例如,你必须维护时态,并且必须正确使用介词、连词等来创建语法正确的句子。比如,像下面这样的句子:

"I" (pronoun) "want" (Verb) "to" (Preposition) "learn" (Verb) "OOD" (Noun)

你看,你按照一定的顺序组合单词,并且你还选择了正确的单词来构成一个有意义的句子。

Farhana:好的,那么,这有什么意义呢?

Shubho:这相当于面向对象原则。OOP 讲的是进行面向对象编程的基本原则和核心思想。这里,OOP 可以与基本的英语语法相比较,基本的语法教你如何使用单词来构建一个有意义且正确的句子,而 OOP 教你构建类,将属性和方法封装在类中,并建立它们之间的层次结构并在你的代码中使用它们。

Farhana:嗯……我明白了。那么,OOD 在这里如何发挥作用呢?

Shubho:你很快就会得到答案。现在,假设你需要写一些关于某些主题的文章和散文。你可能还想写一些关于你专业知识的不同主题的书。仅仅知道如何构建句子不足以写出好的散文/文章或书籍,对吧?你需要写很多东西,并学会以一种好的方式来解释事情,这样读者才能轻易理解你想表达的意思。

Farhana:听起来很有趣……继续。

Shubho:现在,如果你想写一本关于某个特定主题(例如,“学习面向对象设计”)的书,你必须知道如何将主题分成更小的部分。你还需要为这些主题写章节,并需要在章节中撰写序言、引言、解释、示例以及许多其他段落。你需要设计整本书,并学习一些写作技巧的最佳实践,以便读者能够轻松理解你试图解释的内容。这涉及到大局。

在软件开发中,OOD 解决的是大局问题。你需要以一种模块化、可重用且灵活的方式来设计你的类和代码,并且有一些好的指南可以遵循,这样你就不用重复造轮子了。你可以应用一些设计原则来设计你的类和对象。有道理吗?

Farhana:嗯……有初步的想法了,但还需要学更多。

Shubho:别担心,你很快就会学会的。继续我们的讨论。

主题:为什么需要 OOD?

Shubho:这是一个非常重要的问题。当我们能够快速创建一些类并完成开发和交付时,为什么还要关心面向对象设计呢?这还不够吗?

Farhana:是的,我以前不知道 OOD,但我仍然能够开发和交付项目。那么,有什么大不了的呢?

Shubho:好吧,我给你引用一句经典名言:

“如果水面和软件开发需求都是冻结的,那么在水面行走和根据需求开发软件都是容易的。”

- Edward V. Berard

Farhana:你的意思是软件开发需求一直在变化?

Shubho:正是如此!软件的普遍真理是“你的软件注定要改变”。为什么?

因为你的软件解决的是现实世界的业务问题,而现实世界的业务流程不断演变和改变——总是如此。

你的软件今天能做它该做的事情,而且做得足够好。但是,你的软件足够智能以支持“变化”吗?如果不是,那么你就没有一个设计精良的软件。

Farhana:好的,那么,请解释一下“设计精良的软件”吧,先生!

Shubho:“设计精良的软件能够轻松适应变化;它可以扩展,并且是可重用的。”

而应用良好的“面向对象设计”是实现这种智能设计的关键。那么,什么时候你可以声称你在代码中应用了良好的 OOD 呢?

Farhana:这也是我的问题。

Shubho:如果你的代码满足以下条件,你就实现了面向对象设计:

  • 当然,是面向对象的。
  • 可重用的。
  • 可以以最小的努力进行更改。
  • 可以在不更改现有代码的情况下进行扩展。

Farhana:还有呢?

Shubho:我们并非孤军奋战。许多人已经对这个问题进行了深入思考并付出了巨大努力,他们试图实现良好的面向对象设计,并识别出一些基本原则来指导面向对象设计(你可以用这些基本灵感来制定你的面向对象设计)。他们还识别出了一些适用于常见场景的通用设计模式(基于基本原则)。

Farhana:能举些例子吗?

Shubho:当然。有很多设计原则,但在基本层面,有五个原则合称为 SOLID 原则(感谢伟大的 OOD 导师 Uncle Bob)。

S = Single Responsibility Principle
O = Opened Closed Principle 
L = Liscov Substitution Principle
I = Interface Segregation Principle
D = Dependency Inversion Principle

在接下来的讨论中,我们将详细探讨其中的每一个。

主题:单一职责原则 (SRP)

Shubho:我先给你看海报。我们应该感谢制作这些海报的人,它们真的很有趣。

单一职责原则海报

它说,“仅仅因为你可以在单个设备上实现所有功能,你不应该这样做”。为什么?因为它最终会给你带来很多可管理性问题。

让我用面向对象的术语来解释这个原则。

一个类永远不应该有超过一个修改的原因。

或者换句话说:“一个类应该只有一个职责。”

Farhana:能解释一下吗?

Shubho:当然,这个原则说,如果你有一个类有多个修改原因(或有多个总体职责),你需要根据它们的职责将该类分成多个类。

Farhana:嗯……这意味着一个类不能有多个方法吗?

Shubho:一点也不是。相反,你肯定可以在一个类中拥有多个方法。问题在于,它们必须满足一个单一的目的。那么,为什么拆分很重要呢?

它之所以重要,是因为

  • 每个职责都是一个变化轴。
  • 如果类具有多个职责,代码就会耦合。

Farhana:能举个例子吗?

Shubho:当然,看看下面的类层次结构。实际上,这个例子是 Uncle Bob 提供的,再次感谢他。

显示违反 SRP 原则的类层次结构

在这里,Rectangle 类执行以下操作:

  • 计算矩形面积的值。
  • 在 UI 中渲染矩形。

并且,有两个应用程序在使用这个 Rectangle 类:

  • 一个计算几何应用程序使用此类来计算面积。
  • 一个图形应用程序使用此类在 UI 中绘制矩形。

这违反了 SRP(单一职责原则)!

Farhana:怎么会?

Shubho:你看,Rectangle 类实际上在做两件不同的事情。它在一个方法中计算面积,在另一个方法中返回矩形的 GUI 表示。这会导致一些有趣的问题:

  • 我们必须在计算几何应用程序中包含 GUI。部署几何应用程序时,我们必须包含 GUI 库。
  • 对图形应用程序的 Rectangle 类的更改可能会导致计算几何应用程序的更改、构建和测试,反之亦然。

Farhana:越来越有趣了。所以我想我们应该根据类的职责来拆分它,对吗?

Shubho:正是如此。你能预测我们应该怎么做吗?

Farhana:当然,让我试试。以下是我们可能需要做的事情:

将职责分成两个不同的类,例如:

  • Rectangle:此类应定义 area() 方法。
  • RectangleUI:此类应继承 Rectangle 类并定义 Draw() 方法。

Shubho:完美。在这种情况下,Rectangle 类将由计算几何应用程序使用,而 RectangleUI 类将由图形应用程序使用。我们甚至可以将类分成两个单独的 DLL,这样当一个类需要实现更改时,我们就无需触及另一个类。

Farhana:谢谢,我认为我理解了 SRP。有一点,SRP 似乎是将事物分解成分子部分的想法,以便它们可以重用并且可以集中管理。那么,我们是否也应该在方法级别应用 SRP 呢?我的意思是,我们可能写了一些代码行很多的方法来做多件事情。这些方法可能违反 SRP,对吧?

Shubho:你说对了。你应该分解你的方法,使每个方法做一件特定的工作。这将允许你重用方法,并且在需要更改时,你可以通过修改最少量的代码来实现更改。

主题:开闭原则 (OCP)

Shubho:开闭原则的海报来了。

:开闭原则海报

如果用面向设计的方式来解释,它将是这样的:

软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。

最基本地说,这意味着你应该能够在不修改类的情况下扩展类的行为。就像我应该能够在不改变身体的情况下穿上任何衣服一样,哈哈。

Farhana:有意思。你可以通过穿任何你想要的衣服来改变你的造型,而不需要改变你的身体。所以你对扩展是开放的,对吗?

Shubho:是的。在 OOD 中,“对扩展开放”意味着模块/类的行为可以被扩展,并且我们可以让模块以新的、不同的方式运行,以应对需求的变化或满足新应用程序的需求。

Farhana:你的身体是关闭修改的。我喜欢这个例子。所以,核心类或模块的源代码在需要扩展时就不应该被修改。能举些例子吗?

Shubho:当然,看看下面的例子。这不支持“开闭”原则:

显示违反开闭原则的类层次结构

你看,Client 和 Server 类都是具体的。所以,如果出于任何原因更改了服务器实现,客户端也需要更改。

Farhana:有道理。如果浏览器与特定服务器(如 IIS)紧密耦合,那么如果服务器因为某种原因被另一个服务器(如 Apache)替换,浏览器也需要更改或替换。那将是多么糟糕!

Shubho:正确。所以,以下是正确的设计:

显示开闭原则的类层次结构

在这个例子中,添加了一个抽象服务器类,客户端持有一个抽象类的引用,具体的服务器类实现了抽象服务器类。所以,如果因为任何原因服务器实现被更改了,客户端不太可能需要任何更改。

这里的抽象服务器类是关闭修改的,而具体的类实现是**开放**扩展的。

Farhana:根据我的理解,抽象是关键,对吗?

Shubho:是的,基本上,你抽象了你系统中的核心概念,如果你做得好,当功能需要扩展时(比如服务器是一个抽象概念),它很可能不需要任何更改。你会在实现中定义抽象的东西(例如,IISServer 实现 Server),并且尽可能多地针对抽象(Server)进行编码。这将允许你扩展抽象的东西并定义一个新的实现(例如,ApacheServer),而无需更改客户端代码。

主题:里氏替换原则 (LSP)

Shubho:“里氏替换原则”这个名字听起来很费劲,但这个思想非常基础。看看这个有趣的帖子:

里氏替换原则海报

这个原则是说:

子类型必须能够替换其基类型。

或者换句话说:

“使用基类型引用的函数必须能够使用派生类对象而不自知。”

Farhana:抱歉,对我来说有点含糊。我认为这是 OOP 的基本规则。这是多态,对吧?为什么在这个问题上需要一个面向对象原则?

Shubho:好问题。这是你的答案:

在基本的面向对象原则中,“继承”通常被描述为“is a”关系。如果一个“Developer”是一个“SoftwareProfessional”,那么“Developer”类应该继承“SoftwareProfessional”类。这种“Is a”关系在类设计中非常重要,但很容易被冲昏头脑,导致错误的设计和糟糕的继承。

“里氏替换原则”只是一种确保继承被正确使用的方法。

Farhana:我明白了。有趣。

Shubho:是的,确实如此。我们来看一个例子:

显示里氏替换原则示例的类层次结构

在这里,KingFisher 类扩展了 Bird 基类,因此继承了 Fly() 方法,这很好。

现在看看下面的例子:

里氏替换原则的已更正类层次结构

鸵鸟是一种鸟(当然是!),因此它继承了 Bird 类。那么,它能飞吗?不能!在这里,设计违反了 LSP。

所以,即使在现实世界中这似乎很自然,但在类设计中,鸵鸟不应该继承 Bird 类,应该有一个单独的类来处理不能真正飞的鸟,而鸵鸟应该继承那个类。

Farhana:好的,明白了。那么,让我试着指出 LSP 为什么如此重要:

  • 如果不维护 LSP,类层次结构将一团糟,如果将子类实例作为参数传递给方法,可能会出现奇怪的行为。
  • 如果不维护 LSP,基类的单元测试将永远无法通过子类。

我说对了吗?

Shubho:你说得完全正确。你可以设计对象并应用 LSP 作为验证工具来测试层次结构是否正确地进行了继承。

主题:接口隔离原则 (ISP)

Shubho:今天我们将学习“接口隔离原则”。这是海报:

接口隔离原则海报

Farhana:这是什么意思?

Shubho:它的意思是:

客户端不应该被迫依赖它们不使用的接口。

Farhana:请解释一下。

Shubho:当然,这是你的解释:

假设你想买一台电视机,你有两个选择。一个有很多开关和按钮,其中大部分看起来很复杂,对你来说似乎并不必要。另一个只有几个开关和按钮,看起来很熟悉并且合乎逻辑。假设这两台电视机提供的功能大致相同,你会选择哪一个?

Farhana:显然是第二个,开关和按钮更少的那一个。

Shubho:是的,但为什么?

Farhana:因为我不需要那些看起来复杂且不必要的开关和按钮。

Shubho:正确。同样,假设你有一些类,并且你通过接口暴露这些类的功能,以便外界可以知道类的可用功能,客户端代码可以针对接口进行编写。现在,如果接口太大,暴露的方法太多,对外界来说就会显得很复杂。而且,方法过多的接口可重用性较低,“胖接口”带有额外的无用方法会导致类之间的耦合增加。

这还会导致另一个问题。如果一个类想实现该接口,它必须实现所有方法,其中一些方法可能根本不需要该类。因此,这样做也会引入不必要的复杂性,并降低系统的可维护性或健壮性。

接口隔离原则确保接口的开发方式是每个接口都有自己的职责,因此它们是特定的、易于理解和可重用的。

Farhana:我明白了。你的意思是接口应该只包含必要的方法,而不是别的?

Shubho:正是如此。我们来看一个例子。

下面的接口是一个“胖接口”,它违反了接口隔离原则:

违反接口隔离原则的接口示例

请注意,IBird 接口除了 Fly() 方法之外,还定义了许多鸟类的行为。现在,如果一个 Bird 类(例如,Ostrich)实现了这个接口,它就必须不必要地实现 Fly() 方法(鸵鸟不会飞)。

Farhana:这是正确的。那么,这个接口必须拆分吗?

Shubho:是的。“胖接口”应该被分解成两个不同的接口,IBirdIFlyingBird,其中 IFlyingBird 继承 IBird

接口隔离原则示例中接口的正确版本

如果有一种不会飞的鸟(例如,鸵鸟),它将实现 IBird 接口。如果有一种会飞的鸟(例如,KingFisher),它将实现 IFlyingBird 接口。

Farhana:那么,如果我回到那个有很多开关和按钮的电视机的例子,那台电视机的制造商肯定有一个蓝图,其中包含了开关和按钮。每当他们想创建一个新的电视机型号时,如果需要重用这个蓝图,他们就需要创建与蓝图中包含的开关和按钮数量相同的开关和按钮。这不允许他们重用蓝图,对吗?

Shubho:正确。

Farhana:而且,如果他们真的想重用他们的蓝图,他们应该将电视机的蓝图分成更小的部分,以便在创建任何新类型的电视机时都可以重用该蓝图。

Shubho:你明白了。

主题:依赖倒置原则 (DIP)

Shubho:这是 SOLID 原则中最后一个。这是海报:

依赖倒置原则海报

它说……

高层模块不应该依赖于低层模块。两者都应该依赖于抽象。

Shubho:我们来举一个现实世界的例子来理解它。你的汽车由很多对象组成,比如发动机、车轮、空调和其他东西,对吧?

Farhana:是的,当然。

Shubho:好的,这些东西都不是刚性地内置在一个单元中的;相反,它们都是“可插拔的”,这样当发动机或车轮出现问题时,你可以修复它(而不修复其他东西),甚至可以更换它。

更换时,你只需要确保发动机/车轮符合汽车的设计(例如,汽车可以接受任何 1500 CC 的发动机,并且可以在任何 18 英寸的车轮上运行)。

此外,汽车可能允许你将 2000 CC 的发动机安装在 1500 CC 的发动机位置,前提是制造商(例如,丰田)相同。

那么,如果你的汽车的不同部件不是以这种“可插拔”的方式制造的呢?

Farhana:那将是可怕的!因为那样的话,如果你的汽车发动机坏了,你就得修理整辆车或者买一辆新的!

Shubho:是的。那么,“可插拔性”是如何实现的呢?

Farhana:“抽象”是关键,对吗?

Shubho:是的。在现实世界中,汽车是更高级别的模块/实体,它依赖于像发动机或车轮这样的低级别模块/实体。

汽车不是直接依赖于发动机或车轮,而是依赖于某些发动机或车轮规范的抽象,这样,如果任何发动机或车轮符合该抽象,就可以与汽车组装,汽车就能运行。

让我们来看看下面的类图:

依赖倒置原则类层次结构

Shubho:在上面的 Car 类中,请注意有两个属性,它们都属于抽象类型(Interface)而不是具体类型。

发动机和车轮是可插拔的,因为汽车将接受任何实现声明接口的对象,而这不会要求对 Car 类进行任何更改。

Farhana:那么,如果代码中没有实现依赖倒置,我们就面临着:

  • 损坏使用低级类的更高级别代码。
  • 当低级类发生变化时,需要花费大量时间和精力来更改更高级别代码。
  • 产生可重用性较低的代码。

Shubho:你说得太对了,亲爱的!

摘要

Shubho:除了 SOLID 原则之外,还有许多其他面向对象原则。有些是:

  • “组合优于继承”:这指的是偏好组合而非继承。
  • “最少知识原则”:这指的是“你的类知道得越少越好”。
  • “共同封闭原则”:这指的是“相关的类应该被打包在一起”。
  • “稳定抽象原则”:这指的是“一个类越稳定,它就越应该由抽象类组成。”

Farhana:我难道不也应该学习那些原则吗?

Shubho:是的,当然。你有整个万维网可以学习。只需在网上搜索这些原则并尝试理解。当然,如果你需要帮助,不要犹豫问我。

Farhana:我听说有许多设计模式是建立在这些设计原则之上的。

Shubho:你说得对。设计模式不过是在常见重复出现的情况下提供的一些普遍建议的设计。它们主要受面向对象设计原则的启发。你可以将设计模式视为“框架”,将 OOD 原则视为“规范”。

Farhana:那么,我接下来要学习设计模式吗?

Shubho:是的,亲爱的。

Farhana:那会很令人兴奋,对吧?

Shubho:是的,那将非常令人兴奋。

下一篇

© . All rights reserved.