软件原理与模式再探





5.00/5 (17投票s)
软件设计和理解原理与模式的个体要素的基础方面
引言
本文档旨在为希望扩展其原理和模式知识的中级和资深程序员提供帮助。在阅读本文档之前,您应熟悉通用的面向对象编程 (OOP)、设计模式和架构。
在本文档中,我将探讨原理和模式背后的深层含义,以扩展对其的认识。除了示例之外,我不会试图涵盖任何具体的原理或模式。
大多数开发人员都熟悉围绕面向对象编程 (OOP) 的许多流行术语,即使只是为了将它们填满简历和预期的面试问题;然而,除了找工作之外,对这些术语和概念的熟悉程度实际上可能弊大于利。是的,弊大于利!因为大多数时候,当我们学习这些概念时,我们是在职业生涯的早期学习它们,而那时我们实际上还无法深入理解它们。
我最近采访了一位“20年经验的架构师”,让他解释“控制反转 (IoC)”与其他创建型模式的区别。他无法做到——至少无法以连贯的方式解释。暂时抛开枯燥的面试,问问自己,能够理解和剖析这些差异是否有价值?
我给您一个关于这个问题的答案线索……它以“是”开头,以“是”结尾。诚然,大多数时候剖析这些技术差异并没有巨大的价值,但开发是一个永无止境的问题解决过程,其中差异最终将变得至关重要。如果您想将自己定位为“专家”或“架构师”,您就需要了解这些差异。
这里发生的是一个经典的“我学过了,就永远知道”的案例。问题在于,我们通常无法在没有循环学习过程的情况下完全理解新的、复杂的信息。例如,需求是新的、复杂的数据片段,随着理解的加深而演变。这是《敏捷宣言》背后的主要驱动力,也是推动开发更多地转向敏捷而非瀑布模型的部分根本原因。
那么,我们该如何扩展我们对面向对象编程 (OOP) 的知识呢?我们可以像处理需求一样,将循环、演进的学习过程应用于原理和模式(以及生活中的一切)。我们必须接受,我们从一开始并非无所不知。这个循环始于我们理解:学得越多,知道得越少,这就像深入研究需求一样……
软件设计目标
在深入任何领域之前,我们需要确保理解,面向对象编程 (OOP) 和模式并非源于某种自命不凡的创造单一编码方式的愿望。最终目标是实现更高的软件开发生产力。因此,无论您选择做什么,无论一个解决方案在技术上可能有多好,您都必须问,该选择是否比其他选择更能促进生产力。
让我们仔细分析一下,以防您立即想到“并非如此!它是为了让软件更清晰、更易于维护!”您认为人们为什么想要清晰、易于维护的代码?这是为了让开发人员通过更少的时间修复错误、更容易扩展功能、在进行更改时更灵活等等来提高生产力。
回到敏捷与瀑布模型,两者的目标都是更好的需求。我知道很多开发人员批评瀑布模型,但瀑布模型也有其目的。有些系统的故障成本很高(生命损失、数百万美元损失等),对于这些系统,瀑布模型的目标是前期严格的流程来减轻故障的发生。普通的网站不会有同样的风险,因此敏捷和滚动式前进的方法不仅是可以接受的,而且它们通常能产生最佳的生产力。
重点是根据目标和生产力做出选择。我们可能会陷入为原则(面向对象编程 (OOP) 及其他)而牺牲自身生产力的陷阱。如果瀑布模型适合您的项目,最好选择该方法。如果修改您的敏捷流程以包含更多控制效果最好,那就这样做。
“您不会需要它” (YAGNI) 是另一个很好的例子,说明了原则如何扼杀生产力。花费开发时间来创建用于可扩展性的钩子和功能,而从未使用这些钩子和功能,无论它多么优雅,都是浪费时间。不仅如此,通常来说,代码越多意味着复杂性越高,可理解性越低。
那么,什么才算生产力高的代码呢?这取决于……绝大多数软件都是为解决业务需求而编写的,因此在大多数开发团队中,满足业务需求才定义了生产力。因此,生产力是按公司、按项目定义的。
假设编写草率的代码以提前发布更改会产生技术债务,但无法提前发布会导致项目被取消。哪种产生的价值更大?干净的代码?还是保持软件项目存活?显然,大多数日常的软件决策并非如此极端,但重点是,干净的代码并非总是最具生产力的选择。
干净的代码绝对应该是默认的首选方式,但为了保持业务价值,您需要有多干净?这引出了开发人员面临的常见难题——确定需要保持的干净代码级别。
让我们以命名约定为例,来说明“干净代码级别”的意思……最好使用该代码所在开发环境的行业标准命名约定;然而,即使偏离标准,通常也能接受使用一致的命名约定。如果名称完全没有意义(例如 `x`、`y`、`MySuperDuperClass`)或者约定不一致,那么就存在一个更大的问题。
这些级别都很明确。前两个是可接受的,最后一个则不是。那么灰色地带呢?糟糕的命名选择呢?
假设场景……乔刚刚提交了一个拉取请求,他的命名选择很糟糕。乔对批评相当敏感,并且本周有一个关键项目即将到期。您是审阅者。您是否要详细分析所有糟糕的命名选择?还是您会放过它,因为您不想冒着破坏他在关键项目上的工作/心态的风险?
如果乔错过关键项目的截止日期,可能会造成严重的后果。如果乔感到过度批评,这对职业关系会产生什么影响?糟糕命名选择的代价是多少?有时提高生产力意味着选择战斗,这是任何开发人员职业生涯的关键部分。事实是,目标比技术细节更重要。
请不要误会我的意思。 在绝大多数情况下,是干净的代码创造了最大的价值。但我确实想强调,应该由选择所产生的价值来决定何时做出该选择。
这个决策过程中的一个陷阱是最终陷入一个地方,那里充斥着草率的代码,而技术债务从未得到解决。随着技术债务的累积,必须定期采取措施来解决它,否则生产力将不可避免地下降到难以解决的地步。当达到那个点时,该软件基本上就成了一个“每日 WTF”故事……
软件的目标确立了任何交付软件的价值,因此目标应该驱动软件决策过程。记住要选择战斗,并保持开发务实。
如何避免“绿色保龄球大屠杀”
在我看来,软件设计讨论中最令人讨厌的论点之一是“有数千种解决方案”的论点。也就是说,没有最好的方法,因为有数千种解决方案可以解决问题。那是胡说八道。有最好的方法,也绝对有比其他方法更好的方法。在设计时可能不清楚甚至不可能确定哪种方法最终是最好的,但您绝对可以做出明智的决策并为设计选择提供一定程度的量化。
大多数初始设计都有某种缺陷。这并非不一定是因为缺乏努力,而仅仅是因为在当今敏捷环境的项目开始时,几乎不可能看到一切。如果您从未查阅过“pitfall”(陷阱)的实际定义,那么它在这里
引用陷阱
隐藏或出乎意料的危险或困难。
这里的关键点是“隐藏”。这意味着无论任何人多么厉害,他们可能都看不到前方的问题。请记住,在这些情况下
- 你认为你可以凭感觉编码过关。
- 别人对你完美的设计有意见。
- 意外的困难变得像《星球大战》的引用一样常见。
成为陷阱的受害者并非世界末日,并且尽管无法完全消除它们,但有一些编码措施可以帮助减轻它们的损害。减轻损害的一个典型方法是计划陷阱的发生,即使您不知道它们会出现在哪里。
更大的失误是反模式
引用反模式
- 对反复出现的问题的一种常见应对方式,通常无效且可能适得其反。
这里有一个我经常看到的“反模式”……开发人员使用“IoC”(控制反转),却在接口实现所在的同一个库中声明接口。IoC 的一个巨大好处是它支持依赖注入,而这种反模式会破坏这一点。在这种情况下,任何解析接口的人都会自动对实现代码产生依赖。与简单地使用实现类相比,几乎没有好处。最佳方法是有一个包含接口声明的高层库作为依赖,然后将实现放在另一个解耦的库中。
反模式比陷阱更糟糕,因为它经常会引发危险的反应,例如
- 我知道我的代码有效。
- 我不应该这么做。
- 但大家都是这么做的……
这些回应常常形成“盲区”,导致开发人员开始“修复”可以正常工作的代码。与此同时,会产生大量意外的副作用,并且在实际问题解决时,代码需要进行全面回归测试。换句话说,反模式是生产力的巨大杀手。
那么,糟糕的决策与原理和模式有什么关系?如果您不深入探究原理或模式的目的,您很可能会遇到陷阱并屈服于反模式。接下来,您就会发现自己为了挽回面子而编造关于“绿色保龄球大屠杀”的故事,或者任何其他成为代码问题“真正原因”的事情……
原理、模式和谈判
在《加勒比海盗:黑珍珠号的诅咒》中,伊丽莎白·斯旺根据海盗的规矩要求谈判。在与海盗船长巴博萨会面后,他以这个结束了谈判
引用首先,您返回岸上并非我们谈判或协议的一部分,所以我无能为力。其次,您必须是海盗才能适用海盗法典,而您不是。第三,法典更像是“指导方针”,而非实际规则。
伊丽莎白·斯旺因为试图使用她不完全理解的东西而措手不及。这种误解的一个重要部分是谈判的原理与指导方针之间的区别。开发人员经常遇到这种情况……
因此,让我们尝试通过首先确定关键术语来避免这种情况
- 原则 - 某事物的基本来源或基础
- 模式 - 重复的设计
- 最佳实践 - 被接受或规定为正确或最有效的商业或专业程序
- 特殊人才 - 认为自己超出其所在行业的普通水平的个人或公司
原则可能是一个棘手的问题,因为许多建议和指导方针仅仅是建议和指导方针。棘手的部分是有些是必须适用的刚性规则,否则您可以将其抛诸脑后。
例如,N 层架构的一个原则是物理上强制执行层交互,以实现分层责任隔离的目标。我不知道有多少次我看到开发人员称分层架构为 N 层……
如果只有一层,或者所有层都位于同一物理位置,那么它就不是 N 层。有多少层或每层有多少层由开发人员自行决定。它们有不同的名称是有原因的!
那么,为什么这很重要?在分层开发中,没有什么可以阻止开发人员违反应用程序层交互的标准。这也意味着代码可能更难分发到未来的应用程序或服务中。如果您不理解这些差异,那么您可能没有为项目接下来的事情做好准备……
在此基础上,让我们谈谈模式……需要理解的第一件事是,模式主要有两种类型
- 设计模式
- 架构模式
两者之间存在巨大的差异,并且非常重要。让我们假设在本文档中,我只讨论我认为重要的主题,这样我就不必一直提及它……
设计模式有三个分类,分别是
- 创建型
- 行为型
- 结构型
设计模式是实现常见问题解决方案的特定方式。因此,创建型模式是如何创建对象的特定实现。
最常见的创建型设计模式之一是单例。单例的原则细节是
- 私有构造函数,以便类控制任何初始化
- 在任何给定时间,该类只有一个实例已初始化
请注意,这些原则是如何基于实现的?私有构造函数……这告诉我必须如何编写特定的代码行……类必须执行 x、y、z……这意味着我必须编写特定的功能……
如果您不实现私有构造函数,则根本不实现单例。在任何给定时间,其他人都可以创建该类的另一个实例,而设计模式的整个目标就是防止这种情况发生!
对象的静态实例不是单例。容器中的共享实例不是单例。这两者都属于您所说的生命周期对象,但再次强调,这有所不同。
架构模式是更高级别的模式,通常定义代码的物理结构以及层之间的交互方式。以下是一些常见的架构模式
- N-Tier
- 面向服务架构 (SOA)
- 模型-视图-* (MV*)
- 领域驱动设计 (DDD)
架构模式的范围大致相同(整个项目),但经常混合使用。事实上,提到的四种架构模式在现代业务应用程序中经常混合使用。例如,您可以通过物理分离每个层来实现 MVVM 和 N 层。模型层可以基于 DDD,并使用 SOA 与业务服务进行通信。
也可以混合设计模式,但由于设计模式是针对实现特定的,因此有些是相互排斥的。例如,经常看到惰性初始化的单例;然而,您不能有一个创建单例的工厂。您可以有一个返回单例的方法,但那不是工厂……
关于惰性初始化的单例,我们现在进入下一个主题——最佳实践。惰性初始化生命周期对象或任何资源密集型对象是一种最佳实践。只创建您需要的,对吧?
因此,以下是一种惰性初始化线程安全单例的最佳实践方法
public sealed class MySingleton
{
private static object initializationSyncRoot = new object();
private static MySingleton current;
public static MySingleton Current
{
get
{
if (current == null)
{
lock (initializationSyncRoot)
{
if (current == null)
{
current = new MySingleton();
}
}
}
return current;
}
}
private MySingleton()
{
}
}
我曾多次见过(也写过)没有双重 `null` 检查的同步根,但这是处理并发值检查的优化方式。绝大多数情况下,访问时 `current` 值不会是 `null`,因此无需同步。在罕见的启动时竞争关键区域的情况下,您会获取锁,然后确保没有人先设置 `current`,最后设置字段。
除了正确实现单例模式并进行线程安全的惰性初始化外,我们还有正确的命名约定和相对标准的类组织(字段、属性、构造函数、方法)。总而言之,这是一个很好的例子。
当然,您不必使单例线程安全,但当它很容易做到时,通常是最好的选择。最佳实践之所以被称为最佳实践,是因为整个行业都遇到了许多因此而未采取该做法而导致的问题。
所以,如果存在所有这些最佳的编程方式,那应该很容易,对吧?问题在于,我曾待过的几乎所有公司都说过
- 我们的业务需求比大多数公司更严苛。
- 我们的数据比大多数公司更复杂。
我从未从我待过的任何公司听到过
- 我们的软件需求很普通。
- 我们无法证明我们拥有的技术债务的合理性。
这些情感背后的态度通常会导致回避最佳实践。当然,有时确实有合理的理由偏离最佳实践和其他行业标准实践,但很可能很少这样做,而且不是因为您的需求与其他地方如此不同。
这些说法也倾向于培养“特殊人才”,因为您毕竟如此独特,对吧?“特殊人才”会查看现有的最佳实践并说诸如
- 那不适用于我。
- 我可以做得更好。
它适用!最低公分母(行业)的特点是它包含了所有人。我不是那种诉诸权威论证的人,但大多数时候,专家之所以是专家是有原因的。如果您认为自己可以做得更好,您也许可以,但您绝对应该能够说明原因和方式。
当然,“特殊人才”的真正问题不在于存在自大妄想,而在于它无处不在。我曾在一个团队工作,他们一直吹嘘“世界一流”的数据。然而,这些数据客观上是我在生产系统中见过的最糟糕的数据——导致了大量的麻烦。没有标准的命名约定、缺乏引用完整性、无意的 1NF/2NF 表等等。最糟糕的是,按名称存储计算得出的季度数据,例如 `JFM_exports`,其单位有时不一致且无法识别……
糟糕的性能占主导地位,而“独特性”则成为替罪羊。项目之所以没有错过截止日期,不是因为业务需求比其他任何地方都更紧张。这是因为自称“数据库之神”的 Joe Schmoe 不知道如何设计数据库。
如果我们能够认识到代码和开发环境的这些各个方面,也许就能使事情更容易处理。如果某件事看起来比应有的要困难,那么很有可能就是如此。不幸的是,对于遗留开发,有时只能束手无策。至少如果能被识别出来,就有准备的方法。
试金石
判断一个项目随着不断发展而顺利进行的方式是,事情会变得越来越容易。等等……项目变得更大,功能更多,它应该更容易进行更改吗?这是怎么回事?
把它想象成这样……如果您在做一顿饭,有人已经预热了烤箱,烧开了水,拿出了香料,切好了食材等等,做饭不是更容易了吗?当然,也有其他结果……烤箱满了,所有锅都脏了,而且没有更多的蒜了……在这种情况下,做任何事情都非常困难,您只能做意大利面,但这就是重点……您需要与现有事物抗争的程度通常是衡量它有多干净的良好指标。
开发中的大部分痛苦是由以下两件事之一引起的
- 程序的根本架构
- 技术债务
如果您不熟悉“技术债务”这个词,它是为了某些其他优先级而做出的次优编码选择。这里“债务”是一个关键的词,因为您期望偿还它;然而,绝大多数时候它只会累积,直到项目应该宣布破产。
可以根据项目中的技术债务量来观察不同的交付模式。一个应用程序表现出的模式通常是它设计良好程度的良好指标。因此,第一个试金石是交付时间。
注意:请原谅我出色的画图软件艺术。
如果您绝对完美地掌握了架构和设计,那么交付时间应该看起来是这样的
基本上,这意味着您在架构和设计上进行了大量的初始投资。在框架和基本应用程序逻辑建立之后,一切都变得非常顺畅。无需进行大规模重构,即使技术债务不断增长,关注点分离也能限制暴露,因此不会显著影响交付。
这是光谱的另一端
这意味着初始交付非常容易,因为您随意地进行编码并推送更改。一切都很顺利,直到达到某个复杂性的临界点,即使是小更改的开发成本也会飙升,因为没有逻辑分离。修复一个问题往往会产生更多问题。由于事情只能变得如此糟糕,因此会很快趋于平缓;然而,代码此时已经严重损坏,任何大规模的重构实际上都是一次重写。
这是一个更务实的场景,您希望看到
您正在尽力而为,考虑到所有事情,包括您的 TPS 报告和老板在您身后施压。当事情开始变得糟糕时,您会咬紧牙关进行一些重构。您只提取了您需要的东西,但当情况再次变得糟糕时,您将不得不再次忍受。
我在此简略提及的关键概念是分离。任何良好设计的标志,无论选择何种架构,都是关注点分离。它非常重要,不仅 OOP 直接解决了它,而且另外两个基本概念也解决了它——SOLID 和 GRASP。
花点时间想想……如果一个代码段被正确地隔离了,这意味着内部类代码的更改不会破坏任何外部代码。如果一个代码段被正确地隔离了,这也意味着行为的更改只需要进行一次。如果这两者都成立,那么通过重构处理问题有多容易?
重构是一个很好的试金石。您需要更改多少个类来修复一个错误?添加一项增强功能有多容易?重构对于维护、可扩展性和灵活性也至关重要,这使其成为最重要的编码类型。
编码类型?是的,有三种类型的编码更改
- 扩展
- 修改
- 重构
希望这三个术语您都熟悉,因为每个人都应该知道重构,而前两个被开放/封闭原则涵盖。开放/封闭原则是 SOLID 中的“O”,它规定代码应该是开放扩展的,但封闭修改的。
非常重要的是要注意,修改不等于更改代码。重构和修复错误显然会更改代码,但这些不一定是修改。修改代码是修改代码的预期行为。例如,修改将是采用一个名为 `GetList` 的方法并更改返回类型、添加参数或使其在内部执行完全不同的功能。如果您只需要重构内部方法代码以执行相同的功能,那只是重构。
对于已发布的软件,应避免修改;然而,如果是在开发早期,或者应用程序是隔离的,那么是可以容忍的。话虽如此,应用程序生命周期中需要多少修改是另一个试金石。如果您发现自己经常需要或想要修改代码功能,那可能是一些糟糕的设计。
通常,如果需要添加参数,这可以通过扩展(例如通过重载)来处理。这样,任何现有代码将继续按预期运行,并且只是增加了功能——因此是扩展。扩展也可以包括重构。例如,在参数更改的情况下,您可能会将代码移到新方法中,并从旧方法中传递一个 `null` 参数。
因此,总而言之,我们的三个试金石是
- 交付时间
- 重构所需的精力
- 修改代码行为的需求或愿望
如果您在这些三个领域中的任何一个发现问题,不要找借口,而是寻找答案。
是的,是的,是的,是的
好吧,我说的够多了,但说真的,每个人都需要不断讨论这些话题。除非您解决自己的想法并让您的想法受到他人的挑战,否则您的知识将不会扩展。
不要把任何事情个人化。即使有人称您的想法愚蠢,也要挺住并询问原因——他们可能是对的。您真诚地更想要什么?能够扩展您职业生涯的知识?还是自尊?
永远努力不要成为房间里最聪明的人。您可以从解决自己的想法中学到很多东西,但远不及有更聪明的人帮助您。
不要试图死记硬背定义。尝试学习概念和应用。我对此犹豫了很长时间,直到我看到 PowerShell 的创建者、著名的 Windows 工程师 Jeffrey Snover 在 PowerShell 中依赖帮助。
希望您喜欢我今天的长篇大论!
想法?评论?侮辱?
历史
- 2017年3月1日:初始版本