Rails系统重构:从单一复杂系统到多个小应用集群

面对复杂的Rails应用带来的挑战,本文提出了一种将单一系统拆分为多个轻量级应用集群的重构方案,解决了代码臃肿、测试部署困难等问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Ruby on Rails近几年在国内受到越来越多的开发者的青睐,Rails应用也从较简单的内部系统深入到复杂的企业级应用。Rails“习惯优于配置”的思想以及ActiveRecord等众多优秀的技术极大地提高了开发效率,但在业务复杂的大型系统中,Rails应用也会面临很多问题。

\

本文将介绍一种Rails系统重构方案,将复杂的Rails单一系统拆分成相互协作的多个轻量级应用集群,从根本上解决Rails系统在处理复杂的业务时代码臃肿、开发效率降低、难以维护与部署等问题。

\

复杂Rails系统存在的问题

\

用Rails可以快速搭建一个较简单的应用,但当业务需求急剧增长,功能越来越复杂时,系统的维护和扩展会变得越来越困难。一般情况下,问题主要表现在以下几个方面:

\

代码臃肿

\

我们知道Rails提倡RESTful架构,所以良好的代码组织方式是对每一个实体(Model)都有相应的处理器(Controller)来进行操作,而Rails每个Controller都有一个相应的Helper以及若干个View。这样当系统功能比较复杂时,代码量会急剧增大。

\

首先是Model的数量会很多。由于Model都放在app/models目录下面,而Rails又不支持Model分目录,虽然可以通过修改load_paths解决这一问题,但有时(如目录与Model重名)会造成难以调试的错误,所以当Model数量比较多时,这个目录会变得难以管理。

\

其次是Controller的目录层级会比较深。为保持清晰的代码结构,Controller应该按照功能分类存放在不同的目录下。所以当功能比较多时,很容易出现四五层甚至更深的目录结构,这不仅使代码难以管理,routes的配置和解析也变得很复杂,例如会出现level1_level2_level3_level4_controller_name_path这样很长的routes Helper方法。

\

难以测试与部署

\

复杂的业务代码不仅会增加写测试代码的难度,运行测试的时间也必然会随之增加。大量的fixtures不仅难以管理,还经常会造成互相干扰。

\

复杂的系统也增加了部署的风险,一个小错误可能会导致整个系统的崩溃。为了降低这种风险,需要延长系统部署的周期,只在特定的时间或系统有重大更新时才部署,这样就在一定程度上弱化了Rails系统根据用户需求快速升级迭代的优点。

\

影响团队建设

\

除了技术上的问题,复杂的Rails系统对团队建设也会产生不利影响。首先如果某个开发人员提交了测试无法通过的代码,会对其他人的工作产生影响,降低开发效率;其次对于复杂的系统,增加新功能或修复bug都变得比较困难,久而久之程序员就会产生惰性,代码能少改的就少改,严重阻碍了系统的快速进化;最后在团队有新人加入时,会担心由于其不熟悉系统造成系统崩溃,而不敢放手让他真正参与进来,这样对新人的成长是十分不利的。

\

轻量级应用集群

\

为解决复杂的Rails系统产生的一系列问题,我们将单一系统按照业务功能进行划分,每一部分用一个独立的Rails应用来实现,从而形成若干个轻量Rails应用集群,这些应用相互协作,共同实现整体业务逻辑。

\

拆分后每一个Rails应用具有如下特征:

\
  • 有独立的数据库,可以独立运行;\
  • 程序代码量比较小,一般情况下只需要一到两个程序员开发与维护;\
  • 高内聚、低耦合。\

系统进行拆分后,需要解决一系列关键的问题,例如:如何保持用户体验的一致性、应用之间如何交互、如何共享用户等。下面将逐一针对这些问题介绍解决方案。

\

用户体验一致性

\

