CatCoding

给老婆介绍 OOD(翻译)

2011-03-03

晚上看到个有趣的文章,翻译了一下,看过 Head First 绕过。 原文在这里

我的妻子 Farhana 想重新她软件开发师的职业生涯 (她以前也是个软件开发师,但是因为第一个孩子的出生而没有继续下去)。所以,这段时间我在帮助她学习一些 OOD 方面的东西,我是一个比较有开发经验的程序员。

从我早期的职业生涯中,我发现不管是多么复杂的技术问题,如果从普通交谈中以平常生活常见的角度去解释往往变得更容易理解。因为之前我和她有不少富有成果的交谈,我想可以和大家一起分享一下这种学习 OOD 的有趣方式。

下面是我们学习 OOD 的对话:

OOD 介绍

  • Shubho : 好,让我们开始学习 OOD,你已经知道了面向对象三大特性,对吗?

  • Farhana: 你是指封装、继承、多态吗?是的,这些我知道。

  • Shubho : 好,希望你已经知道了使用对象和类,让我们今天开始学习 OOD。

  • Farhana: 等等,知道面向对象特性还不够面向对象程序设计吗?我的意思是,我能定义类,封装成员变量和函数,我也能根据类之间的关系定义继承类。那还有什么需要学的么?

  • Shubho : 好问题,OOP 和 OOD 是两码事。让我给个例子给你。当你还是小孩的时候你学会了字母表,对吧?

  • Farhana: 嗯

  • Shubho : 好,你也学会了如何用字母形成一个个有意义的单词,同时,你也学会了一些语法来造句子。比如,你要维持时态,使用介词、连接词、和其他语法来造出正确的句子。比如说一个句子像下面这样。”I” (pronoun) “want” (Verb) “to” (Preposition) “learn” (Verb) “OOD” (Noun) 你看,你要让这些单词安特定的顺序组成,你也 要选取正确的词来使得这个句子有意义。

  • Farhana: 呃,这是什么意思?

  • Shubho : 这和 OOP 是类似的。OOP 是面向对象程序设计的基本原则和核心思想。这里,OOP 对应于英语语法,这些基本语法告诉你如何用单词去构造一句有意义的话,
    OOP 告诉你使用类,封装成员变量和方法,也告诉你在代码中使用继承关系。

  • Farhana: 嗯,有点懂了。那么 OOD 对应于什么呢?

  • Shubho : 你马上就知道。好,现在比如说你想要就一个论题写一些文章。你也想就一些你比较精通的方面写一些书。知道如何遣词造句还不够写一篇好文章或者好书出来,对吧?你还需要学习很多,你需要知道如何用一种好的方式去解释一个东西,这样读者才能了解你到底在说什么。

  • Farhana: 有点趣,继续。

  • Shubho : 好,现在比如说你想就 OOD 方面写一个本书,你需要知道如何把这个主题分为小题目。然后在这些小议题上面逐章地写,你还要写前言、简介、解释、例子,还有许多其他段落。你需要知道如何从整体上把握这本书的构造,甚至需要一些写作技巧。这才能让你的书通俗易懂。在软件设计领域,OOD 同样是个更上层的角度。你需要好好的设计,使得你的类和代码可以更好地模块化、复用、灵活。使用这些设计原则可以是你少重复发明轮子。懂了吗?

  • Farhana: Hmm,我明白了一些,但是请继续。

  • Shubho : 别急,一会你就知道了。我们只管讨论就是了。

Why OOD?

  • Shuboho : 这有个很重要的问题,为什么我们需要 OOD,我们明明就能很快的稀里糊涂的设计一些类,赶快完成开发然后交付?这还不够么?

  • Shubho : 就是,我以前也不知道 OOD,我仍然能开发完成项目。那这有什么问题么?

  • Shuboho : 好,让我来给你一个经典的引用:

Walking on water and developing software from a specification are easy if both are frozen.” - Edward V. Berard
(如果水是冰冻的在上面行走很方面,如果规格书是不变的,开发软件也很方便)

  • Shubho : 你是说软件的需求说明书一直都在变化?

  • Shuboho : 正确,最普遍的真理就是”你的软件注定都要变化”,为什么?因为你的软件需要解决的是现实生活中的问题,而这些都是会变化的—永远会变。你的软件按照今天需要做的,做的足够好。但是你不设计得足够好,你的软件足够灵活来应对”变化”吗?

  • Shubho : 好,这样,快给我介绍什么是”设计得足够灵活的软件”!

  • Shuboho : “一个设计的灵活的软件是容易适应变化的,它能够便于扩展和复用”。而使用一种好的”面向对象设计”方式是得到这种灵活设计的关键。但是,我们有什么标准来说明我们的代码中使用了良好的 OOD?

  • Shubho : 呃嗯,这也是我的问题。

  • Shuboho : 你需要做到了下面几点:

面向对象方式
可复用     修改代价最小化
不修改现有代码的基础上扩展

