flyback

10个现代软件开发过度设计上的错误

flyback · 2017-01-07翻译 · 472阅读 原文链接

有些事情总是会增加的:两颗星星之间的距离,宇宙中的熵值,还有客户对软件需求!很多杂志都说不要过度设计,但是并没说为什么,这里列举了十条清楚的观点。

重要提醒: 以下的一些观点像“不要滥用泛型”常常被误解为“不要使用泛型”,“不要创造不必要的包装类”被认为“不要用包装类”等等。我仅仅是在讨论过度设计而不是在讨论要如何养成好的代码风格。

1. 工程师比客户更聪明

工程师通常认为自己是最聪明的因为万物都是他们创造的。这是使我们过度设计的第一个错误。如果我们计划100件事情,商人通常会提出我们从来没想过的第101事情。如果我们解决了1000个问题,他们将会反馈10,000个问题。我们以为所有都在我们的掌控之下,然而实际上我们连我们具体的方面都不知道。

我们以为所有都在我们的掌控之下,然而实际上我们连我们具体的方面都不知道。 图片来源 http://jamesbond.wikia.com/wiki/Casino_Royale and http://fortune.com/2015/07/06/failed-trump-businesses/

在我15年的开发生涯中,我从未看到一个客户减少软件功能的需求,他们的想法通常会偏离原来的初衷,对于客户这是可以理解的,这也不是客户的错。

TL;DR — 客户总是对的。

Tip: 如果你没有时间读本文的一下部分,这一点其实已经足够了。


2. 业务功能是可以重用的

当客户提出越来越多的功能需求(这是我们都知道将会发生的)我们通常做出这样的反应:

(没有写出处的必要, 图片由Keynote制作)

我们试着尽可能地分组和概括逻辑。这就是为什么大多数MVC模型的系统到最后就剩下臃肿的模型或者臃肿的控制器。但是正如我们早已知道的,客户的需求只会增加,不会减少。

所以替代的,我们应该这样反应:

(没有写出处的必要, 图片由Keynote制作)

在系统中,共享功能的重用和抽象往往随着时间的推移而趋于稳定。当功能增多的时候系统会保持稳定或者会相对的轻量。如果不这么做的话,系统很可能由于太过臃肿而崩溃。(更可能会被重写).

举例: 我们为以前的客户建立了一个用户配置文件系统。起初我们认为功能需求与以前的很相似的,所以采用了重用的CRUD(增加(Create)、读取查询(Retrieve)、更新(Update)和删除(Delete))控制器。但是到最后却产生13种不同的开始流程——初始的连接,首次注册的信息填写,编辑信息等等,这使得重用的组件根本起不了作用。相似的,顺序视图和顺序编辑流与实际的排序流本质上是不同的.。

在水平划分业务功能的时候先试着垂直的划分。在各种情况下考虑——隔离服务,基于中继的服务,特定于语言的模块等等。这也对从一个功能转换到另一个功能十分方便。否则的话当系统的一部分发生改变的时候将会系统将会变得很复杂。

TL;DR — 功能的隔离好于功能间的组合

Tip: 在你写的代码中找一个面向外部的功能(Endpoint/Page/Job等)看看其他人要理解多少个选择语句?


3. 一切都是通用的

(有时候这一点与前一点可以结合,但是它仍可以应用于独立的项目)

  • 想要连接一个数据库?写一个通用的Adapter
  • 查询数据库? 通用的Query
  • 传递参数? 通用的Params
  • 创建参数? 通用的Builder
  • 映射返回值? 通用的Data Mapper
  • 处理用户请求? 通用的 Request
  • 执行整个事件? 通用的 Executor
  • 等等

有点工程师就是这样,不解决业务上出现的问题而要浪费时间去寻找一个完美的抽象类,答案是非常简单的。

什么是完美的抽象类? 图片来源 https://www.pinterest.com/pin/415738609324811773/

设计总是为了解决不断变化的现实世界的需求。因此即使我们奇迹般的找了一个完美的抽象类,它仍然有一个有效期的标签 #1 — The House wins in the end. 今天最好的设计其实是不设计,这有一篇震惊的文章 写容易被删除的代码,而不是可拓展的.

TL;DR — 复制比错误的抽象更好

相反的,复制有时是正确的抽象。当我们看到系统的某些部分具有相似的代码,一个好的抽象就出现了 复制暴露了许多的使用情况并使得界限更加清晰。

