原文链接:https://berk.es/2022/09/06/frameworks-harm-maintenance/
声明:本文为 CSDN 翻译,未经许可禁止转载。
作者 | Bèr Kessels

在本文中,我们来磋商一下利用框架构建软件,对软件的可掩护性有哪些危害。我认为:
利用框架有损于软件的可掩护性。
框架与个人或团队有着不同的目标。
框架设计中的权衡会危及项目的可掩护性。
框架的构建初衷便是为了掌握你的项目。
以解耦的办法采取框架,不仅能享受框架带来的好处,而且还可以避免危害可掩护性。
框架是什么?
首先,我们来弄清楚框架的准确含义。框架不仅仅是利用第三方代码,也不仅仅是一种方法或架构:
软件框架(software framework),常日指的是为了实现某个业界标准或完成特定基本任务的软件组件规范,也指为了实现某个软件组件规范时,供应规范所哀求之根本功能的软件产品。
软件框架与普通的代码库之间有几个关键的差异:
掌握反转:框架与库或标准用户运用程序不同,全体程序的掌握流不是由调用者决定的,而是由框架决定的。而这常日是通过模板来实现的。
可扩展性:用户可以按照重载的办法扩展框架,即编写用户专用的代码来供应特定的功能。
不可修正的框架代码:一样平常来说,框架代码不应该被修正,但可以接管用户的扩展。换句话说,用户可以扩展框架,但不能修正其代码。
根据定义,框架的紧张功能是供应功能、行为、流程和默认值,而且所有这些都是框架内置的,个中一些是不可变动或指定的。框架许可用户添加代码,但不能变动其代码。
所有的软件框架都可能引入掩护的问题,但我个人利用框架的履历仅限于Web做事(API、后端、全栈)、命令行和GUI。2022年,越来越多的软件朝着Web发展,因此,本文谈论的例子也仅限于Web框架。
人们利用框架的目的是,以更标准化、更快、更随意马虎、更安全、更好的可扩展性、更同等或更有趣的办法开拓软件。然而很讽刺的是,根据维基百科的先容,利用框架不会供应任何好处,相反只有各类弊端。
标准化背后的思想是,迫使开拓职员按照事先定义好的办法编写代码。利用框架不仅可以统一代码的组织办法,而且API和逻辑也更随意马虎辨识。然而,我创造结果却事与愿违。2021年开拓运维现状报告(参考链接:https://puppet.com/blog/2021-state-of-devops-report/)表明,利用框架之类的技能根本无法担保项目成功。而强制开拓职员利用框架只能导致情形进一步恶化。
低效公司的通病每每表现在:由一个团队定义标准、流程、实践、框架或架构,而其他团队则必须遵守。
相反,高绩效公司每每缺少这些所谓的“统一标准”。
换句话说:逼迫标准化技能,每每得不偿失落。
实在,这并非没有道理:如果逼迫公司中的每个人都利用 Django,而无论项目的实际情形如何,那么终极一定有很多项目会由于选择Django而处处碰钉子。
只管如此,框架确实能够为某个项目或团队供应好处。但是,标准化(和统一)基本没有任何好处,乃至弊大于利。
项目的开拓速率、意见意义性以及难易度,在很大程度上取决于项目所处的阶段。利用框架天生模型的代码,可以节省编写初始代码的韶光。这一点我也赞许。但是,对付一个开拓了十几年的中大型团队来说,节省的这点韶光(半个小时?)是微不足道的。尤其是,经由了这么长的韶光,框架可能天生了数百个这样的模型,而别的几万个小时都花在了修正和掩护现有代码上。下面,我会详细解释从项目的长期发展来看,这种短暂的“开拓速率提升”换来的却是对可掩护性的危害。
此外,安全性和性能非常依赖于大环境。框架会向项目添加大量代码。运气好的话,这些代码无伤大雅;但倘若运气不佳,则可能引入大量的潜在攻击和大量的开销。我将不才文中展示实在不该用框架更加随意马虎确保安全性并提高性能。
“有损于掩护性”指什么?
软件顺利启动,并投入利用,接下来我们只须要正常掩护。掩护常日分为以下几类:
纠正式的软件掩护:修复bug;
预防式的软件掩护:防止缺点,稳步改进;
完美式的软件掩护:润色与润色;
适应式的软件掩护:持续开拓。
不过,在本文中,我打算将软件投入利用后的所有变更都视为掩护。
在掩护期间内,任何阻碍掩护事情持续开展的成分,都应被视为危害。因此,如果利用框架会导致新功能的发布速率减慢,则视为危害。
此外,如果在软件开拓的早期,利用框架有助于快速发布功能,但相应的代价是导致后期新功能的发布速率减慢,则视为有损于掩护性。
第三种危害是,框架的利用导致我们须要付出额外的努力,但这部分事情并不能为客户供应代价,比如框架升级、弃用、教诲和信息摄入(例如学习新功能)等。这些事情须要付出昂贵的代价,而且每每是稀缺资源,比如你须要花费大量韶光升级技能栈,原来这些韶光应花在供应用户或市场想要的新功能。
末了一种危害是,将来框架有可能不再适宜项目。如果框架朝着不同的方向发展,或者利用了框架的软件朝着不同的方向发展,那么二者就不再适配。
框架与个人或团队有着不同的目标
Ruby on Rails创始人DHH曾表示:
虽然你寄予了框架巨大的希望,但框架并没有对你做出任何承诺。框架可以按照创始人的喜好,朝着任何方向发展。而你只能像一只虔诚的小狗一样默默跟随。
我敢肯定,大多数框架的创始人对用户没有任何敌意,他们发自至心关心用户,而DHH肯定也希望用户在利用Rails时感想熏染到快乐。但是,这些创始人更关心的是有多少用户乐意利用框架,并一起相随,而不是你能否在接下来的十五、二十年内连续创造代价。
许多Web框架,比如Django、Rails、Spring、Gatsby 和 Symfony等的营销词中都提到了掩护以及可掩护性。
Symfony:加快创建和掩护PHP Web运用程序的速率。摆脱重复的编程任务,享受掌握代码的力量。
那么他们是如何实现的呢:
利用最佳实践确保运用程序的稳定性、可掩护性和可升级性。
关于框架如何供应长期的支持,Rails 的官方态度是:
当某个版本系列不再受支持时,修复缺点和安全问题的任务由您自行承担。我们会供应修补程序的向后移植并发布到git,但是不会发布新版本。如果你无力掩护自己的版本,则应升级到受支持的版本。(参考链接:https://rubyonrails.org/maintenance)
他们的态度很明确:框架不会长期供应支持。为了让项目利用最新版本的Rails,你须要更新或移植框架,但这些事情都须要资源。
再者,即便眼下框架与你的目标完备同等,但将来呢?尤其是对付刚刚启动的项目来说,谁又能预知未来呢?你的产品会坚持Web运用的路线?你确定将来只发布Windows桌面版的运用程序?你确定在接下来的几年中关系数据库是最佳存储办理方案?你确定你须要可扩展性?十年之后JavaScript PWA还会存在吗?
然而,在选择框架构建产品时,你就与它深度绑定了。永久绑定了。在项目之初,在拥有的信息量最少的那一刻,你却做出了最关键的决定。
框架设计中的权衡会危及项目的可掩护性
与其他软件一样,框架的创建者必须做出权衡。例如,从盛行框架的网站宣扬中就可以看出,所有的盛行框架都格外看重开拓速率和可扩展性。
然而,这两个特色与可掩护性没有任何关系,相反在有些情形下还会危害可掩护性。
开拓速率的提升部分来自样板代码的天生,但更多时候来自继续。框架天生代码就意味着创建新代码,但不卖力掩护这些代码。例如react-boilerplate 或 create-react-app等框架就会天生大量的样板代码,它们只是代码天生器。但代码必须掩护,否则就会降级,并引发各种问题,比如大量重复、不一致、不兼容等,也便是我们常说的“代码糜烂”。
框架可以通过其他手段办理代码糜烂的问题,比如将所有代码都放入超类(或可重用函数)中,这样就能在一个合理的地方统一供应样板代码。作为用户(即利用框架的开拓职员),你可以继续类,或者采取mixin的办法利用其他类、模块或函数的代码。
例如,在Rails中,你只须要继续“一个模型”,就可以让工具公开大量方法。举个例子,假设Post有三个数据库字段:
class Post < ActiveRecord::Base; end
那么,你至少可以得到 767 个公共类方法和 487 个公共实例方法,也便是说,你可以通过子类化继续1200 多个方法!
由于Post类供应了这么多方法,以是你就必须掩护它们。毕竟,你的类为用户供应了这些方法。这些方法存在于你的类中、你的实例中。
它们深埋于框架的代码中,这就成了你的任务,由你来掩护它们。这便是框架的实质,你无法改变,也无法掌握。
框架乃至可以决定在某个时候弃用或修正某个方法。由于利用了框架,以是我们供应了大量的公共接口,却没有能力掌握它。我们的统统都将受到管束,寄希望于框架的创建者是个好心人,能供应更新,并担保框架的向后兼容性和可用性。虽然大多数框架的创建者都很友好,但谁也无法担保这些API永久稳定。还有Drupal之类的框架供应的升级如此弘大,导致用户不得不完备重写项目,而且每隔几年就要经历一次这样的升级!
虽然有些框架很友好,会努力保持向后兼容,而且每次升级都是很小的一步,但更新还是避免不了。而我们只能俯首听命,必要时修正现有代码。
虽然许多框架不像 Rails 那样极度,公共接口包含 1200 多个方法。但所有框架都为用户供应了 API、函数和类,毕竟这正是框架存在的意义。
我们利用这些代码,并随着韶光的推移,将我们的代码更加紧密地耦合到框架中。直到我们的代码完备依赖于框架。
以是人们常说,在框架内开拓软件,而不是利用框架开拓软件,由于你确实是在框架中构建项目。
此外,框架所能供应的性能与扩展水平是相较于其他类似的框架而言的。如果我们能选择底层架构,并进行优化,那么就能利用更少的代码,编写更高效、更具扩展性的软件。而另一方面,各种框架却因导致项目涌现性能问题,而频繁地涌如今各大新闻头条中。例如,推特的“Fail-Whale”(失落败鲸)事宜便是由于Rails糟糕的性能引发的,后来推特宣告用Java重写了Rails代码库。这次事宜证明,大多数框架都会显著增加性能开销。
扩展和性能问题的常见地决方案是,选择适宜的架构,优化底层代码,并减少总代码量,这就意味着我们必须能够在创造性能问题时自由修正代码。只有节制足够的信息,我们才能做出精确的选择和优化。而框架会危害可扩展性,由于我们很难从一个框架迁移到更适宜的其他框架或架构,或者建立更得当的设置。在碰着“Fail-Whale”之类的问题时,我们都希望优化有问题的代码,而不是用Java重写所有代码。
框架的构建初衷便是为了掌握你的项目
利用框架开拓软件时,项目一定会与框架深度绑定。每次我们在Rails中编写:belongs_to(:author),或者在Django中编写:models.ForeignKey(\"大众Band\公众),就会导致我们的项目与框架的绑定更加紧密。
如果只是很小的一部分代码绑定到框架,那么还能担保一定的可掩护性。然而,当这种绑定的覆盖范围很大,界线模糊或完备消逝时,就很难掩护了。当我们的领域和业务逻辑与框架代码混在一起;当高等业务观点与底层的架构机制混在一起;当业务逻辑混入底层架构,我们必须阅读掌握器、视图、模型、工厂、做事、配置文件、库、框架代码,才能搞明白为什么案例A中创建了User,而案例B不须要,那么可掩护性就无从谈起了。
框架抽象出了许多技能细节,它们会供应一个ORM来抽象数据库的处理,有时开拓职员乃至根本不须要知道自己正在利用数据库。他们只需调用model.save或User.find_by(email: \公众example.com\公众) ,就能保存或获取数据,而根本不知道这些数据实际上保存在PostgreSQL、sqlite还是MongoDB中。虽然我们不会被绑定到特定的数据库,但会绑定到ORM和框架。你可以自由利用任何数据库,但代价是无法再利用另一个ORM和框架。
HTTP、存储(如数据库)、事宜总线、日志记录、通报等底层的机制,所有这些都是细节,它们与你的业务逻辑和领域无关。
司帐运用的架构该当叫做“司帐”,而不是 Spring & Hibernate。@unclebobmartin
然而,这些框架还鼓励开拓职员将逻辑与框架代码稠浊在一起。他们供应了各种API、类和函数,供我们在业务逻辑中利用。因此,我们的代码不仅会与框架紧密耦合,而且还会将业务逻辑和样板代码彻底混在一起。更糟糕的是,他们常常鼓励我们通过这些“细节”来传播业务逻辑。在MVC模型中,M是存储,V是模板,而C是HTTP层,却没有供应一个统一的、合乎逻辑的地方来保存逻辑和领域代码。框架鼓励我们将这些代码放在最近的地方,而不是最方便掩护的地方。
在框架中开拓软件时,类似于如下的情形并不少见:
def create
if User.exists?(email: params[:email])
render :new, status: :already_exists
elsif user.save
flash[:success] = flash_message_for(@user, :successfully_created)
redirect_to edit_admin_user_path(@user)
else
render :new, status: :unprocessable_entity
end
end
def user_params
params.require(:user).permit(permitted_user_attributes |
[:use_billing,
role_ids: [],
ship_address_attributes: permitted_address_attributes,
bill_address_attributes: permitted_address_attributes])
end
仔细阅读上述代码,会让人感到心惊肉跳。这段代码非常缺少连贯性,我们的思维从领域逻辑一跃而下,经由框架API到交付机制的细节,然后辗转安全细节,再到业务逻辑,末了返回。看似是一段HTTP层的代码,里面却夹杂着许多业务逻辑。
如果是在一个干净的分层架构中,我们肯定会分离这些技能细节,避免将它们稠浊在一起,同时将业务逻辑统一放在一个地方。
在这样的架构中,框架的浸染并不主要,领域(或层)的意义就在于独立、没有任何依赖关系。这样的领域代码不会依赖于反序列化 JSON、HTTP 标头、数据库事务、连接池等任何技能细节。这样的领域只关心领域措辞,比如它只会调用抽象方法posts_repository.create(post)。
这样的系统拥有良好的可掩护性,由于所有代码的浸染都很明确。这样的系统是隔离的,而且是一个整体。如果你想修正Post的存储(比如你放弃MongoDB,转而采取直接在磁盘中保存Markdown文件),则只需修正PostsRepository。任何与业务逻辑干系的代码都不须要动。
将这些实现细节放入单独的一层,那么软件就会更加易于掩护,由于代码变更都是单独的。有了这样的架构,即便利用了框架,也会被抛在一边,而且每次只需改换一小块的难度会大大降落。
以解耦的办法采取框架,不仅能享受框架带来的好处,而且还可以避免危害可掩护性
许多人可能会说,不该用框架则意味着我们须要动手编写所有代码。这种非黑即白的意见有点过于极度。我们可以很好地利用库和框架,同时也要编写好代码。我们该当依赖(安全)专家来编写关系到安全的代码。如果可以避免,我们又何须学习如何编写加密算法或处理密码的代码。我们该当利用库来处理这些细节。
但是,我们该当明确指定一个单独的地方。卖力将HTTP路径映射为方法调用的代码就该当放在HTTP层,不应该牵扯任何业务逻辑。隔离度越高,可掩护性就越好。代码令牌认证等处理不应该由我们编写,而是该当统一放入一个单独的、有界线的区域。最好将其封装起来,并转换成领域措辞,如authentication.is_known_as_admin(request.token)。
发送的方法该当大略地定义为messenger.deliver(recipent, body)。该方法的背后是一个完全的通报框架,不仅供应指数退避重试、缓冲、智能路由等功能,而且可以推送关照和发送电子邮件。
保存用度的方法叫做expenses_repository.add(expense),其背后可能利用了天下上最繁芜的分布式数据库框架,或者利用了一个俊秀的框架将用度推送到某个在线司帐工具中。
关键不是永久不要利用框架,而是要隔离它们,并统一从一个地方调用。将框架的影响范围降到最低,这是我们的任务。
然而,大多数框架预先定制了很多技能细节,并且都稠浊在一起。因此,我们很难将它们分开。这样的框架已经失落去了意义,很快就会变成库。
为什么没有这样的框架?
首先,我们的基本思路是不依赖于框架,但构建框架却不该用框架,这与框架本身的目标背道而驰。
其次,可掩护性良好的软件须要随着韶光的推移而不断发展,以适应不断变革的需求。
从HTTP迁移到事宜总线时,显然你不再须要HTTP框架。当从基于 Web 的做事转而利用原生移动运用的做事时,你所须要的也不再是HTML/CSS/asset,而是序列化和处理 JSON 要求的方法。可掩护性哀求软件不断发展。HTTP框架供应HTTP做事,但是当需求发生变革,且你不再须要HTTP做事时,却没办法删掉这些框架。一些 MVC 框架供应利用关系数据库的 ORM,但如果ORM框架过期,你也没办法摆脱它们。
第三,有些实现并不须要框架。例如,CQRS之类的架构实际上便是一个大略的if语句:if(is_command) { command(params) } else { query(params) },写这种代码根本不须要框架。
末了,掩护事情的难易程度与利用特定的工具或框架无关。正如Symfony指出的那样:
最佳实践可以担保运用程序的稳定性、可掩护性和可升级性。
而“最佳实践”之一便是不要让框架掌握你的项目!