前人已经在这方面做了许多工作,他们已经对一些通用的场景列出了一些通用的设计准则。最基本的五点可以简称为 SOLID 原则 (Uncle BoB)。

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

下面我们逐一介绍上面的几个原则。

Single Responsibility Principle 单一职责原则

Shubho : 先来看幅图,很形象。你能把所有的功能都集成在一个东西上,但是真的不应该。为什么?因为这为以后增加了很多额外的管理工作。我来用 OO 术语解释一下,”不能有多个理由去改变一个类”,或者说”一个类有且只能有单一职责”。

Farhana: 能解释一下吗?


  • Shubho : 让我们来看这个继承的例子,这是从 Uncle Bob 书上弄来的。Rectangle 类做了两件事,
  1. 计算矩形的面积

  2. 在 UI 上画出矩形
    两个程序要用这个类,

  3. 一个几何计算的程序要用来计算面积

  4. 一个图形界面程序要用来在 UI 上画一个矩形
    这就违反了 SRP 原则。

  • Farhana: 怎么?

  • Shubho : 你看,一个矩形类包含了两个不同的动作,一个计算面积,一个画矩形,这导致了下面的问题:

  1. 在几何计算的程序中我们要包含 GUI,进而又需要包含 GUI 所用的图形库。

  2. 任何因为图形界面而在这个类上面所做的修改将导致几何计算程序重新编译测试,相反也是。

  • Farhana: 变得有趣了,所以我们应该根据其功能把这个类分开,对吧?

  • Shubho : 正是,那么该如何做?

  • Farhana: 我来试试,也许该这样,根据职责分为两个类,比如:

    Rectangle这个类定义方法 method()
    RectangleUI这个类从 Rectangle 继承并定义 Draw() 方法
  • Shubho : 非常好,现在两个程序分别使用两个不同的类,我们甚至可以将两个类放在不同的 Dll 文件里面,这样任何一个类的改动不会影响到另外一个程序。

  • Farhana: 谢谢,我想我理解了 SRP。一方面,SRP 是一种把东西分开到一些便于复用和集中管理的小模块中。那么,我们同样也能在成员函数这一级别来使用这个原则吧?我是说,如果我写了很多很多行代码在一个函数中完成几件不同的事,这也违反了 SRP 原则,对吧?

  • Shubho : 是的,你应该把这个函数分成几个小的分别做一份特定的事。这也让你只需要很小的代价来应付变化。

Open-closed Principle 开闭原则

  • Shubho : 这幅图是说开闭原则的。

  • Shubho : 先来解释一下:软件实体 (类、模块、函数等等) 应该对扩展开放,对修改封闭。最基本的层次,你应该能够在不修改一个类的基础上扩展它的行为。比如,我不需要在我的身体上做什么改变,就能穿上一件衣服,哈哈。

  • Farhana: 有趣,你能穿不同的衣服来改变的外貌,而不需要对你的身体做改变,所以你是对扩展开放的,对吧?

  • Shubho : 是的,在 OOD 里面,对扩展开放意味着我们能够扩张模块/类,对需求的变化添加一些新的东西。

  • Farhana: 而你的身体对修改是关闭的,我喜欢这个例子。那么核心的类和模块在扩展的时候是不能被修改的,你能具一些例子吗?

  • Shubho : 好,我们来看这副图,这是一个违反了开闭原则的例子。

  • Shubho : 你看,服务端和客户端是直接连接的,这样不管是因为什么原因,当服务端实现改变了的时候,客户端也需要改变。

  • Farhana: 恩,懂了点。如果一个浏览器只是针对于特定的服务器 (比如 IIS),如果因为什么原因我们需要换一个服务器 (比如 Apache),浏览器也需要改变,这真是恐怖。

  • Shubho : 对,下面这个设计应该要好。

  • 那个抽象的服务器类对修改是关闭的,而具体的子类实现对扩展是开放的。

  • Farhana: 恩,懂了。抽象是关键,对吧?

  • Shubho : 对,我们应该抽象系统中那些核心的概念,如果你抽象得好,当添加新功能的时候不需要修改。比如上面服务端是个抽象概念,如果 IISServer 是服务器的一种实现,现在需要扩展服务端这个概念,比如说一种新的 ApacheServer 实现,而这些扩展对客户端程序没有任何影响。

Liskov’s Substitution Principle 里氏可替换原则

  • Shubho : LSP 原则听起来很难理解,其实含义很简单,看下面这副图。这个原则意思就是:子类必须能够替换其继承的基类。或者换一种说法:基类能使用的方法,子类也能使用。

  • Farhana: 对不起,听起来很难懂。我认为这时 OOP 的基本规则,这时多态,对吗?

  • Shubho : 好问题,答案是:在基本 OOP 里面,”继承”被描述成一种”is-a”的关系,如果”开发者”是一个”软件职业者”,那么”开发者”类应该继承”软件职业者”,这种”is-a”的关系在类的设计中非常重要,但是这样非常容易导致一种错误的继承设计。LSP 原则是一种保证正确使用继承的方法。让我们看个例子。