Tip: 在服务间共用的抽象有时会导致 微型服务最终变成分散的大石柱.


4. 表面的封装

这是在整篇文章中最困难的一个观点。提醒一下我们正在讨论过度设计。

在使用额外的库之前希先封装它,不幸的是大多数我们写的封装类都是非常表面的,我们在提供功能性和创建一个好的封装类之间徘徊,所以大多数我们写的封装类都与底层库十分相似(有些情况下是1:1的镜像关系,而有时候以10倍的功耗来完成原始库中1/10的功能)如果我们在不久对底层库进行改动,这个库的封装类到后来也不得不更改。有时我们也将业务逻辑融入到封装类中,使它既不是一个好的封装类也不是一个好的业务代码,而是处于在两者之间的层次。

这是2016. 外部库和我们的客户都有很大的提升。开源库十分流行的,它们被十分厉害的人专门所编写,有着高质量,可测试的代码。有更加清晰,可测试,更高效的API,允许我们去按照初始化——组装(Instrument)——实现的步骤去构建。

TL;DR — 封装是一个异常,而不一个规范,不要给一个好的库进行封装。

Tip: 创建一个模糊的封装可不是开玩笑的。封装一个库的想法来源于可配置这一想法,并将配置过后的具体实现隐藏起来。


5. 使用质量检测工具

盲目使用高质量代码的规范(像把所有的变量改为 “private final”,为所有的类写一个借口, 等等) 并没有把代码变得更好

查看 企业级编程 (or Hello World). 它有大量的代码。在微观层次中每一个类都遵循SOLID原则,使用各种设计模式(工厂模式,建造者模式,策略模式等)以及编程技巧(泛型,枚举等),从CQM工具中它们都获得了很高的代码质量评分。

TL;DR — 总是后退一步来宏观的看待问题

相反的,自动化CQM工具擅长于跟踪测试覆盖率,而并不能告诉我们是否我们正在测试一个正确的东西。一个基准工具能够跟踪性能表现,但是它不能告诉我们程序是否是并行执行的还是顺序执行的。只有人才能解决这些问题。

5.1. 划分层次

我们采取简洁,紧密的动作来把程序划分为10或20个层次,它们之中的任何一个层都离不开整体而独立存在。这是由于我们想要遵循“测试代码”或“单一责任原则”的概念。

在过去 — 这由继承来实现 A extends B extends C extends D

总共有200层 (没有写出处的必要, 图片由Keynote制作)

现在 — 人们做着相似的事情,让每一个类都有一个接口来继承同时把它注入到下一层中,就是为了SOLID原则。

2016年采用SOLID原则的层次 (没有写出处的必要, 图片由Keynote制作)

SOLID原则的提出是为了防止继承和其他OOP概念被滥用,而大多数开发人员都不知道这些概念是哪或为什么来的,仅仅是错误的遵照。

TL;DR — 原则是为了转变观念,不是要像工具一样盲目的使用。

学习一种不同的语言然后尝试以一种不同的思维去做事。这一才会成为一个更好的开发者。这些概念在新的语言当中可能根本不起作用 我们不应该以遵循某种原则的名义来分割某个清晰的设计


6. 过度使用

过度使用泛型. 现在一个简单的 “HelloWorldPrinter” 都会变成 “HelloWorldPrinter<>”.

当已经是很明显的只是为了处理一种数据类型或者通用的类型已经足够的情况下不要使用泛型。

多度使用策略模式. 每一个IF语句现如今都成了一个策略.

为什么?

过度使用DSL. 在哪都使用DSL(领域特定语言)

我不知道…

使用虚拟对象测试. 测试的时候每一个对象都是虚拟的.

如何去改变…

元编程是很酷的,在哪都是用元编程

谁知道为什么…

枚举/可覆写的方法/Traits这些都很酷,在哪都用

这是错的.

TL;DR — TL;DR不应该到处都用


7. –ity

  • Configurability(可配置性)
  • Security(安全性)
  • Scalability(可量测性)
  • Maintainability(可维护性)
  • Extensibility(可扩展性)

很难反驳

例1:让我们为我们的公司构建一个CMS,它遵循可拓展性,这样人们就可以很容易的增加一个新的字段。

结论: 客户永远不会使用它,因为它们必须有一个开发者坐在他旁边然后帮他完成操作,或许我们需要点击的接口替换成花几个小时来成为一个简单开发者的指南?

例 2: 让我们设计一个大型数据库并遵循可配置性,我们应该通过一个文件对数据库进行各种配置

