缺失的课程:秘密
缺失的课程:软件工程的秘密
你需要知道的主要事情
image by auhor
我女儿的软件工程教授(以下简称 Phud)分配了一本 SE 书( 抬头设计模式 ) 上课,但根据她的说法,从来没有听过课,实际上也没有再提到过。
Phud 引入的唯一模式是模型-视图-控制器 (MVC) 架构。
MVC,被广泛使用。好的。很好的做法,同意。
但是 Phud 没有涵盖主要的设计模式——那些被编入标准工作的模式 设计模式 作者:埃里克·伽玛、理查德·赫尔姆、拉尔夫·约翰逊和约翰·弗利赛德斯。 (这本书有时被称为 四人帮 或者通常只是 GoF——尽管我的写作风格令人讨厌,但 GoF 并不是什么双关语)。
而且他没有解释模式的起源(克里斯托弗·亚历山大,我的英雄),为什么有必要知道和使用它们,或者, 最重要的是 ,他们藏在心里的秘密, 良好设计的基本原则。
想象一下一个软件工程课程,它没有介绍 SE 中最重要的一个概念……正如播音员在兴登堡号从天空中倒塌燃烧时哭喊的那样: 哦,人性。
这是什么秘密?
嘘。在这里,芽:不要问,不要说
而已。
我们作为软件工程师所做的几乎所有事情的隐藏基础。它是我们对 1 和 0 的逻辑排列中最基本的概念。
秘密——不问,不说。
好的, 完毕 .
SE 的核心秘密揭示了: 秘密。 你好美女!
(麦克下降)
…
只是在开玩笑。
我还没说完,因为你不相信我,是吗?完全没有。
你在想“秘密。这是一些模糊的废话* t。另一篇从不解释任何内容的点击诱饵文章。”
相反,亲爱的。
小号 排泄 s真的是秘密。
不问不说 .
这是一个非常简单的原则,但需要比你想象的更多的纪律,我希望你给我一四分钟时间来解释。
从著名的 MVC 开始:
模型——视图——控制器 是主要的架构模式之一,它是 全部 关于保守秘密。 View 对 Model 的了解越少越好,因为,你知道 Model 是一个模型并且坚持自己。她不喜欢分享,也不想知道关于 View 的任何事情,因为她根本不在乎。控制器是一个控制狂,它的存在是为了让模型和视图彼此分开。除非 Model 和 View 根本不说话,否则他会不高兴。嫉妒的b * st * rd。
但是之后 分层 ,另一种占主导地位的架构模式,也在保守秘密。它说上层可以与下层通信,但不能与其他层通信。每一层都隐藏着它们的内部结构,它们是如何在内部真正工作的。鬼鬼祟祟的小家伙,正在分层。也许,如果设计师很活泼,他们会让 Lower 和 Upper 顶嘴。但是,无论是 Lower 还是 Upper,都永远不可能在一百万年内越过我的心,希望去参加 Phish 音乐会,与两步外的 Layer 交谈。因为,如果他们这样做了,秘密就丢失了,它不再是分层模式,而是另一种模式,我们称之为 高度耦合混乱 .
高度 耦合 混乱。
耦合。 这是另一个词,我女儿的 Phud 显然发音有问题,因为他从未提到过。
耦合 , 或者 耦合 , 是软件工程中的术语,用于描述两个软件实体的相互关联程度(意味着任何两段代码,而不仅仅是 C++/Java/etc 对象)。耦合,就像比萨饼和凤尾鱼一样,是一件坏事。甚至还有一个特定的公式来衡量软件耦合的集群效应(我不会解决它,因为你不太可能让它使用它 IRL。你未来的经理更不可能听说过它。 )
耦合 意味着一个软件知道(使用)接口或更糟的是,另一个软件的内部。例如,如果你的 HTML 下拉列表选择事件触发了一个查询 MySql 表并使用结果修改下拉列表内容的函数,则 UI 与数据库高度耦合。坏的, 坏的 狗。这完全违反了模型-视图-控制器模式,因为您已经创建了从视图(UI)到模型内部(数据库名称、属性名称和数据类型、登录/数据库连接信息等)的直接依赖关系,并且完全绕过了控制器的角色。
因此,您不再坚持 MVC 模式,而是采用了历史悠久的 1970 年代模式,即意大利面条代码。
Spaghetti Aglio e Olio, by author
我们今天经常提到的 Javascript。
JS Spaghetti screenshot, sadly by author
真实的故事:
我最早工作的地方之一是雇佣了两个程序员,他们在同一台计算机上一起工作——这是我在现实生活中见过的唯一的函数式结对编程,这是在 1984 年——他们的代码是一个坚实的文本块。 BASIC 的一页又一页,没有一个无关的空白,一行中有多个语句。从字面上看,除了他们之外没有人能理解它是如何工作的,这显然是他们的目标——工作保障。他们把“保密”带到了荒谬的程度。我离开六个月后,公司破产了;不是因为我的重要性(我根本不是),也不是因为这两个开发人员直接,而是因为经理们惊人的无能,他们的许多有问题的决定中只有一个是让公司的核心知识产权被扣为人质两名编码员(还有持签证的中国公民!)。这两位程序员现在可能是明星敏捷开发人员:结对编程、夜间构建、代码审查(经理——“嗯……看起来很棒”),而且他们没有使用任何评论。太棒了!
如果您忽略基本的数据结构模式,例如 向量 , 堆 , 地图 等等。我在创建子系统时通常使用的第一个设计模式是 辛格尔顿 , 工厂 , 建造者 , 和 正面 .我不会在这里详细解释这些模式,但很快:
辛格尔顿 是单个的、通常是全局的、变量或(通常)和对象的管理器。其目的是将用户与如何以及何时创建资源的知识隔离开来,并提供一种统一的方式来访问它。
Photo of 1921 Saargebiet, Ruhr, Germany stamp, by author
工厂 和 建造者 模式是一个类或一组具有相同作用的类:将对象的实例化和经常删除从一个内聚的类家族中分离出来。例如,在简单图形系统的常用教科书示例中,您可能有一个生产所有 Shape 对象的工厂。工厂通常生产特定或 具体的 类(在本例中为 Circle、Square 等),但使用并返回它们(通常) 抽象的 基类(Shape)——同样,尽可能将用户与管理特定形状隔离开来。
正面 通常是一个单一的类,或一小组有凝聚力的类,用作另一个通常更复杂的子系统/对象/硬件接口的接口。例如,在传统的对象-RDBMS 映射中,通常有一个外观将用户与 DB 内部隔离。
我经常使用的复合模式是让 Singleton 创建一个 Facade,它管理我正在隔离的给定子系统的 Builder(s) 和/或 Factory(s)。
你有没有注意到我每次都用这个词 隔离 以上?您可以将隔离替换为 ' 避免知道' 或者,按照本文的主题, '保守秘密' .
让我们再击败这个死去的汤汤,然后用一些具体的实现细节来消化它:
这 代理人 模式的存在是为了将用户与另一个系统隔离开来。
适配器 — 使系统 A 与系统 B 一起工作,将 A 的用户与系统 B 隔离开来。
Photo of 1921 Saargebiet, Ruhr, Germany stamp, by author
桥…原型…建造者 ……等等,所有常见的 GoF 模式都存在以隐藏东西(除其他外)。
普遍的想法是,这些通用模式为问题提供了预先构建的通用解决方案,从而减少了开发人员的认知负担(我们必须同时记住多少)。
当然。
但是这些模式的主要、潜在的目的是通过隐藏接口和实现细节来防止耦合,通常使类消费者(即你)的生活更简单。这就是它们被普遍使用的方式——它们是很好的设计。
作为软件工程师,我们的目标是遵循这些良好设计的相同原则:减少函数之间、对象之间、类族之间(甚至在可行的情况下在类族成员内)、子系统之间以及离散系统之间的耦合。
保守秘密——不问,不说。
这一原则贯穿于我们所做的一切,甚至在传统设计模式之外,从高级到低级:
Kubernetes 和 Docker—— 它们在系统级别的主要目的是向用户隐藏内容。您无需了解每个盒子内转动的所有齿轮即可使用它。 “云”本身旨在将您与部署、硬件、负载平衡等知识隔离开来。
卑微的支架 — {},在那些支持它的语言中,作用域变量——即在大括号外的代码中隐藏它们。
在设计软件时,这个原则永远不会远离我们的脑海。
如果有多种方法可以做某事,我会问自己两件事:第一,隐藏更多,第二,更简单。
通常这两个问题的答案是相同的,因此选择本身。有时它们会发生冲突,这就是做出艰难决定的地方。
那么,一些实际的例子呢?
很高兴你问。谢谢。
类级别示例:访问说明符
创建类时,任何成员的默认隐藏级别都是“私有”,除非您引入“受保护”或“公共”关键字。为什么是这样?
因为隐藏信息是良好设计的基本原则。
所以,我们应该跟风。将属性和函数设为私有,除非继承的类明显需要使用它们,然后才将它们提升为受保护的。如果另一个不在层次结构中的类需要使用一个成员,则将该类的函数设为友元,如果绝对必要则将整个类设为友元。如果成员属性需要公开,请三思。访问器功能还不够吗? 真的吗?
如果成员函数需要公开,则在需要时这样做,但不是在此之前,“以防万一”。
这是“ 不要告诉 ” 我们首要指令的一部分——不要告诉(暴露)类中不需要暴露的任何部分。并检查定义 需要 当它出现时。
那么构造函数呢?构造函数应该总是公开的吗?
不。
需要向其他系统公开但不由它们管理的对象“A”的一种方便技术是使构造函数私有/受保护,并仅允许朋友访问负责的 Builder/Factory。这意味着“A”类的消费者不能直接创建“A”的实例,即使他们愿意。
设计级别:多态性,或过于多情的对象
多态性,或子类型,在这里是为了我们的目的,继承。在前面几段的示例中,我们将 Shape 作为几个特定子形状的超类。在实践中,我们的代码应该尽可能引用 Shape 而不是子类型(圆形、三角形等)。这就是多态性——字面意思是具有多种形状。
为什么我们使用超类型(基类)而不是子类?
因为使用 Shape 的代码只耦合到 Shape,而不是子类型。这是“不要问,不要说”口头禅中“不要问”部分的一个例子——不要问/使用不必要的细节。如果您不需要知道正在操纵哪个子类,那就不要。
但是等等,这还不是全部,伙计们!如果您现在采取行动,您可以以 9.99 美元的价格获得易碎类层次结构和联轴器。而且,作为奖励,我们将免费包含机会主义继承!
假设(在一个非常人为的示例中)您正在创建一个名为 Tribble 的新类,并且您想在另一个类 DeathStar 中使用成员函数或一系列函数,例如 move()、explode() 或 something() .但 DeathStar 已经继承自 Spacecraft,而 Tribble 已经继承自 Alien。由于许多原因,这些继承链接无法更改 - 因此,您创建一个名为 TvSciFiObject 的通用根类,并让 Spacecraft 和 Alien 都从它派生(因此允许 DeathStar 和 Tribble 访问),然后将所需的方法移动到基础,TvSciFiObject。
初学者一直在做这种废话。我确实做到了。
但是你看到了问题,对吧? Tribble 和 DeathStar(加上它们的父母)现在在基本层面上是耦合的。是的,我们利用了 move() 逻辑,但代价是大大拓宽了类层次结构。由于所有外星人和所有航天器都来自一个共同的基地,Spock 和 Vader 现在将是相关的。
哎呀。
如果我们改变 move() 的工作方式,充其量只能从头开始重新测试整个系统。最糟糕的是,我们刚刚杀死了Spock。再次。
这是一个例子 脆弱的类层次结构 问题:层次结构太宽泛——太耦合——导致意想不到的后果。安全更换变得困难;它成为了 脆 .这很糟糕——唐纳德特朗普穿着粉红色的胸罩和内裤很糟糕。
密切相关的是 机会遗传 问题——假设我们只是让 DeathStar 直接从 Tribble 继承,即使它们是完全独立的特许经营权。这是个 投机取巧 通过继承使用 move() 等方法。我们拯救了 Spock,但我们仍然将 Tribble 和 DeathStar 结合在一起。而且由于家长班永远不应该了解他们的孩子(就像在晚年一样,照顾父母是孩子们的责任),DeathStar 甚至不会知道它的变化正在伤害无辜的小 Tribble。
再说一次,不好。
顺便说一句——这是隐藏东西的另一个基本原则——父类不应该知道,即向下耦合,到他们的孩子。
虽然这超出了本文的范围,但当您想在另一个类中使用一个类的功能时,您可以使用四人组中的装饰器模式将常用功能移到一个单独的类中。或者功能模板也可以为此工作。
系统级别:RESTing on my Facade
我目前正在对以非 SQL 格式收集的各种大型到超大型政府数据库进行存储、搜索和提供情报。相同的软件对从 Web 抓取和/或商业网站的 REST API 收集的数据进行操作。记录数以亿计。我的服务器软件都是 C++,但消费者可能是其他 C++ 进程、PHP 或 Javascript(也许是 Node)。
我不希望服务器知道它的消费者,我想尽可能地将消费者与服务器隔离。所以,就像现在很常见的那样,我提供了一个从我的服务器到我的基于 Web 的 UI 的 REST 接口。
对于那些不熟悉的人来说,rest 接口是这样工作的:一个程序调用一个 URL,例如:
www.urlgoeshere.com/queryname?parameterdata=有时
并且通常会取回 JSON 或 XML 数据。 (我仍然更喜欢 XML,因为它的表达更丰富,尽管解析起来要困难得多)。
看到图案了吗? REST API 是一个 正面 — 更复杂系统的更简单接口,在这种情况下跨越语言、处理器和地理边界。
但在我通过 REST 发布的 Facade 背后,是我的底层系统的另一个 C++ Facade,它本身由多个系统组成,其中一些系统本身可以被其他 Facades 访问,因为它们是多个项目使用的公共服务器/库......立面在其他层次上分层,每个层次都有自己的建造者和工厂,就像洋葱一样。
为什么?
这是否更简单是有争议的——我认为这是整体的,尽管添加了连接代码。 (简单与隐藏辩论的一部分)。但根本原因是我希望内部流程以及我公开的 REST API 使用相同的 Facade,以尽可能地将每个子系统与其他子系统隔离。系统的核心是(我相信是)一个复杂的元模型,它使所有的查询变得神奇。自然语言处理 (NLP) 也将使用相同的系统,该系统将具有完全不同的 UI 和顶部用例。
所以,我不希望每个消费者都知道元模型。它会暴露太多,并为底层表示引入太多耦合。此外,尽管版本已经开发和部署了 40 多年(系统的萌芽是我在高中时的第一个想法之一,当时我什至还不知道 TRS-80 磁带驱动器上的 RAM 和数据存储之间的区别)当前的迭代是全新的代码,是一个很大的飞跃(我希望如此),并且非常处于实验阶段。它会不断发展,我想将不同的消费系统从中分离出来,因为虽然每个新用途都越来越复杂,但不太复杂的用例的接口不应该改变。
最后,我不想公开元模型,因为它是高度专有的。因此 JSON 和 XML 在 Facade 后面呈现。
我想对世界其他地方以及我自己保守秘密。不要让左手知道右手在做什么,等等。
这是一个很好的、通用的架构模式——一个 REST(或 SOAP,或其他)发布的接口,位于为特定 API 提供服务的 Facade 之上。然后,在该 Facade 之下,是您系统的 Facade,如果足够复杂并且该系统还必须支持来自 REST API 以外的消费者的编程访问,则它本身由 Facades 组成。
我如何证明这是一个“好模式”?注意,我没说 最好的 , 或者 只要 .我刚才说 好的 ,并且我可以说没有吹嘘或资格,因为多年来我已经多次使用这种模式来根据我在a中概述的标准交付软件 上一篇文章——我能够完成(做)、执行任务(做)、花费必要金额(或更少)、按时交付(曾经)并且仍然可以支持的软件。
这使得它客观 好的 在我的书中。
这使它比大多数编写的软件都要好。
旁注:如果您交付的软件项目按照它所说的那样运行,并且您可以让它运行很长一段时间?
不管它的内部是否像梅毒、独眼黑猩猩一样丑陋并且有轻微的内存泄漏—— 你,我的朋友,是精英俱乐部的一员。 恭喜 ,正如他们在德国所说的那样。
有趣的事实
谷歌“有多少软件项目失败了”,你会看到类似下面的内容:66% 的软件项目失败,大约 90% 的大型项目失败。
另一个令人难以置信但绝对真实的故事:
我咨询了世界上最大的公司之一。我敢肯定,你们中的大多数人已经不止一次将自己的生命和身体健康托付给了这个特殊的企业。他们的一个大型(5 亿美元)项目失败了,而我是一个团队中的一员,负责挽救它(剧透:不可能)。我让高级成员,大多数团队负责人,许多人在公司工作了 10 到 20 年,有些人接近退休,在一个房间里被烤。在收到几个令人沮丧的问题后,我终于问了这个问题:
“你们中有多少人在这家公司从事过一个实际交付的项目?请举手。”
我不拉屎你,没有手。
不是一个。
PS。不要问公司的名字。我不想冒着因为从这里到奇点的违约而被起诉的风险。
…
就是这样了。
我希望我已经说服了你 不问不说 是软件工程的核心,从大括号“{”到云,以及介于两者之间的一半设计模式。
忽略这一基本原则,后果自负。
秘密 隐藏在众目睽睽之下。
但你没有从我这里听到。
This astonishing artwork is by the author. Inquire about publishing rights…
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明