- 浏览: 237600 次
- 性别:
- 来自: 北京
文章分类
最新评论
-
DerekZhao:
这个工具后来还有更新吗?
发布了一个基于Javascript的html内容提取器 -
lubojian:
还是不行啊
codeigniter如何支持中文URL? -
华仔2548:
我刚才又重新下载了下ndk 发现toolchains/arm- ...
ndk-build 报 Fatal error: invalid -march= option: `armv5te' 错误的解决办法 -
华仔2548:
我看到这个方法 在下面这个网页http://stackove ...
ndk-build 报 Fatal error: invalid -march= option: `armv5te' 错误的解决办法 -
坦克狗:
必须通过命令来启动模拟器.emulator -avd 模拟器名 ...
用busybox扩展android系统的控制台命令
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:
- 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
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:
- 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
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:
- class Customer < ActiveResource::Base
- self.site = "http://localhost:3000/"
- 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:
- >> 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)
- => []
>> 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:
- 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
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.
- class Customer < ActiveResource::Base
- self.site = "http://localhost:3000/"
- self.user = "api"
- self.password = "kittens"
- 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:
- 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
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:
- >> ServiceLocator[:crm]
- => "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:
- class Customer < Service(:crm)
- # my custom methods here.
- 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
发表评论
-
windows 下安装 RMagick的关键步骤
2012-10-24 11:34 710set CPATH=C:\Program Files\I ... -
windows下无法安装systemTimer
2011-05-20 23:05 1440可以安装 glazel-SystemTimer试试 g ... -
Rails中的事务
2011-03-20 21:34 267T2.transaction do ... -
windows 下安装 hpricot
2010-12-01 18:05 988gem install hpricot --platfo ... -
Ruby中浮点数转换问题的解决办法
2010-10-28 12:28 1633在ruby中输入 puts (10.12 * 100) ... -
Watir 1.6.5中文支持问题
2010-08-19 13:55 862修改 c:\ruby\lib\ruby\gems\1.8\ge ... -
google api 403 错误的解决办法
2010-07-26 00:39 2511最近发现使用google api一直出现403错误,这是个认证 ... -
rails中使用url传递sessionid
2010-07-16 17:45 1629Enabling url parameter ... -
centos 下 sqlite-ruby 安装
2010-07-14 19:59 1050yum install sqlite-devel ... -
Rails中的memcached过期的两个小问题
2010-03-21 18:25 1012在项目用了memcached存储session,还用了 ex ... -
使用magick 遇到 "convert: Non-conforming drawing primitive definition `text'"错误的解决办法
2010-01-31 17:04 2646将 -draw 'text 5,21 "ABCDEF ... -
如何在迁移中使用char类型
2009-11-17 10:58 808t.column :code, "char(2)& ... -
rails sqlserver adapter非UTC时区问题解决办法
2009-11-11 11:36 1211#config.time_zone = 'UTC' ... -
rails操作sql server乱码问题的解决办法
2009-10-01 20:41 1209class String require 'iconv' ... -
rails 的两个小技巧
2009-06-17 17:41 10421.可以使用alias_attribute 给model的字段 ... -
undefined method `use_transactional_fixtures=' 错误
2009-06-07 18:13 980用rails 2.3.2编写单元测试的时候遇到 undef ... -
rails2.3.2 在windows下render :file的一个bug
2009-05-20 17:20 1007在windows上render :file时使用绝对地址的时候 ... -
render page without layout for ajax request
2009-05-07 17:05 893在ApplicationController中加入以下代码 ... -
convert hash to object with OpenStruct
2009-04-07 11:39 996require 'ostruct' class Object ... -
在Rails中实现Layout的嵌套
2008-03-05 18:09 2557需求: 一个页面是用了layout/application. ...
相关推荐
rails for zombies 课件
For a .NET developer, learning Rails is as much about the cultural and philosophical shifts in thinking as it is about the technical learning curve. In this book, we hope to break down some of these ...
Rails for Zombies. This is a tutorial of ruby on rails application.
Pragmatic.Bookshelf.Rails.for.PHP.Developers
Bootstrap 3 和 Rails 4(样例用的是Ruby 2.1.1,Rails 4.1.4) Table of Contents Preface 1 Chapter 1: Introducing Web Application Development in Rails 7 Why Bootstrap with Rails? 8 Setting up a Todo ...
Ruby for Rails 英文原版, pdf格式 <br>本书是一部专门为Rails实践而写的经典Ruby著作,由四部分组成,共17章。第一部分讲述Ruby和Rails的编程环境。第二部分和第三部分与 Rails紧密联系,着重对Ruby这门语言...
Rails for Java Developers
ruby on rails for eclipse开发插件
这是Agile Web Development with Rails for Rails 3.2, 为3.2的版本修改过的
NULL 博文链接:https://qianjigui.iteye.com/blog/250876
适合初学者的ruby教程
从官网上下载的最新的rails4.0.3开发教材。不足之处是mobi版的,需要kindle阅读器,好在这个阅读器也是免费的。
[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 的基本知识,...安装 ruby ,rails...mysql 数据库...用 rails 实现的是一个简单的网上书籍管理程序 ...
weixin_rails_middleware, 微信集成 ruby weixin_rails_middleware for integration weixin.
Ruby On Rails For Dummies
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 ...
Packt.Publishing.Aptana.Radrails.An.Ide.for.Rails.Development.May.2008.pdf
You'll be prepared for real-world application deployment, and we'll give you a taste of how Ruby and Rails are reshaping the Microsoft application landscape, including a look at IronRuby.