系统进行拆分后,由若干个轻量级应用共同协作来完成某项业务操作。由于每一个应用都是独立的Rails程序,而一个较为复杂的业务流程可能要在多个应用间跳转,所以首先要解决用户体验的一致性问题。

\
统一的css框架
\

用户体验最直观的方面就是页面的样式。为了保证用户在不同的程序间跳转时没有突兀的感觉,每个应用看起来都应该“长的一样”。为达到这一目的,我们采用统一的css框架来控制样式。

\

在layout里面调用Helper方法:

\
\\u0026lt;%= idp_include_js_css %\u0026gt;
\

这将产生以下html代码:

\
\\u0026lt;script src =\"/assets/javascripts/frame.js\" type=\"text/javascript\"\u0026gt;\u0026lt;/script\u0026gt;\\u0026lt;link href=\"/assets/stylesheets/frame.css\" media=\"screen\" rel=\"stylesheet\" type=\"text/css\" /\u0026gt;\
\

在frame.css中,会设定好html标签以及如导航等常用结构的样式,应用中的页面只要使用定义好的标签及css类,就可以实现统一风格的界面。

\
通用客户端组件
\

拆分后的Rails应用虽然处理的业务逻辑各不相同,但在用户交互上有很多相似的元素,例如查询表单、日历形式显示信息等。把产生这些元素的代码抽象成通用的方法,不仅可以保持用户体验的一致性,更可以减少代码重复,提高开发效率。

\

例如要产生如下图所示的查询表单,只需要指定待查询的数据库字段以及必要的查询参数即可,具体的实现逻辑封装在search_form_for这个Helper方法中。

\
\\u0026lt;%= search_form_for(Course, :id, [:price, {:range=\u0026gt;true}], :published, [:created_at, {:ampm=\u0026gt;true}]) %\u0026gt;\
\

50cee496abc62b09885a8a33befb3446.png

\

数据共享与交互

\

由于每个Rails应用都是整个业务系统的一部分,除了保证用户体验的一致性外,还需要解决程序间的数据交互与共享问题。下面我们以一个简单的例子来说明如何实现。

\
示例程序
\

示例程序是一个简单的在线学习系统,用户在线购买和学习课程。按照业务逻辑将系统拆分成四个应用,分别用于课程信息管理(course)、用户注册登录及帐号管理(user)、订单系统(purchase)以及在线学习系统(learning)。这几个Rails应用中各自业务的实现比较简单,不再赘述。

\

8de2cf711eafd42b206882c737f486a2.png

\
只读数据库
\

在示例程序中,用户需要购买课程后才能开始学习。由于我们对系统进行了拆分,订单和课程在两个不同的应用中进行管理,而用户下定单时需要查看课程列表,这就涉及到一个应用(purchase)如何获取另一个应用(course)数据的问题。

\

最直观的做法是course提供一个service,purchase调用这个service来取得课程列表。但service调用效率比较低,代码处理也比较复杂,所以应该尽量避免使用。我们仔细分析一下这个需求就会发现,在purchase显示的课程列表逻辑上很简单,只需要知道课程的名称、价格等基本属性就足够了,所以可以考虑直接从数据库读取这些信息。

\

由于系统拆分后每个应用都有独立的数据库,所以我们需要给purchase中的Model类设定指向course的数据库连接。代码如下:

\
\# purchase: /app/models/course_package.rb:\class CoursePackage \u0026lt; ActiveRecord::Base \  acts_as_readonly :course \end \
\

这样CoursePackage除了数据库指向不同外,其他和普通的Model一样。

\
\# purchase: /app/views/orders/new.erb.html\\u0026lt;ul\u0026gt;\\u0026lt;% CoursePackage.all.each do |package| %\u0026gt;\    \u0026lt;li\u0026gt;\u0026lt;%= package.title %\u0026gt; \u0026lt;%= package.price %\u0026gt;\u0026lt;/li\u0026gt;\\u0026lt;% end %\u0026gt;\\u0026lt;/ul\u0026gt;\
\

