欢迎来到我们关于Ruby on Rails模式和反模式系列的第一篇文章。在每一篇文章中,我们都会深入探讨你在使用Rails应用程序时可能遇到的各种模式。
今天,我们将展示什么是(设计)模式,然后也尝试解释什么是反模式。为了更好地说明解释,我们将使用已经存在了相当长一段时间的Ruby on Rails框架。如果Rails由于某种原因不是你的那杯茶,请坚持住,这里描述的想法(或模式)可能与你最终使用的任何技术产生共鸣。
但在我们开始解释什么是模式和反模式之前,我们是如何走到需要它们的这一步的?为什么我们的软件需要有这些东西?为什么我们需要设计我们的解决方案?
是的,你是一个设计师
甚至从早期的计算机编程时代开始,人们就必须处理他们所编写的程序的设计问题。写一个程序(或软件)就是为一个问题设计一个解决方案。当你写软件的时候,你就是一个设计师--请随意将其附加到你的工作头衔上。设计好的解决方案很重要,因为我们写的软件会被其他人阅读和/或编辑。另外,我们想出的解决方案在未来也会被其他人建立起来。
考虑到这一切,几代工程师开始在他们的职业生涯中看到类似的代码和架构设计。人们开始提取和记录问题的标准解决方案。有人会说这是我们人类运作的一种自然方式。我们喜欢在一切事物中进行分类和寻找模式,软件也不例外。
作为人类,随着软件工程变得越来越复杂,模式开始越来越多地出现。软件设计模式开始在世界各地的工程师中发展和巩固自己。书籍、论文和讲座的出现,进一步传播了经过深思熟虑和实践检验的解决方案的理念。这些解决方案节省了很多人的时间和金钱,所以我们来看看设计模式这个术语,看看它到底是什么。
什么是设计模式?
在软件工程中,模式被描述为一种可以重复使用的解决方案,以解决一个共同的问题。模式是在软件工程师中被认为是一种良好的做法。由于软件工程师设定了它们,他们可以迅速从模式变成它们的对立面--反模式--但我们稍后会说到这一点。
一个设计模式会给你指出解决问题的方法,但它不会给你一段准备好的代码,让你插入软件的其他部分。把模式看成是写好代码的指南,但你必须想出实现的方法。在日常编码中使用模式是在80年代末出现的,Kent Beck和Ward Cunningham提出了一个使用"模式语言 "的想法。
模式语言的想法是在70年代末由Christopher Alexander在他的书《模式语言》中提出的。你可能会感到惊讶,但这本书不是关于软件工程的,而是关于建筑物的架构。模式语言是一套有组织的、连贯的模式,每一个模式都描述了一个问题和一个解决方案的核心,可以用很多方式使用。听起来很熟悉?(提示:框架,另一个提示:Rails)
后来,软件工程中的设计模式在1994年Gang Of Four出版的传奇书籍《设计模式》之后变得有名,受众众多。在这本书中,有对现今使用的模式的解释和定义--工厂、单子、装饰器,仅举几例。
很好,现在我们已经熟悉或刷新了关于设计和模式的知识,让我们看看什么是反模式。
什么是设计反模式?
如果你认为模式是好人,那么反模式就是坏人。更准确地说,软件反模式是一种可能被普遍使用但被认为是无效的或反作用的模式。反模式的典型例子是包含许多功能和依赖关系的上帝对象,这些对象可以被提取并分离成不同的对象。
造成代码中反模式的常见原因有很多。例如,一个很好的例子是当好人(模式)变成坏人(反模式)。比方说,你在以前的公司里习惯于使用一种特定的技术,并在其中获得了很高的能力。在这个例子中,让我们使用Docker。你知道如何有效地将应用程序打包到Docker容器中,在云中协调它们,并将它们的日志从云中拉下来。突然间,你得到了一份新的工作,你需要运送前端应用程序。由于你对Docker有很多了解,也知道如何用它来运送应用程序,你的第一个决定是把所有东西打包并部署到云中。
但是,你不知道,在你目前的工作中,前端应用程序并不复杂,把它们放到容器中可能不是最有效的解决方案。起初听起来是个好主意,但后来证明是适得其反。这种反模式被称为"金榔头"。
它可以用一句话来概括,"如果你有一把锤子,一切看起来都像钉子"。如果你对Docker和服务的协调真的很在行,那么所有的东西都是一个Docker服务,都是为了在云中进行协调的。
这些事情发生了,而且会发生。好人转为坏蛋,反之亦然。但是,Ruby和Rails在这幅图中的地位如何?
首先是Ruby,然后是Rails
大多数人是通过使用Ruby on Rails认识Ruby的,Ruby on Rails是一个用于快速构建网站的流行框架。我也是通过同样的方式认识Ruby的,这没有错。Rails是基于这种成熟的软件模式,称为模型-视图-控制器,简称MVC。但在我们深入了解Rails中的MVC模式的细节之前,经常发生的一个大谬误是在没有正确学习Ruby的情况下使用Rails。
当你有一个想法并想快速建立它的时候,Rails框架是首选的框架之一。如今,情况完全不同了,Rails仍然被使用,但已经没有了它的鼎盛时期的程度。由于使用和运行如此简单,很多初学者开始使用rails的新命令来构建他们的网络应用。然后发生了什么,沿着这条路,问题开始出现了。作为一个初学者,你被使用Rails开发的速度和简单性所吸引,一开始一切都运行得如此神奇和顺利。然后你发现你已经把很多 "魔法 "视为理所当然,你不明白幕后发生了什么。
我有这个问题,我相信许多初学者和高级初学者都有这个问题。你从一个框架开始,你在它的基础上建立,当你试图添加一些高度定制的东西时,你不能,因为你已经用完了那个框架的所有魔法点。在这一点上,你必须回到起点,学习基础知识。回到过去并不是什么大事,我们中最好的人都会遇到这种情况。但是,如果你没有学习基本的东西就继续前进,问题就会越来越大,比如在Ruby中。在这方面,有一本好书可以帮助你,那就是《全面发展的Ruby主义者》。
作为一个初学者,你不必从头到尾读它。但要把它放在你身边,以便你能迅速查阅。我并不是说你应该突然停止你正在做的事情并读完这本书,而是不时地停下来,刷新你对Ruby基础知识的认识,它可能会为你打开一些新的视野。
MVC:Rails的面包和黄油
好的,但是MVC呢?模型-视图-控制器模式已经存在很久了。它已经被许多框架所采用,涉及大量的语言,如Ruby(Rails)、Python(Django)、Java(Play、Spring MVC)。它的理念是有独立的组件来完成它们的工作:
- 模型处理数据和业务逻辑。
- 视图用于数据的展示和用户界面。
- 控制器通过从模型中获取数据并向用户展示视图,将两者联系在一起。
理论上听起来很好,而且当逻辑最小,你的网站没有复杂的逻辑时,它是非常好的。这就是事情变得棘手的地方,但我们一会儿就会说到这一点。
MVC像野火一样在整个网络开发界蔓延开来。即使是像React这样的库,最近也非常流行,它被解释为你的网络应用的视图层。没有其他模式被普及到无法摆脱的地步。Rails用ActionCable增加了发布-订阅,其中渠道的概念被描述为MVC模式的控制器。
但在如此广泛使用的模式中,有哪些反模式呢?让我们来看看MVC模式每个部分的一些最常见的反模式。
模型问题
随着应用程序的增长和业务逻辑的扩展,人们往往会使他们的模型过于拥挤。持续的增长会导致一种叫做 "胖模型 "的反模式。
著名的 "胖模型,瘦控制器 "模式被认为是坏人,有些则是好人。我们会说,拥有任何一个胖子都是一种反模式。为了更好地理解它,让我们进入一个例子。想象一下,我们有一个像Spotify或Deezer的流媒体服务。在它里面,我们有一个像这样的歌曲模型:
class Song < ApplicationRecord
belongs_to :album
belongs_to :artist
belongs_to :publisher
has_one :text
has_many :downloads
validates :artist_id, presence: true
validates :publisher_id, presence: true
after_update :alert_artist_followers
after_update :alert_publisher
def alert_artist_followers
return if unreleased?
artist.followers.each { |follower| follower.notify(self) }
end
def alert_publisher
PublisherMailer.song_email(publisher, self).deliver_now
end
def includes_profanities?
text.scan_for_profanities.any?
end
def user_downloaded?(user)
user.library.has_song?(self)
end
def find_published_from_artist_with_albums
...
end
def find_published_with_albums
...
end
def to_wav
...
end
def to_mp3
...
end
def to_flac
...
end
end
像这样的模型的问题是,它们成为可能与一首歌曲相关的不同逻辑的倾销地。这发生在随着时间的推移,方法被慢慢地逐一添加。然后,整个模型就显得庞大而复杂,把逻辑分割到其他几个地方可能会被证明在未来是有益的。
从一开始,你就可以看到这个模型破坏了一些推荐的做法。它打破了单一责任原则(SRP)。它涉及到通知追随者和发布者。它检查文本的亵渎性,有将歌曲导出为不同音频格式的方法,等等。拥有这一切增加了模型的复杂性,我甚至无法想象这个模型的测试文件。
如何重构这个模型主要取决于在其他地方如何调用和使用方法。我将介绍一些关于我们如何处理这些的一般想法,你可以选择最适合你情况的方法。
通知追随者和发布者的回调可以被提取到作业中。工作将被排队,而逻辑则被保留在模型之外,就像这样:
class NotifyFollowers < ApplicationJob
def perform(followers)
followers.each { |follower| follower.notify }
end
end
class NotifyPublisher < ApplicationJob
def perform(publisher, song)
PublisherMailer.song_email(publisher, self).deliver_now
end
end
作业将在独立的进程中独立运行,远离模型。现在你可以单独测试你的作业逻辑,只需检查适当的作业是否从你的模型中被排入。
比方说,检查脏话和用户是否下载了歌曲都是在我们应用程序的视图部分发生的。在这种情况下,我们可以使用Decorator模式。一个可以让你快速入门的流行解决方案是Draper gem。有了它,你可以写一个类似于这个的装饰器。
class SongDecorator < Draper::Decorator
delegate_all
def includes_profanities?
object.text.scan_for_profanities.any?
end
def user_downloaded?(user)
object.user.library.has_song?(self)
end
end
然后,你可以在你的控制器中调用decorate ,比如说。
def show
@song = Song.find(params[:id]).decorate
end
并在你的视图中使用它,像这样。
<%= @song.includes_profanities? %>
<%= @song.user_downloaded?(user) %>
如果你不喜欢使用依赖关系,你可以滚动你的装饰器,但我们将在另一篇博文中讨论这个问题。现在你已经分离了大部分的模型问题,让我们来处理寻找歌曲和转换歌曲的方法。我们可以使用模块来分离它们:
module SongFinders
def find_published_from_artist_with_albums
...
end
def find_published_with_albums
...
end
end
module SongConverter
def to_wav
...
end
def to_mp3
...
end
def to_flac
...
end
end
歌曲模型将扩展SongFinders 模块,所以它的方法可以作为类方法使用。歌曲模型将包括SongConverter 模块,所以它的方法可以在模型实例上使用。
所有这些都应该使我们的Song模型变得非常纤细和有意义:
class Song < ApplicationRecord
extend SongFinders
include SongConverter
belongs_to :album
belongs_to :artist
belongs_to :publisher
has_one :text
has_many :downloads
validates :artist_id, presence: true
validates :publisher_id, presence: true
after_update :alert_artist_followers, if: :published?
after_update :alert_publisher
def alert_artist_followers
NotifyFollowers.perform_later(self)
end
def alert_publisher
NotifyPublisher.perform_later(publisher, self)
end
end
还有更多的模型反模式,而这只是模型可能出现问题的一个例子。请继续关注本系列的另一篇博文,我们将在那里详细介绍更多的模型反模式。现在,让我们看看视图会出什么问题。
视图问题
除了模型问题,Rails用户有时也会为他们的视图的复杂性而苦恼。在过去,HTML和CSS是Web应用程序中视图部分的王者。慢慢地,随着时间的推移,JavaScript开始统治,前端的几乎所有方面都是用JavaScript编写的。关于这一点,Rails遵循了一个有点不同的范式。你不应该在视图中使用所有的JavaScript,而应该只在视图中 "撒 "上JS。
在任何情况下,要在同一个地方处理HTML、CSS、JS和Ruby都会变得很混乱。构建Rails视图的棘手之处在于,领域逻辑有时会在视图中找到。这是一个禁忌,因为它破坏了MVC模式,这是一个开始。
另一种情况是在你的视图和参数中使用过多的嵌入式Ruby。也许一些逻辑可以放在一个帮助器或装饰器(也称为视图模型或演示器)中。我们将在本系列的一些下一篇文章中讨论它的例子,敬请关注。
控制器问题
Rails控制器也会出现各种不同的问题。其中之一就是胖控制器的反模式。
之前,我们的模型很胖,但它失去了一些重量,现在我们注意到,控制器在这个过程中增加了一些额外的重量。通常情况下,这种情况发生在业务逻辑被放在控制器内,但其实际位置在模型或其他地方。在大模型部分分享的一些想法仍然可以适用于控制器--提取代码到呈现器,使用ActiveRecord回调,求助于服务对象。
有些人甚至会使用Trailblazer或dry-transaction这样的宝石。这里的想法是创建处理特定事务的类。将所有的东西从控制器中移出,保持模型的瘦小,你在这些独立的类中存储和测试逻辑,有些人称之为服务、事务、行动,以及类似的。
总结
还有很多反模式,甚至更多的解决方案。试图在这篇文章中涵盖所有的东西将花费太多的空间和时间,这将使我们的文章看起来很胖(就像我们谈到的模型和控制器)。请务必关注我们的系列文章,在那里我们将深入研究Rails中MVC模式的每一个方面。在那里,你会发现如何处理最著名的反模式。在那之前,我希望你喜欢这个关于什么是模式和反模式以及Ruby on Rails框架中最常见模式的概述。