其他
不要让框架控制你的项目,过度依赖框架会害了你!
责编:Java就该这么学 | 作者 | Bèr Kessels、译者 | 弯月 | 出品 | CSDN(ID:CSDNnews)
原文链接:https://berk.es/2022/09/06/frameworks-harm-maintenance/
在本文中,我们来探讨一下使用框架构建软件,对软件的可维护性有哪些危害。我认为:
使用框架有损于软件的可维护性。
框架与个人或团队有着不同的目标。
框架设计中的权衡会危及项目的可维护性。
框架的构建初衷就是为了控制你的项目。
以解耦的方式采用框架,不仅能享受框架带来的好处,而且还可以避免损害可维护性。
框架是什么?
首先,我们来弄清楚框架的准确含义。框架不仅仅是使用第三方代码,也不仅仅是一种方法或架构:软件框架(software framework),通常指的是为了实现某个业界标准或完成特定基本任务的软件组件规范,也指为了实现某个软件组件规范时,提供规范所要求之基础功能的软件产品。软件框架与普通的代码库之间有几个关键的区别:
控制反转:框架与库或标准用户应用程序不同,整个程序的控制流不是由调用者决定的,而是由框架决定的。而这通常是通过模板来实现的。
可扩展性:用户可以按照重载的方式扩展框架,即编写用户专用的代码来提供特定的功能。
不可修改的框架代码:一般来说,框架代码不应该被修改,但可以接受用户的扩展。换句话说,用户可以扩展框架,但不能修改其代码。
“有损于维护性”指什么?
软件顺利启动,并投入使用,接下来我们只需要正常维护。维护通常分为以下几类:
纠正式的软件维护:修复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还会存在吗?然而,在选择框架构建产品时,你就与它深度绑定了。永远绑定了。在项目之初,在拥有的信息量最少的那一刻,你却做出了最关键的决定。
框架设计中的权衡会危及项目的可维护性
与其他软件一样,框架的创建者必须做出权衡。例如,从流行框架的网站宣传中就可以看出,所有的流行框架都格外注重开发速度和可扩展性。然而,这两个特征与可维护性没有任何关系,相反在有些情况下还会损害可维护性。
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、存储(如数据库)、事件总线、日志记录、消息传递等底层的机制,所有这些都是细节,它们与你的业务逻辑和领域无关。另外,搜索公众号Java后端栈后台回复“私活”,获取一份惊喜礼包。会计应用的架构应该叫做“会计”,而不是 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指出的那样:最佳实践可以保证应用程序的稳定性、可维护性和可升级性。而“最佳实践”之一就是不要让框架控制你的项目!欢迎有需要的同学试试,如果本文对您有帮助,也请帮忙点个 赞 + 在看 啦!❤️
在 GitHub猿 还有更多优质项目系统学习资源,欢迎分享给其他同学吧!
「Java就该这么学」建立了读者Java交流群,大家可以添加小编微信进行加群。欢迎有想法、乐于分享的朋友们一起交流学习。
扫描添加好友邀你进Java群,加我时注明【姓名+公司+职位】
版权申明:内容来源网络,版权归原作者所有。如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。
往日文章:
看看人家那快速开发代码生成器系统,那叫一个优雅!面试官:谈谈过滤器和拦截器的区别?