通过acts_as_readonly这个方法,可以让purchase的类CoursePackage从course数据库中读取数据。需要注意的是,为了保证数据维护的一致性,CoursePackage的数据库连接是只读的,这样可以避免数据在多个应用中被修改。

\

acts_as_readonly的核心实现如下(限于篇幅,设置连接为只读等代码并未列出):

\
\def acts_as_readonly(app_name, options = {})\  config = CoreService.app(app_name).database\  establish_connection(config[Rails.env])\  set_table_name(self.connection.current_database + \".\" + table_name)\end\
\

app_name是每个应用在集群中的唯一标识。purchase通过CoreService来获取course的数据库配置并设置连接。那么,CoreService向什么地方发送请求,又是如何知道course的配置信息呢?

\

在应用集群中,为了降低应用间的耦合性,我们采用集中式的配置管理。选择某一个应用作为core,其他应用在server启动时将自己的配置信息发送到core集中存储。例如我们在示例程序中选择user做为core应用,purchase需要查询course的配置时,就通过CoreService向user发送请求,user根据名称查询出course包括数据库在内的所有配置信息,并返回给purchase。交互过程如下图所示:

\

3dab5be849ed09a5720e7eae34a4174e.png

\

采用集中式的配置管理后,应用之间的调用都通过core来进行,这样就把应用之间的交互由网状结构变成以core为中心的星型结构,降低了系统配置管理的复杂度。

\

应用程序的配置信息保存在config/app_config.yml中,示例如下:

\
\app: course #名称,应用在系统中的唯一标识\url: example.com/course #url\api:\  course_list: package/courses\
\

从上文可以看出,通过只读数据库,我们可以完全无缝地读取其他应用的数据,并且代码非常简单明了,并没有增加应用间的耦合性。

\

只读数据库适应于业务逻辑比较简单的数据读取,如果数据需要预先进行复杂的操作,就无法简单地通过只读数据库取得数据。另一方面,应用间有时候确实需要进行一些写操作,这时候就需要借助于其他手段了。

\
Web Service
\

示例程序中,用户在purchase成功购买课程后,需要在learning这个应用中激活课程。这个过程可以通过Web Service来实现,由learning提供service接口,purchase调用这个接口并传递必要的参数。

\

Rails程序一般通过ActiveResource来简化service的开发,learning中提供服务的Controller代码示例如下:

\
\# learning: app/controllers/roadmap_services_Controller.rb\def create\  Roadmap.generate(params[:roadmap_service])\end\
\

purchase通过RoadmapService来调用learning的service接口。

\
\# purchase: app/models/roadmap_service.rb \class RoadmapService \u0026lt; ActiveResource::Base\  self.site = :learning\end\\RoadmapService.create(params)\
\

我们对ActiveResource::Base类的site=方法进行了扩展,这样只需要指定提供service的应用名称(learning)就可以找到service的url。实现的原理仍然是通过向core发送请求,查询应用的url。

\
DRY
\

以上介绍了如何保持用户体验的一致性以及应用间如何交互,我们可以看到这些功能的实现方法与应用的业务逻辑并不相关,属于“框架支持代码”,所以为了避免代码重复并且进一步简化开发,我们把这些方法封装到gem里面,这样每个Rails应用只需要引用这个gem,就可以无缝地集成到框架中来,并且可以使用gem里面包装好的一系列方法。

\

我们已经将数据共享部分的核心代码开源,文中一些省略的代码(如acts_as_readonly)也可以在此处找到,具体可参见http://github.com/idapted/eco_apps

\

用户及权限控制

\

除了数据交互外,另一个重要问题是用户的管理,包括系统登录、权限控制等方面。示例程序中,我们用user这个应用来管理用户信息。

\
单点登录
\

在应用集群中,用户登录某一个应用后,再访问其他应用时应该不需要再次验证,这就需要实现多个应用间的单点登录。

\

实现单点登录有很多方法,我们采取一种非常简单的方式,就是多个应用共享session。代码如下:

\
\# config/initializers/idp_initializer.rb \ActionController::Base.session_store = :active_record_store \ActiveRecord::SessionStore::Session.acts_as_remote :user, :readonly =\u0026gt; false\
\

在initializer中指定所有应用都用user的sessions表存储session数据。当然也可以使用其他session存储方式,例如memcache等,只要保证所有应用的设置都一样即可。

\
权限控制
\

我们采用基于角色的权限管理来控制对应用程序的访问,并且在core应用中集中管理。应用中每一个Controller作为一个权限控制节点,在server启动时,像配置信息一样,各个应用将自己的Controller结构发送到core,由core统一管理与配置。如下图所示:

\

87e85ce7d90da1d24c74bd7e013ec546.png

\

从示例程序可以看出,将大系统拆分成小应用是基于业务来进行的,每一个应用处理一套功能上接近的、完整的业务逻辑。而每一个小应用Controller的结构,对于有多种角色的系统来说,应该按照角色来组织,这样可以有比较清晰的结构。Controller做为节点的方案也在一定程度上强迫开发者按照角色设计良好的Controller结构。

\
辅助系统
\

除了统一的UI、数据交互和用户共享外,还可以把一些常用的功能如上传附件、发送邮件等抽象出来,在更高级别上减少重复代码。

\

由于这些功能比较复杂,不像UI Helper等用简单的一两个方法就可以完成,所以我们用独立应用和对应的gem相结合来实现。

\

以发送邮件功能为例,首先创建一个Rails应用mail,主要功能包括管理邮件模版、统计发送数量、完成邮件发送等;然后创建一个gem,在这个gem中包含MailService,其他应用引用这个gem后就可以调用MailService的相关方法完成邮件发送。例如:

\
\MailService.send(:welcome_mail, \"customer@example.com\
资源下载链接为: https://pan.quark.cn/s/9e7ef05254f8 行列式是线性代数的核心概念,在求解线性方程组、分析矩阵特性以及几何计算中都极为关键。本教程将讲解如何用C++实现行列式的计算,重点在于如何输出分数形式的结果。 行列式定义如下:对于n阶方阵A=(a_ij),其行列式由主对角线元素的乘积,按行或列的奇偶性赋予正负号后求和得到,记作det(A)。例如,2×2矩阵的行列式为det(A)=a11×a22-a12×a21,而更高阶矩阵的行列式可通过Laplace展开或Sarrus规则递归计算。 在C++中实现行列式计算时,首先需定义矩阵类或结构体,用二维数组存储矩阵元素,并实现初始化、加法、乘法、转置等操作。为支持分数形式输出,需引入分数类,包含分子和分母两个整数,并提供与整数、浮点数的转换以及加、减、乘、除等运算。C++中可借助std::pair表示分数,或自定义结构体并重载运算符。 计算行列式的函数实现上,3×3及以下矩阵可直接按定义计算,更大矩阵可采用Laplace展开或高斯 - 约旦消元法。Laplace展开是沿某行或列展开,将矩阵分解为多个小矩阵的行列式乘积,再递归计算。在处理分数输出时,需注意避免无限循环和除零错误,如在分数运算前先约简,确保分子分母互质,且所有计算基于整数进行,最后再转为浮点数,以避免浮点数误差。 为提升代码可读性和可维护性,建议采用面向对象编程,将矩阵类和分数类封装,每个类有明确功能和接口,便于后续扩展如矩阵求逆、计算特征值等功能。 总结C++实现行列式计算的关键步骤:一是定义矩阵类和分数类;二是实现矩阵基本操作;三是设计行列式计算函数;四是用分数类处理精确计算;五是编写测试用例验证程序正确性。通过这些步骤,可构建一个高效准确的行列式计算程序,支持分数形式计算,为C++编程和线性代数应用奠定基础。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值