`

Rails Modularity for Lazy Bastards

    博客分类:
  • Ruby
阅读更多


Rails Modularity for Lazy Bastards

2009-04-16 04:31, written by Gregory Brown

When we develop standalone systems or work on libraries and frameworks, modularity seems to come naturally. When something seems to gain a life of its own, we pull it off into its own package or subsystem to keep things clean. This is a natural extension of the sorts of design decisions we make at the object level in our software, and helps us work on complicated problems, often working alongside other hackers, without losing our wits.

It seems a bit surprising that these helpful guiding principals can often evaporate when we bring our software to the web. Sure, we may make use of plugins and gems for foundational support, but if you take a good look at a Rails application that has more than 50 models or so, you’ll be hard pressed to find a unified purpose behind all that logic.

However, if you try to break things down into core sets of functionality, you may find that certain vertical slices can be made that allow you to break out bits of functionality into their own separate focus areas. For example, you may discover that part of your application is actually a mini CRM system. Or maybe you’ve snuck in a small reporting system without noticing it consciously. The list goes on, but the underlying idea here is that the larger a system gets, the harder it is to define its core purpose.

While splitting out these subsystems may seem like the obvious choice in a standalone application, there seems to be a certain amount of FUD about this strategy when it comes to the web. This probably originates from a time before REST, in which interaction between web applications was overly complex,making the costs of fragmentation higher than the benefits of a modular architecture. These days, we live in better times and work with better frameworks, and should reap the benefits that come along with it.

But actions speak louder than words, so let’s take a look at some of the underlying tech and how to use it. Building on the CRM scenario, I’ll start by showing how to access a Customer model from another application using ActiveResource.

Sharing Model Data via ActiveResource

Suppose that we’ve got a CRM system that has a Customer model that has a schema that looks something like this:

  1. create_table :customers do |t|  
  2.   t.string :first_name  
  3.   t.string :last_name  
  4.   t.string :email  
  5.   t.string :daytime_phone  
  6.   t.string :evening_phone  
  7.   t.timestamps  
  8. end  
  create_table :customers do |t|
    t.string :first_name
    t.string :last_name
    t.string :email
    t.string :daytime_phone
    t.string :evening_phone
    t.timestamps
  end

With this, we can do all the ordinary CRUD operations within our application, so I won’t bore you with the details. What we’re interested in is how to accomplish these same goals from an external application. So within our CRM system, this essentially boils down to simply providing a RESTful interface to our Customer resource. After adding map.resources :customers to our config/routes.rb file, we code up a CustomersController that looks something like this:

  1. class CustomersController < ApplicationController  
  2.   
  3.   def index  
  4.     @customers = Customer.find(:all)  
  5.     respond_to do |format|  
  6.       format.xml { render :xml => @customers }  
  7.       format.html  
  8.     end  
  9.   end  
  10.   
  11.   def show  
  12.     customer = Customer.find(params[:id])  
  13.     respond_to do |format|  
  14.       format.xml { render :xml => customer.to_xml }  
  15.       format.html  
  16.     end  
  17.   end  
  18.   
  19.   def create  
  20.     customer = Customer.create(params[:customer])  
  21.     respond_to do |format|  
  22.       format.html { redirect_to entry }  
  23.       format.xml { render :xml => customer, :status   => :created,   
  24.                                             :location => customer }  
  25.     end  
  26.   end  
  27.   
  28.   def update  
  29.     customer = Customer.update(params[:id], params[:customer])  
  30.     respond_to do |format|  
  31.       format.xml { render :xml => customer.to_xml }  
  32.       format.html  
  33.     end  
  34.   end  
  35.   
  36.   def destroy  
  37.     Customer.destroy(params[:id])  
  38.     respond_to do |format|  
  39.       format.xml { render :xml => "":status => 200 }  
  40.       format.html  
  41.     end  
  42.   end  
  43.   
  44. end  
  class CustomersController < ApplicationController
  
    def index
      @customers = Customer.find(:all)
      respond_to do |format|
        format.xml { render :xml => @customers }
        format.html
      end
    end
 
    def show
      customer = Customer.find(params[:id])
      respond_to do |format|
        format.xml { render :xml => customer.to_xml }
        format.html
      end
    end
 
    def create
      customer = Customer.create(params[:customer])
      respond_to do |format|
        format.html { redirect_to entry }
        format.xml { render :xml => customer, :status   => :created, 
                                              :location => customer }
      end
    end
 
    def update
      customer = Customer.update(params[:id], params[:customer])
      respond_to do |format|
        format.xml { render :xml => customer.to_xml }
        format.html
      end
    end
 
    def destroy
      Customer.destroy(params[:id])
      respond_to do |format|
        format.xml { render :xml => "", :status => 200 }
        format.html
      end
    end
 
  end

This may look familiar even if you haven’t worked with ActiveResource previously, as it’s basically the same boiler plate you’ll find in a lot of Rails documentation. In the respond_to block, format.xml is what matters here, as it is what connects our resource to the services which consume it. The good news is we won’t have to actually work with the XML data, as you’ll see in a moment.

While there are a few things left to do to make this code usable in a real application, we can already test basic interactions with a secondary application. Using any other rails app we’d like, we can add an ActiveResource model by creating a file called app/models/customer.rb and setting it up like this:

  1. class Customer < ActiveResource::Base  
  2.   self.site = "http://localhost:3000/"  
  3. end  
  class Customer < ActiveResource::Base
    self.site = "http://localhost:3000/"
  end

Now here comes the interesting part. If you fire up script/console on the client side application that is interfacing with the CRM system, you can see the same familiar CRUD operations, but taking place from a completely separate application:

  1. >> Customer.create(:first_name => "Gregory":last_name => "Brown")  
  2. => #<Customer:0x20d2120 @prefix_options={}, @attributes={"evening_phone"=>nil, "updated_at"=>Thu Apr 16 03:53:59 UTC 2009, "daytime_phone"=>nil, "id"=>1, "first_name"=>"Gregory", "last_name"=>"Brown", "created_at"=>Thu Apr 16 03:53:59 UTC 2009, "email"=>nil}>  
  3.   
  4. >> Customer.find(:all)  
  5. => [#<Customer:0x20a939c @prefix_options={}, @attributes={"evening_phone"=>nil, "updated_at"=>Thu Apr 16 03:53:59 UTC 2009, "daytime_phone"=>nil, "id"=>1, "first_name"=>"Gregory", "last_name"=>"Brown", "created_at"=>Thu Apr 16 03:53:59 UTC 2009, "email"=>nil}>]  
  6.   
  7. >> Customer.find(1).first_name  
  8. => "Gregory"  
  9.   
  10. >> Customer.delete(1)  
  11. => #<Net::HTTPOK 200 OK readbody=true>  
  12.   
  13. >> Customer.find(:all)  
  14. => []  
  >> Customer.create(:first_name => "Gregory", :last_name => "Brown")
  => #<Customer:0x20d2120 @prefix_options={}, @attributes={"evening_phone"=>nil, "updated_at"=>Thu Apr 16 03:53:59 UTC 2009, "daytime_phone"=>nil, "id"=>1, "first_name"=>"Gregory", "last_name"=>"Brown", "created_at"=>Thu Apr 16 03:53:59 UTC 2009, "email"=>nil}>
 
  >> Customer.find(:all)
  => [#<Customer:0x20a939c @prefix_options={}, @attributes={"evening_phone"=>nil, "updated_at"=>Thu Apr 16 03:53:59 UTC 2009, "daytime_phone"=>nil, "id"=>1, "first_name"=>"Gregory", "last_name"=>"Brown", "created_at"=>Thu Apr 16 03:53:59 UTC 2009, "email"=>nil}>]
 
  >> Customer.find(1).first_name
  => "Gregory"
 
  >> Customer.delete(1)
  => #<Net::HTTPOK 200 OK readbody=true>
 
  >> Customer.find(:all)
  => []

While the interface and behavior isn’t downright identical to ActiveRecord, it bears a striking resemblance and allows you to retain much of the functionality that is needed for basic data manipulation.

Now that we can see the basic functionality in action, let’s go back and fix a few key issues. We definitely want to add some sort of authentication to this system, as it is currently allowing any third party application to modify and destroy records. We also will most likely want a more flexible option for locating services than just hard coding a server address in each model file. Once these two things are in place, we’ll have the beginnings of a decentralized Rails based application.

API Keys with HTTP Basic Authentication

I want to preface this section by saying I’m typically not the one responsible for any sort of security hardening in the applications I work on. That means that I’m by no means an expert in how to make your applications safe from the malignant forces of the interweb. That having been said, what follows is a simple technique that seems to work for me when it comes to slapping a simple authentication model in place.

First, in the app that is providing the service, in this case, our fictional CRM system, you’ll want something like this in your ApplicationController:

  1. def basic_http_auth  
  2.   authenticated = false  
  3.   authenticate_with_http_basic do |login, password|  
  4.     if login == "api" && password == API_KEY  
  5.       authenticated = true  
  6.     end  
  7.   end  
  8.   
  9.   raise "Authentication failed" unless authenticated  
  10. end  
  def basic_http_auth
    authenticated = false
    authenticate_with_http_basic do |login, password|
      if login == "api" && password == API_KEY
        authenticated = true
      end
    end
 
    raise "Authentication failed" unless authenticated
  end

Here, API_KEY is some shared secret that is known by both your service providing application, and any client that wishes to use your service. In this blog post, I’ll be using the string “kittens”, but you’ll obviously want to pick something longer, and with significantly more entropy.

After dropping a before_filter in your CustomersController that points to basic_http_auth, you’ll need to update your ActiveResource model definition.

  1. class Customer < ActiveResource::Base  
  2.   self.site = "http://localhost:3000/"  
  3.   self.user = "api"  
  4.   self.password = "kittens"  
  5. end  
  class Customer < ActiveResource::Base
    self.site = "http://localhost:3000/"
    self.user = "api"
    self.password = "kittens"
  end

If you forget to do this, you won’t be able to retrieve or modify any of the customer data. This means that any application that does not know the shared secret may not use the resource. Although this is hardly a fancy solution, it gets the job done. Now, let’s take a look at how to make integration even easier and get rid of some of these hard coded values at the per-model level.

Simplifying Integration

So far, the work has been pretty simple, but it’s important to keep in mind that if we really want to break up our applications into small, manageable subsystems, we might need to deal with a lot of remote resources.

Pulled directly from some commercial work I’ve been doing with Brad Ediger of Madriska Media Group (and of Advanced Rails fame), what follows is a helper file that provides two useful features for working with remote resources via ActiveResource:

  1. require 'yaml'  
  2. require 'activeresource'  
  3.   
  4. class ServiceLocator  
  5.   
  6.   API_KEY = "kittens"  
  7.   
  8.   def self.services  
  9.     return @services if @services  
  10.     config_file = File.join(RAILS_ROOT, %w[config services.yml])  
  11.     config = YAML.load_file(config_file)  
  12.     @services = config[RAILS_ENV]  
  13.   end  
  14.   
  15.   def self.[](name)  
  16.     services[name.to_s]  
  17.   end  
  18. end  
  19.   
  20. def Service(name)  
  21.   Class.new(ActiveResource::Base) do  
  22.     self.site = "http://#{ServiceLocator[name]}"  
  23.     self.user = "api"  
  24.     self.password = ServiceLocator::API_KEY  
  25.   end  
  26. end  
  require 'yaml'
  require 'activeresource'
 
  class ServiceLocator
 
    API_KEY = "kittens"
 
    def self.services
      return @services if @services
      config_file = File.join(RAILS_ROOT, %w[config services.yml])
      config = YAML.load_file(config_file)
      @services = config[RAILS_ENV]
    end
 
    def self.[](name)
      services[name.to_s]
    end
  end
 
  def Service(name)
    Class.new(ActiveResource::Base) do
      self.site = "http://#{ServiceLocator[name]}"
      self.user = "api"
      self.password = ServiceLocator::API_KEY
    end
  end

The ServiceLocator part was Brad’s idea, and it represents a simple way to map the URLs of different services to a label based on what environment you are currently running in. A basic config/services.yml file might look something like this:

  development:
    crm: localhost:3000
    reports: localhost:3001

  production:
    crm: crm.example.com
    reports: reports.example.com

This is nice, because it allows us to configure the locations of our various services all in one place. The interface is very simple and straightforward:

  1. >> ServiceLocator[:crm]  
  2. => "localhost:3000"  
  >> ServiceLocator[:crm]
  => "localhost:3000"

However, upon seeing this feature, I decided to take it a step farther. Though it might sacrifice a bit of purity, the Service() method is actually a parameterized class constructor that builds up a subclass filling out the API key and service address for you. What that means is that you can replace your initial Customer definition with this:

  1. class Customer < Service(:crm)  
  2.   # my custom methods here.  
  3. end  
  class Customer < Service(:crm)
    # my custom methods here.
  end

Since Rails handles the mapping of resource names to class names for you, you can easily support as many remote classes from a single service as you’d like this way. When I read this aloud in my head, I tend to think of SomeClass < Service(:some_service) as “SomeClass is a resource provided by some_service”. Feel free to forego the magic here if this concerns you, but I personally find it pleasing to the eyes.

Just Starting a Conversation

I didn’t go into great detail about how to use the various technologies I’ve touched on here, but I’ve hopefully provided a glimpse into what is possible to those who are uninitiated, as well as provided some food for thought to those who already have some experience in building decentralized Rails apps.

To provide some extra insight into the approach I’ve been using on my latest project, we basically keep everything in one big git repository, with separate folders for each application. At the root, there is a shared/ folder in which we keep some shared library files, including some support infrastructure for things like a simple SSO mechanism and a database backed cross-application logging system. We also vendor one copy of Rails there and simply symlink vendor/rails in our individual apps, except for when we need a specific version for a particular service.

The overarching idea is that there is a foundational support library that our individual apps sit on top of, and that they communicate with each other only through the service interfaces we expose. We’ve obviously got more complicated needs, and thus finer grained controls than what I’ve covered in this article, but the basic ActiveResource functionality seems to be serving us well.

What I’d like to know is what other folks have been doing to help manage the complexity of their larger Rails apps. Do you think the rough sketch of ideas I’ve laid out here sounds promising? Do you foresee potential pitfalls that I haven’t considered? Leave a comment and let me know.

 

http://blog.rubybestpractices.com/posts/gregory/rails_modularity_1.html

分享到:
评论

相关推荐

    ruby on rails for dummies

    《Ruby on Rails for Dummies》是一本专门为初学者设计的Ruby on Rails教程,它旨在帮助新手快速理解并掌握这个强大的Web开发框架。Ruby on Rails(简称Rails)是基于Ruby编程语言构建的一个开源Web应用程序框架,它...

    Rails for Zombies

    Rails for Zombies是一份面向初学者的教程,通过学习本教程,用户能够掌握创建Ruby on Rails应用程序的基本知识。 Rails for Zombies教程中的"Deep in the CRUD"部分深入讲解了CRUD(创建Create、读取Retrieve、...

    Ruby+for+Rails

    **Ruby for Rails** Ruby是一种面向对象的动态编程语言,它以其简洁、优雅的语法和强大的元编程能力而闻名。在Web开发领域,Ruby与Rails框架的结合,即Ruby on Rails(RoR),开创了Web应用的新纪元。Ruby on Rails...

    Pragmatic.Bookshelf.Rails.for.PHP.Developers

    Pragmatic.Bookshelf.Rails.for.PHP.Developers

    Pragmatic.Bookshelf.Rails.for.Java.Developers.Feb.2007

    《Rails for Java Developers》是一本专为Java开发者撰写的书籍,旨在帮助他们快速掌握Ruby on Rails框架。本书由Stuart Halloway和Justin Etheredge合著,并于2007年2月出版。该书的目标读者是那些对Ruby on Rails...

    Ruby for Rails

    Ruby for Rails 英文原版, pdf格式 &lt;br&gt;本书是一部专门为Rails实践而写的经典Ruby著作,由四部分组成,共17章。第一部分讲述Ruby和Rails的编程环境。第二部分和第三部分与 Rails紧密联系,着重对Ruby这门语言...

    ruby on rails for eclipse开发插件

    ruby on rails for eclipse开发插件

    Ruby and Rails Development for Visual Studio

    NULL 博文链接:https://qianjigui.iteye.com/blog/250876

    Agile Web Development with Rails for Rails 3.2

    ### Agile Web Development with Rails for Rails 3.2 #### 核心知识点概览 - **Rails 3.2概述** - **敏捷开发方法论** - **Model-View-Controller (MVC) 模式** - **Ruby on Rails基础与高级特性** - **面向对象...

    ruby on rails guides for rails4.0.3(英文mobi版)

    从官网上下载的最新的rails4.0.3开发教材。不足之处是mobi版的,需要kindle阅读器,好在这个阅读器也是免费的。

    [Rails] Crafting Rails Applications (英文版)

    [Pragmatic Bookshelf] Crafting Rails Applications Expert Practices for Everyday Rails Development (E-Book) ☆ 图书概要:☆ Rails 3 is a huge step forward. You can now easily extend the framework, ...

    Ruby On Rails For Dummies

    ### Ruby on Rails For Dummies #### 核心知识点解析 **1. Ruby on Rails 概述** - **定义与特点**:Ruby on Rails(简称 Rails 或 RoR)是一种基于 Ruby 语言的开源 Web 应用框架,它采用了 Model-View-...

    Bootstrap for Rails

    Bootstrap for Rails is for Rails web developers who are struggling to get their website designs right. It will enable you to create beautiful responsive websites in minutes without writing much CSS ...

    weixin_rails_middleware, 微信集成 ruby weixin_rails_middleware for integration weixin..zip

    `weixin_rails_middleware` 是一个开源的 Ruby 框架中间件,设计用于帮助开发者轻松地在 Rails 应用程序中集成微信服务。这个中间件提供了与微信API交互的功能,包括验证微信服务器的请求、处理用户消息、以及发送...

    Rails 101 入门电子书

    ### Rails 101 入门电子书知识点详解 #### 一、简介 《Rails 101 入门电子书》是一本非常适合初学者直接入门的书籍,它由xdite编写并出版于2014年6月10日。本书主要针对的是希望学习Ruby on Rails框架的读者,特别...

    Rails项目源代码

    Ruby on Rails,通常简称为Rails,是一个基于Ruby编程语言的开源Web应用框架,遵循MVC(Model-View-Controller)架构模式。这个“Rails项目源代码”是一个使用Rails构建的图片分享网站的完整源代码,它揭示了如何...

    Rails101_by_rails4.0

    《Rails101_by_rails4.0》是一本专注于Rails 4.0.0版本和Ruby 2.0.0版本的自学教程书籍,它定位于中文读者,旨在成为学习Rails框架的参考教材。Rails(Ruby on Rails)是一个采用Ruby语言编写的开源Web应用框架,它...

Global site tag (gtag.js) - Google Analytics