`

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

 
阅读更多

转:http://www.infoq.com/cn/articles/rails-app-refactoring/

 

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方法:

 

<%= idp_include_js_css %>

 这将产生以下html代码:

<script src ="/assets/javascripts/frame.js" type="text/javascript"></script>
<link href="/assets/stylesheets/frame.css" media="screen" rel="stylesheet" 
type="text/css" />

 

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

通用客户端组件

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

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

 

<%= search_form_for(Course, :id, [:price, {:range=>true}], :published, 
[:created_at, {:ampm=>true}]) %>

 

 

数据共享与交互

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

示例程序

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

 



 

只读数据库

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

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

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

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

 

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

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

 
 

通过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。交互过程如下 图所示:

 



 

采用集中式的配置管理后,应用之间的调用都通过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 < 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 => false

 

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

权限控制

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

 



 

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

辅助系统

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

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

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

MailService.send(:welcome_mail, "customer@example.com", :username=>"张三")

 

架构优势及系统拆分原则

小应用集群架构的优势

我们在第一部分已经详细说明了复杂Rails系统的种种弊端,将大系统拆分成小应用集群后,可以从根本上解决这些问题,并且还可以带来许多其他好处。

快速需求响应

由于每个应用只关注于大系统中的一部分业务逻辑,所以应用代码量一般都比较小。这种小规模的应用是非常容易维护与扩展的。

首先小应用代码比较清晰,无论是BUG修复还是功能扩展或者代码重构都很容易进行。其次测试和部署的周期更短,并且由于各个应用间彼此独立,某个应用的崩溃不会影响到其他应用的正常运行。更快的开发部署周期就保证了系统对于用户需求的快速响应,有利于提高产品的竞争力。

新技术的安全迭代

Ruby/Rails技术社区十分活跃,不仅Rails的版本升级速度很快,各种gem、插件、Web Server等也层出不穷。为了不断提高系统的性能、架构与可扩展性,我们经常需要对Rails进行升级或者引进一些新技术。但对于复杂的Rails系 统,升级Rails是一件十分痛苦的事情,由于代码太复杂或没有完善的测试代码,升级后往往会破坏系统的健壮性。另外,引入的新技术可能存在缺陷,运行一 段时期发现问题后又需要退回初始版本,这个反复的过程严重降低了生产效率。 

而将系统进行拆分后,在升级Rails或应用新技术时,可以拿某一个不是特别重要的应用做实验。通过实验总结出一些升级经验,或验证新技术是否可用,没有问题后再推广到其他应用,这样就可以逐步地、安全地完成新技术的更新。

团队建设

对于复杂的Rails系统,团队有新成员加入时,会由于经验、技术水平不足等无法接触系统,往往需经过较长时间的培训才可以真正进入开发。另外由于 系统的复杂度及新手对系统整体缺乏了解,很容易在修改代码后影响系统其他部分。而对于小应用集群,可以让新手从较简单、非核心的系统着手,对技术、业务较 为熟悉后再转移到其他应用,这样就可以迅速、平滑地融入团队。

由于轻量级应用维护、扩展都比较简单,一般一个开发者可以负责多个应用,这样开发者就有了很大的自主性。另外如果有错误也不会影响他人,降低了风险,更有利于团队合作。在这样简单、安全、快速迭代的开发环境中,开发者的工作积极性会更高。

小应用集群架构的缺陷

由于多个应用之间的交互很大一部分是通过只读数据库来进行的,而数据库难以做到远程连接,所以要求所有的应用都部署在同一个局域网上。对于要求有大 量服务器并且分布于多个数据中心(甚至多个国家)的情况,就不能完全依赖于该架构实现。当然广域网上的分布式应用需要考虑更多的问题,超出了本文的讨论范 围,这里不再赘述。

另外由于每个应用都依赖于做为基础库的gem,所以gem里面的方法更改后需要重新启动所有相关的应用,方法才能生效。gem方法一旦出错,会牵连 到多个应用,所以对gem方法的更改一定要慎重,并且要经过充分的测试确保没有问题后再进行部署。为了降低风险,最好有一个gem方法的引用列表,这样可 以确保gem方法升级后不会带来潜在风险。

系统划分原则

系统划分是否合理直接关系到代码和架构的质量。一般来说,拆分系统要遵守以下几个原则:

  • 每个应用独立完成一个完整的业务逻辑。例如在示例程序中,course这个应用应该完成所有的课程管理操作,包括课程的基本信息维护、课 程包的管理等。如果把课程的基本信息维护和课程包维护这两个联系十分紧密的业务功能放在两个应用中,就会造成这两个应用间频繁的交互,导致系统性能和代码 质量的下降。
  • 按照业务而不是按照用户角色来拆分系统。
  • 高内聚。核心业务处理不应该通过service来处理,一个应用中最关键的业务逻辑应该不依赖于其他应用,完全可以自我实现。
  • 低耦合。一个应用不应该知道太多其他应用的业务逻辑,程序之间的接口越小越好。

系统拆分的好坏并没有可以量化的标准,很多时候需要经过多次迭代才能达到比较合理的划分。在开始的时候可以大胆一些,将系统拆分成尽可能多的应用,然后在开发过程中如果发现应用间交互很频繁,则可以将其合并为一个应用。

小结

以上介绍了如何把一个复杂的Rails系统拆分成多个轻量级应用集群,这个框架结构在idapted一年多的实践来看效果还是很令人满意的。以idapted的在线英语学习平台eqenglish为 例,现在大约有15个业务系统,4个支撑应用,核心应用每个约有40个左右的Model。每周有1-2次重大更新部署,每天有30-40次代码签入和 3-5次BUG修复或功能改进更新。由于框架提供了很多基础设施,搭建一个新应用的时间也大大缩短,例如用于跟踪学生学习状态的LPR(Learner Progress Report)一个人两周即开发完成并上线。

当然框架还有很多需要完善的地方,例如应用间的写交互通过消息队列来实现、如何做集成测试等,希望能得到Ruby/Rails社区更多的反馈与意见。

 

 

  • 大小: 23.7 KB
  • 大小: 59.7 KB
  • 大小: 30.9 KB
  • 大小: 109.2 KB
分享到:
评论

相关推荐

    重构你的Rails程式码

    在进行Rails版本升级时,重构代码是一个绝佳的机会。本文将深入探讨为何这是个好时机来进行重构,并提供一系列实用的方法帮助您优化代码。 #### 为什么这是一个好时机重构代码? 1. **信任度提升:** 如果您有足够...

    Rails的精简版本Rails::API.zip

    Rails::API 移除了 ActionView 和其他一些渲染功能,不关心Web前端的开发者可更容易、快速地开发应用程序,因此运行速度比正常的 Rails 应用程序要快。 Rails::API 可以用来创建只提供API服务(API-Only)的 Rails ...

    rails_multisite:Rails应用的多租户

    Rails多站点这个gem为Rails应用程序提供了多数据库支持。 使用其中间件,您可以对应用程序进行分区,以便每个主机名都有自己的数据库。 它提供了一系列用于处理多个数据库的帮助程序,以及一些用于处理这些数据库的...

    ruby on rails在线考试系统

    在这个“ruby on rails在线考试系统”中,我们可以探讨以下几个核心知识点: 1. MVC架构:Rails的核心设计原则是DRY(Don't Repeat Yourself),通过MVC模式将应用程序分为模型、视图和控制器三个部分,各司其职。...

    Ruby on Rails教程:学习使用Rails进行Web开发Ruby on Rails Tutorial: Learn Web Development with Rails

    本书教您如何使用Ruby on Rails开发和部署真正的,具有工业实力的Web应用程序,Ruby on Rails是为诸如Twitter,Hulu,GitHub和Yellow Pages等顶级网站提供支持的开源Web框架。

    react-rails-chat:react-rails 示例应用程序

    自述 此自述文件通常会记录启动和运行应用程序所需的任何步骤。 您可能想要涵盖的内容: Ruby版系统依赖配置数据库创建数据库初始化如何运行测试套件服务(作业队列、缓存服务器、搜索引擎等) 部署说明… 如果您不...

    操作系统集群运维工具:从 Foreman+katello 到麒麟升级管理平台

    操作系统集群运维工具是IT基础设施管理的关键组成部分,它们帮助管理员高效地管理和更新分布在多个物理和虚拟环境中的操作系统。本文主要探讨了从Foreman+Katello到麒麟升级管理平台的转变,这两种工具都是为了优化...

    Ruby on Rails Guides v2 - Ruby on Rails 4.2.5

    - **示例**:例如,在博客系统中,一篇文章可以有多个评论,这就是典型的“一对多”关系。 #### 四、查询 - **方法**:使用Active Record的方法来进行数据库查询,如`find`、`where`、`joins`等。 - **优化**:...

    Rails 中mongrel的安装

    Rails是Ruby on Rails框架的简称,它是一种基于Ruby语言的开源Web开发框架,以其MVC(Model-View-Controller)架构而闻名,旨在简化Web应用的开发过程。在早期的Rails版本中,Mongrel是一个常用的HTTP服务器,用于...

    Ruby-GoOnRails使用Rails生成器来生成一个Golang应用

    通过这种方式,你可以充分利用两种语言的优势,为复杂的Web应用构建高效且易于维护的后端架构。 总的来说,`GoOnRails`是一个创新的工具,它让Rails开发者能够轻松地在Rails项目中引入Go代码,实现高性能API服务。...

    配置高可用的rails

    优化rails应用的响应时间可以从数据库查询优化、视图渲染优化、静态资源服务优化等多方面着手。 - Web框架基准测试:了解不同Web框架的性能基准,可以帮助我们选择合适的工具和优化手段。基准测试能够提供不同框架...

    Ruby+on+Rails快速Web应用开发实战.pdf

    它涵盖了从Rails的历史和版本发展到如何在不同操作系统上安装Rails,再到如何使用Rails进行Web应用的开发。此外,文档还包含了数据库的配置、MVC架构模式的应用,以及如何处理数据、实现CRUD操作和Web服务的相关知识...

    rails_version:用于 https 的 Rails gem

    安装像往常一样,将此行添加到您的应用程序的 Gemfile 并在之后打包: gem 'rails_version', "&gt;= 0.2.2", :group =&gt; :production从注册并获取您的 API 密钥,然后转到您的 config/environments/production.rb 文件并...

    rails-routes:在Rails应用程序上启用configroutes * .rb

    您的Rails应用程序上的路线太多? 我也是! 这个gem可让您将路线分隔在不同的文件上。 只需在config/routes/*.rb创建它们就可以了。 安装 将此行添加到您的应用程序的Gemfile中: gem "rails-routes" 然后执行: ...

    Rails 101S

    - **快速体验Ruby on Rails**:通过创建一个简单的应用来快速上手Ruby on Rails的基本工作流程。 - **创建项目**:使用`rails new`命令创建一个新的Rails项目。 - **配置Git**:设置版本控制系统,确保代码变更...

    Ruby on Rails入门例子

    - **路由(Routes)**:Rails的路由系统将URL映射到特定的控制器动作,定义了应用的导航结构。在`config/routes.rb`文件中配置路由规则。 - **生成器(Generators)**:Rails提供了强大的生成器工具,可以自动生成...

    rails敏捷开发的购物车系统

    在本文中,我们将深入探讨如何使用Rails敏捷开发技术构建一个购物车系统,特别是在参考《rails敏捷开发第四版》中的示例。Rails 3.2.6是本文的基础框架,它是一个强大的Ruby Web应用程序框架,以其MVC(模型-视图-...

    rails-apz:从a到z构建Rails应用程序的指南

    从a到z构建Rails 4.2应用程序的指南。 现场演示 分步教程 屏幕截图 截屏 (仅葡萄牙语) 包括 Rails 4.2 Ajax与UJS 带BootstrapCSS 简单的模式 动作邮件 Sidekiq和Redis的积极工作 测验 与Code Climate和Travis...

    应用Rails进行敏捷Web开发中文第三版

    《应用Rails进行敏捷Web开发》中文第三版是针对Ruby on Rails框架的一本详尽指南,主要聚焦于Rails 2.2.2版本。Ruby on Rails(简称Rails)是一款基于Ruby编程语言的开源Web应用程序框架,它遵循“Don't Repeat ...

    backbone-rails-demo:演示 rails-backbone gem 用法的示例应用程序

    创建一个名为 blog 的新 Rails 应用程序。 rails new blog 编辑您的 Gemfile 并添加 gem 'rails-backbone' 安装 gem 并生成脚手架。 bundle install rails g backbone:install rails g scaffold Post title:...

Global site tag (gtag.js) - Google Analytics