结论:十年间,我只见到过一个客户去努力来完全配置一个数据库。这发生之后,那个所谓的配置文件就没有用了。 有如此多需要操作的工作,功能是的兼容问题,接着客户让我们把数据库里的一半模型都转化为NOSQL数据库,我们气疯了——配置文件上只是一行的改变,我们却需要把整个系统重写。

在今天这个世界,我们已经可以构建一个单一的配置层对于现代的文件系统/KV存储(如Redis/ CouchDB/ DynamoDB/ )甚至像 Postgres/ HSQLDB/ SQLite 这些数据库都可以做这样的事 或者你根本使用数据层(同时挣扎与怎样实现功能上的传递)或者使用其他数据库来作为你的一部分(如postgres geo/json 特色) 这样就可以没有配置上的羞愧,你写的堆栈和你写的代码一样都是解决问题的一部分。当你远离X-ity这些原则的时候,你就会发现新的解决方式的出现,如:你可以采用垂直的打破数据传输权限(使用小型DAOs来实现每一项操作)来替代水平的(配置层),或者对于不同的功能采取不同的数据存储方式。

例3: 我们为客户构建OAuth系统。为了安全对于内部的管理员——我们要求他在使用一次Google的OAuth。 如果有人攻击我们的OAuth,客户不希望攻击者得到他们的管理员权限。Google OAuth是更加安全的,而且谁能够否认更加安全这个观点呢?

结论: 如果有人真想要攻击你的系统,他们并非必须经过OAuth层,在周围还存在许多脆弱的层次,例如他们可以提权。因此为了支持两套OAuth的配置的努力在保护系统安全方面并没有成效,还不如先把基础搞好。

(没有写出处的必要, 图片由Keynote制作)

TL;DR — 不要一味的追求 -ities要弄清楚场景/事例/需求/适用范围

Tip:问一个简单的问题- 举一个事例/场景的例子?然后在其中深入发掘,这将会暴露-ities的缺陷.


8. 开发内部代码

一开始感觉很酷,这几年有很多开源的珍宝:

  • 内部库 (HTTP, mini ORM/ODM, Caching, Config, etc)
  • 内部框架 (CMS, Event Streaming, Concurrency, Background Jobs, etc)
  • 内部工具 (Buildchains, Deployment tools, etc)

被忽略但是:

  • 这些开源的东西需要大量的技巧和深入的理解。一个“Service runner”库需要精通守护进程是怎样工作的,进程管理,I/O重定向,PID文件等等,一个CMS不仅仅是关于把一个数据类型传递给一个字段中——这之间还有依赖关系,验证、向导、通用渲染器等。甚至是一个简单的“retry”库实际上都不是很简单的
  • 需要持续的努力去保持其稳定,甚至是一个微小的开源库都要花费大量时间去维护。
  • 没有人会关心你的开源代码,只有起初的创建者才会花时间去维护它。
  • 而最初的创建者则会最终以“x的创建者”的名号离开
  • 奉献与一个现有框架如今会花很多时间,但是创建一个新的东西将会花费更多的时间。

TL;DR — 重用,Fork,贡献,重审

最后,如果真的想要一直做下去,抱有一个开源的心态去做,与相似的项目竞争,努力说服内部人员去使用它,不要因为这是理所当然的就因为你是一个内部人员。


9. 在原有代码上构建

一旦某段代码正确的实现之后,每一个人都开始在其基础之上毫无保留的构建自己的程序。没有人质疑现状,工作中被引用的代码就被视为正确的方式,甚至是调整现有代码去适应原来的 。

每一天中团队在原有代码上构建与更改原有代码之间的对比:

图片来源 http://ronjeffries.com/xprog/articles/refactoring-not-on-the-backlog/

TL;DR — 重构是编程的一部分,没有任何代码是不能改变的


10.错误的预估

我们经常看到一个好的团队或者是开发者在最后开发出来的东西很糟,我们查看他们的代码然后就想说“WTF,这真是我所仰慕的那个团队/开发者写出来的吗?

高质量的代码不仅需要编程技巧也需要时间。一些聪明的开发者往往会高估自己的能力,到最后他们将发现自己做出程序的丑陋和时间的紧迫。

TL;DR — 预先错误的预估将会降低代码的质量


如果你想继续往下写,非常感谢!提醒一下我仅仅讨论了过度工程化的问题并没有说代码风格, 进一步的链接:

译者flyback尚未开通打赏功能

相关文章