KingFishera 是一种能飞的鸟,它继承 Bird 类没问题。但是如果下面这样:

鸵鸟是一种鸟,所以它基于鸟基类。现在能飞么?不行,所以,这个设计违反了 LSP。所以,即使在真实世界中看起来很自然。但在类的设计中,鸵鸟不应该继承鸟类。应该有一种不能飞的鸟类,然后鸵鸟从这个类中继承。

  • Farhana: 好,我懂了 LSP,让我来指出为什么 LSP 这么重要:
  1. 如果 LSP 不满足,类继承关系将会混乱,如果一个子类实例被当作参数传到一个函数,奇怪的事可能会发生。

  2. 如果 LSP 不满足,单元测试中基类通过而子类通不过。

  • Shubho : 很正确,你能吧 LSP 原则当作一种验证工具,来测试你的继承层次是否正确。

The Interface Segregation Principle 接口分离原则

  • Farhana: 这是什么意思?

  • Shubho : 意思如下:客户代码应该不依赖他们不使用的接口。

  • Farhana: 解释一下。

  • Shubho : 当然,其意思就是,假设你要买一台电视机,现在有两台可供选择,一台有很多转换器和按钮,大部分你都不明白是用来干什么的。另一个只有少数几个按钮和转换器,对你来说很熟悉。你选哪一个?

  • Farhana: 当然是第二个。

  • Shubho : 是的,但是为什么?

  • Farhana: 因为我不需要那么转换器和按钮,那些我不明白,而且对我也没什么用嗄。

  • Shubho : 对,类似的,假设你有一些类,你要暴露一些接口给外界,这样外面的代码才能利用这个类。如果一个类的接口太多,也暴露了很多接口,这对于外界来说是比较混乱的。而且,方法太多的接口也是不利于复用的,这种”大而全”的接口导致类之间的紧耦合。这也导致一个问题,任何使用这个接口的类都需要实现那些方法,而有些对于这个类是根本没用的。所以这么做也带来了不必要的复杂性,导致维护的困难和系统的健壮性问题。接口分离原则保证接口设计得合理,他们都有自己的职责,这样简明、方便理解、利于复用。

  • Farhana: 哦,我懂了。你的意识是指接口只含又那些必须的方法,而不包括冗余的?

  • Shubho : 是的,来看个例子。下面这个例子违反了 ISP 原则。

注意,IBird 接口包含很多鸟的行为,还有 Fly() 行为,现在一个 Bird 类 (鸵鸟) 实现这个接口,它必须实现 Fly() 行为,这对于鸵鸟来说是不行的。

正确的设计是这个。鸵鸟实现 IBird 接口,而可以飞的鸟实现 IFlyingBird 接口。

The Dependency Inversion Principle 依赖倒置原则

  • Shubho : 是说:高层模块不依赖底层模块,两者都依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。

  • 让我们来看一个现实的例子,你的车子包括很多组成部分,有引擎、轮子、空调、还有其他东西,对吧?

  • Farhana: 是的。

  • Shubho : 好,每一件东西都是严格地独立地造的,而且每一样都是可以”插拔”的,所以你的引擎或者轮子坏了,你可以修它,甚至可以换掉它,但是其他部分不需要动。你换的时候需要保证配件和车子的设计是符合的,比如这车子需要 1500Cc 的引擎和 18 英尺的轮子。同时,你的车也可以使用 2000CC 的引擎,任何厂家的都可以。现在,想象一下如果你的车子不设计成这种可”插拔”的,会出现什么问题?

  • Farhana: 那真是太糟糕了!如果车子引擎坏掉你需要修理整个车子,或者卖一辆新的。

  • Shubho : 是的,那么”可插拔”是如何做到的?

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

  • Shubho : 是的。在现实中,汽车是一种更高层次的实体,它依赖于一些第层次的实体,像引擎和轮子。而车子不依赖于具体引擎和轮子,依赖于这些概念。这样,任何符合这个概念的引擎或者轮子都能放进车子让车子跑动起来。看看下面这幅图,注意这里车子类中,有两个属性,都是接口类,而不是具体类。引擎是”可插拔”的是因为它接受任何满足这个抽象的具体实现,而不改变其他部分。

  • Farhana: 那么如果违反了 DIP 原则,将会有下面的风险。
  1. 破坏高层次的代码

  2. 当底层代码改动的时候,需要大量成本改变上层代码

  3. 代码复用不好

  • Shubho : 完全正确!

总结

  • Shubho : 除了 SOLID,还有其他很多原则。

“Composition over Inheritance”: This says about favoring composition over inheritance.

“Principle of least knowledge”: This says that “the less your class knows, the better”.

“The Common Closure principle” : This says that “related classes should be packaged together”.

“The Stable Abstractions principle”: This says that “the more stable a class is, the more it must consist of abstract classes.”

设计模式是 OOD 的特例,DP 就像是对于特定场景的特定框架,而 OOD 则是说明。

公号同步更新,欢迎关注👻