SlideShare a Scribd company logo
Service-Oriented
Design and Implement
     with Rails 3
     ihower @ Ruby Tuesday
          2010/12/15
About Me
•              a.k.a. ihower
    •   http://ihower.tw

    •   http://twitter.com/ihower

• Rails Developer since 2006
• The Organizer of Ruby Taiwan Community
 • http://ruby.tw
 • http://rubyconf.tw
Agenda
•   What’s SOA
•   Why SOA
•   Considerations
•   The tool set overview
•   Service side implement
•   Client side implement
•   Library packaging
•   Caching
What’s SOA
           Service oriented architectures



• “monolithic” approach is not enough
• SOA is a way to design complex applications
  by splitting out major components into
  individual services and communicating via
  APIs.
• a service is a vertical slice of functionality:
  database, application code and caching layer
a monolithic web app example
                 request




             Load
            Balancer




            WebApps




            Database
a SOA example
                                     request




                                 Load
       request
                                Balancer



     WebApp                  WebApps
for Administration           for User




       Services A    Services B




        Database     Database
Why SOA? Isolation
• Shared Resources
• Encapsulation
• Scalability
• Interoperability
• Reuse
• Testability
• Reduce Local Complexity
Shared Resources
• Different front-web website use the same
  resource.
• SOA help you avoiding duplication databases
  and code.
• Why not only shared database?
 • code is not DRY                 WebApp
                              for Administration
                                                      WebApps
                                                      for User


 • caching will be problematic
                                               Database
Encapsulation
• you can change underly implementation in
  services without affect other parts of system
 • upgrade library
 • upgrade to Ruby 1.9
 • upgrade to Rails 3
• you can provide API versioning
Scalability1: Partitioned
     Data Provides
•   Database is the first bottleneck, a single DB server
    can not scale. SOA help you reduce database load
•   Anti-pattern: only split the database
    •   model relationship is broken                   WebApps



    •   referential integrity
    •   increase code complexity            Database
                                               A
                                                                 Database
                                                                    B


•   Myth: database replication can not help you speed
    and consistency
Scalability 2: Caching

• SOA help you design caching system easier
 • Cache data at the right place and expire
    at the right times
 • Cache logical model, not physical
 • You do not need cache view everywhere
Scalability 3: Efficient
• Different components have different task
  loading, SOA can scale by service.

                               WebApps



              Load
             Balancer                                 Load
                                                     Balancer




    Services A    Services A    Services B   Services B    Services B   Services B
Security

• Different services can be inside different
  firewall
  • You can only open public web and
    services, others are inside firewall.
Interoperability
• HTTP is the most common interface, SOA
  help you integrate them:
 • Multiple languages
 • Internal system e.g. Full-text searching engine
 • Legacy database, system
 • External vendors
Reuse

• Reuse across multiple applications
• Reuse for public APIs
• Example: Amazon Web Services (AWS)
Testability

• Isolate problem
• Mocking API calls
 • Reduce the time to run test suite
Reduce Local
         Complexity
• Team modularity along the same module
  splits as your software
• Understandability: The amount of code is
  minimized to a quantity understandable by
  a small team
• Source code control
Design considerations

• Partition into Separate Services
• API Design
• Which Protocol
How to partition into
 Separate Services
• Partitioning on Logical Function
• Partitioning on Read/Write Frequencies
• Partitioning on Minimizing Joins
• Partitioning on Iteration Speed
on Iteration Speed
• Which parts of the app have clear defined
  requirements and design?
• Identify the parts of the application which
  are unlikely to change.
• For example:
  The first version data storage is using
  MySQL, but may change to NoSQL in the
  future without affecting front-app.
on Logical Function

• Higher-level feature services
 • articles, photos, bookmarks...etc
• Low-level infrastructure services
 • a shared key-value store, queue system
On Read/Write
        Frequencies
• Ideally, a service will have to work only with
  a single data store
• High read and low write: the service should
  optimize a caching strategy.
• High write and low read: don’t bother with
  caching
On Join Frequency

• Minimize cross-service joins.
• But almost all data in an app is joined to
  something else.
  • How often particular joins occur? by read/
    write frequency and logical separation.
  • Replicate data across services
    (For example: a activity stream by using messaging)
API Design Guideline
• Send Everything you need
  •   Unlike OOP has lots of finely grained method calls


• Parallel HTTP requests
  •   for multiple service requests


• Send as Little as Possible
  •   Avoid expensive XML
Versioning
•   Be able run multiple versions in parallel:
    Clients have time to upgrade rather than having to
    upgrade both client and server in locks step.
•   Ideally, you won’t have to run multiple versions for
    very long
•   Two solutions:
    •   Including a Version in URIs
    •   Using Accept Headers for Versioning
        (disadvantage: HTTP caching)
Physical Models &
     Logical Models
• Physical models are mapped to database
  tables through ORM. (It’s 3NF)
• Logical models are mapped to your
  business problem. (External API use it)
• Logical models are mapped to physical
  models by you.
Logical Models
• Not relational or normalized
• Maintainability
  • can change with no change to data store
  • can stay the same while the data store
    changes
• Better fit for REST interfaces
• Better caching
Which Protocol?

• SOAP
• XML-RPC
• REST
RESTful Web services
• Rails way
• Easy to use and implement
• REST is about resources
 • URI
 • HTTP Verbs: GET/PUT/POST/DELETE
 • Representations: HTML, XML, JSON...etc
The tool set

• Web framework
• XML Parser
• JSON Parser
• HTTP Client
• Model library
Web framework

• Ruby on Rails, but we don’t need afull features.
  (Rails3 can be customized because it’s lot more
  modular. We will discuss it later)

• Sinatra: a lightweight framework
• Rack: a minimal Ruby webserver interface
  library
ActiveResource

• Mapping RESTful resources as models in a
  Rails application.
• Use XML by default
• But not useful in practice, why?
XML parser

• http://nokogiri.org/
• Nokogiri ( ) is an HTML, XML, SAX, and
  Reader parser. Among Nokogiri’s many
  features is the ability to search documents
  via XPath or CSS3 selectors.
JSON Parser

• http://github.com/brianmario/yajl-ruby/
• An extremely efficient streaming JSON
  parsing and encoding library. Ruby C
  bindings to Yajl
HTTP Client

• How to run requests in parallel?
 • Asynchronous I/O
   • Reactor pattern (EventMachine)
 • Multi-threading
   • JRuby
Typhoeus
        http://github.com/pauldix/typhoeus/



• A Ruby library with native C extensions to
  libcurl and libcurl-multi.
• Typhoeus runs HTTP requests in parallel
  while cleanly encapsulating handling logic
Typhoeus: Quick
                   example
response = Typhoeus::Request.get("http://www.pauldix.net")
response = Typhoeus::Request.head("http://www.pauldix.net")
response = Typhoeus::Request.put("http://localhost:3000/posts/1",
                                 :body => "whoo, a body")
response = Typhoeus::Request.post("http://localhost:3000/posts",
                  :params => {:title => "test post", :content => "this is my test"})
response = Typhoeus::Request.delete("http://localhost:3000/posts/1")
Hydra handles requests
 but not guaranteed to run in any particular order
      HYDRA = Typhoeus::HYDRA.new

      a = nil
      request1 = Typhoeus::Request.new("http://example1")
      request1.on_complete do |response|
          a = response.body
      end
      HYDRA.queue(request1)

      b = nil
      request2 = Typhoeus::Request.new("http://example1")
      request2.on_complete do |response|
          b = response.body
      end
      HYDRA.queue(request2)

      HYDRA.run # a, b are set from here
a asynchronous method
 def foo_asynchronously
     request = Typhoeus::Request.new( "http://example" )
     request.on_complete do |response|
        result_value = ActiveSupport::JSON.decode(response.body)
        # do something
        yield result_value
      end

       self.hydra.queue(request)
 end
Usage
result = nil
foo_asynchronously do |i|
    result = i
end

foo_asynchronously do |i|
    # Do something for i
end

HYDRA.run
# Now you can use result1 and result2
a synchronous method

  def foo
     result = nil
     foo_asynchronously { |i| result = i }
     self.hydra.run
     result
   end
Physical Models
        mapping to database directly




• ActiveRecord
• DataMapper
• MongoMapper, MongoId
Logical Models


• ActiveModel: an interface and modules can
  be integrated with ActionPack helpers.
• http://ihower.tw/blog/archives/4940
integrated with helper?

• For example:
 • link_to post_path(@post)
 • form_for @post
 • @post.errors
A basic model
class YourModel
    extend ActiveModel::Naming
    include ActiveModel::Conversion
    include ActiveModel::Validations

      def persisted?
          false
      end
end
without validations
 class YourModel
     extend ActiveModel::Naming
     include ActiveModel::Conversion

       def persisted?
           false
       end

       def valid?() true end

       def errors
           @errors ||= ActiveModel::Errors.new(self)
       end
 end
Many useful modules
•   MassAssignmentSecurity
•   Serialization
•   Callback
•   AttributeMethods
•   Dirty
•   Observing
•   Translation
Serializers
class Person

  include ActiveModel::Serializers::JSON
  include ActiveModel::Serializers::Xml

  attr_accessor :name

  def attributes
    @attributes ||= {'name' => 'nil'}
  end

end

person = Person.new
person.serializable_hash   #   =>   {"name"=>nil}
person.as_json             #   =>   {"name"=>nil}
person.to_json             #   =>   "{"name":null}"
person.to_xml              #   =>   "<?xml version="1.0" encoding="UTF-8"?>
n<serial-person...
Mass Assignment

class YourModel
    # ...
    def initialize(attributes = {})
        if attributes.present?
          attributes.each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") }
        end
      end
end

YourModel.new( :a => 1, :b => 2, :c => 3 )
MassAssignmentSecurity
class YourModel
    # ...
    include ActiveModel::MassAssignmentSecurity

    attr_accessible :first_name, :last_name

    def initialize(attributes = {})
        if attributes.present?
          sanitize_for_mass_assignment(attributes).each { |k, v| send("#{k}=", v) if
respond_to?("#{k}=") }
        end
    end
end
Scenario we want to
     implement
• an Users web service, which provide basic
  CRUD functions.
• an web application with the Users client
  library
Service implement
Customized Rails3

• We don’t need some components.
• We can customize ActionController
• Building a fast, lightweight REST service
  with Rails 3
  http://pivotallabs.com/users/jdean/blog/articles/1419-building-a-fast-
  lightweight-rest-service-with-rails-3
# config/appliction.rb

%w(
  active_record
  action_controller
  action_mailer
).each do |framework|
  begin
    require "#{framework}/railtie"
  rescue LoadError
  end
end
# config/application.rb
[
  Rack::Sendfile,
  ActionDispatch::Flash,
  ActionDispatch::Session::CookieStore,
  ActionDispatch::Cookies,
  ActionDispatch::BestStandardsSupport,
  Rack::MethodOverride,
  ActionDispatch::ShowExceptions,
  ActionDispatch::Static,
  ActionDispatch::RemoteIp,
  ActionDispatch::ParamsParser,
  Rack::Lock,
  ActionDispatch::Head
].each do |klass|
  config.middleware.delete klass
end

# config/environments/production.rb
config.middleware.delete
ActiveRecord::ConnectionAdapters::ConnectionManagement
# /app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
class ApplicationController < ActionController::Metal
  include AbstractController::Logger
  include Rails.application.routes.url_helpers
  include ActionController::UrlFor
  include ActionController::Rendering
  include ActionController::Renderers::All
  include ActionController::MimeResponds

  if Rails.env.test?
    include ActionController::Testing
    # Rails 2.x compatibility
    include ActionController::Compatibility
  end

end




                       http://ihower.tw/blog/archives/4561
APIs design best practices (1)
 • Routing doesn't need Rails resources
   mechanism , but APIs design should follow
   RESTful.
   (This is because we don't have view in service and we don't need URL
   helpers. So use resources mechanism is too overkill)


 • RESTful APIs is stateless, each APIs should
   be independent. So, requests which have
   dependency relationship should be
   combined into one API request. (atomic)
APIs design best practices (2)
 • The best format in most case is JSON.
   ( one disadvantage is we can’t return binary data directly. )


 • Use Yajl as parser.
   # config/application.rb
   ActiveSupport::JSON.backend = "Yajl"



 • Don't convert data to JSON in Model, the
   converting process to JSON should be
   place in Controller.
APIs design best practices (3)
•   I suggest it shouldn't include_root_in_json
    # config/application.rb
    ActiveRecord::Base.include_root_in_json = false

•   Please notice “the key is JSON must be string”.
    whether you use symbol or string in Ruby, after JSON
    encode should all be string.
•   related key format should be xxx_id or xxx_ids for
    example:
    { "user_id" => 4, "product_ids" => [1,2,5] }.to_json


•   return user_uri field in addition to the user_id field if
    need
a return data example
  model.to_json and model.to_xml is easy to use, but not useful in practice.




# one record
{ :name => "a" }.to_json


# collection
{ :collection => [ { :name => "a" } , { :name =>
"b" } ], :total => 123 }.to_json


                        If you want to have pagination, you
                                need total number.
APIs design best practices (4)
 • except return collection, we can also
   provide Multi-Gets API. through params :
   ids. ex. /users?ids=2,5,11,23
 • client should sort ID first, so we can design
   cache mechanism much easier.
 • another topic need to concern is the URL
   length of GET. So this API can also use
   POST.
an error message
       return example
{ :message => "faild", :error_codes => [1,2,3],
  :errors => ["k1" => "v1", "k2" => "v2" ] }.to_json
APIs design best practices (5)

 • error_codes & errors is optional, you can
   define it if you need.
 • errors is used to put model's validation
   error : model.errors.to_json
HTTP status code
         We should return suitable HTTP status code



• 200 OK
• 201 Created ( add success)
• 202 Accepted ( receive success but not
  process yet, in queue now )
• 400 Bad Request ( ex. Model Validation
  Error or wrong parameters )
• 401 Unauthorized
class PeopleController < ApplicationController

  def index
    @people = Person.paginate(:per_page => params[:per_page] || 20, :page => params[:page])
    render :json => { :collection => @people, :total => @people.total_entries }.to_json
  end

  def show
    @person = Person.find( params[:id] )
    render :json => @person.to_json
  end

  def create
    @person = Person.new( :name => params[:name], :bio => params[:bio],
                          :user_id => params[:user_id] )

    @person.save!
    render :json => { :id => @person.id }.to_json, :status => 201
  end

  def update
    @person = user_Person.find( params[:id] )
    @person.attributes = { :name => params[:name], :bio => params[:bio],
                           :user_id => params[:user_id] }

    @person.save!
    render :status => 200, :text => "OK"
  end

  def destroy
    @person = Person.find( params[:id] )
    @person.destroy

    render :status => 200, :text => "OK"
  end

end
Client implement
Note
• No active_record, we get data from service
  through HTTP client (typhoeus)
• Model can include some ActiveModel,
  modules so we can develop more efficiently.
• This model is logical model, mapping to the
  data from API, not database table. It's
  different to service's physical model ( ORM-
  based)
# config/appliction.rb

%w(
  action_controller
  action_mailer
).each do |framework|
  begin
    require "#{framework}/railtie"
  rescue LoadError
  end
end
Setup a global Hydry
  # config/initializers/setup_hydra.rb
  HYDRA = Typhoeus::Hydra.new
An example you can
 inherited from (1)
  class LogicalModel

    extend ActiveModel::Naming
    include ActiveModel::Conversion
    include ActiveModel::Serializers::JSON
    include ActiveModel::Validations
    include ActiveModel::MassAssignmentSecurity

    self.include_root_in_json = false

    # continued...
  end
class LogicalModel                                                     An example you can
  # continued...                                                        inherited from (2)
  def self.attribute_keys=(keys)
    @attribute_keys = keys
    attr_accessor *keys
  end

  def self.attribute_keys
    @attribute_keys
  end

  class << self
    attr_accessor :host, :hydra
  end

  def persisted?
    !!self.id
  end

  def initialize(attributes={})
    self.attributes = attributes
  end

  def attributes
    self.class.attribute_keys.inject(ActiveSupport::HashWithIndifferentAccess.new) do |result, key|
      result[key] = read_attribute_for_validation(key)
      result
    end
  end

  def attributes=(attrs)
    sanitize_for_mass_assignment(attrs).each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") }
  end
Model usage example

class Person < LogicalModel

  self.attribute_keys = [:id, :name, :bio, :user_id, :created_at, :updated_at]
  self.host = PEOPLE_SERVICE_HOST
  self.hydra = HYDRA

  validates_presence_of :title, :url, :user_id

  # ...
end
class Person < LogicalModel
  # ...
                                                                      paginate
  def self.people_uri
    "http://#{self.host}/apis/v1/people.json"
  end

  def self.async_paginate(options={})
    options[:page] ||= 1
    options[:per_page] ||= 20
    request = Typhoeus::Request.new(people_uri, :params => options)
    request.on_complete do |response|
      if response.code >= 200 && response.code < 400
        log_ok(response)

          result_set = self.from_json(response.body)
          collection = result_set[:collection].paginate( :total_entries => result_set[:total] )
          collection.current_page = options[:page]
          yield collection
        else
          log_failed(response)
        end
      end

    self.hydra.queue(request)
  end

  def self.paginate(options={})
    result = nil
    async_paginate(options) { |i| result = i }
    self.hydra.run
    result
  end

end
will_paginate hack!
      • in order to use will_paginate's helper, we
          must set current_page manually, so we hack
          this way:

# /config/initializers/hack_will_paginate.rb
# This is because our search result via HTTP API is an array and need be paginated.
# So we need assign current_page, unless it will be always 1.

module WillPaginate
  class Collection
    def current_page=(s)
      @current_page = s.to_i
    end
  end
end
from_json & logging
class LogicalModel
  # ...

  def self.from_json(json_string)
    parsed = ActiveSupport::JSON.decode(json_string)
    collection = parsed["collection"].map { |i| self.new(i) }
    return { :collection => collection, :total => parsed["total"].to_i }
  end

  def self.log_ok(response)
    Rails.logger.info("#{response.code} #{response.request.url} in #{response.time}s")
  end

  def self.log_failed(response)
    msg = "#{response.code} #{response.request.url} in #{response.time}s FAILED: #{ActiveSupport::JSON.decode
(response.body)["message"]}"
    Rails.logger.warn(msg)
  end

  def log_ok(response)
    self.class.log_ok(response)
  end

  def log_failed(response)
    self.class.log_failed(response)
  end

end
class Person < LogicalModel
  # ...
                                                                                find
  def self.person_uri(id)
    "http://#{self.host}/apis/v1/people/#{id}.json"
  end

  def self.async_find(id)

      request = Typhoeus::Request.new( person_uri(id) )
      request.on_complete do |response|
        if response.code >= 200 && response.code < 400
          log_ok(response)
          yield self.new.from_json(response.body)
        else
          log_failed(response)
        end
      end
                                                     This from_json is defined by
                                                    ActiveModel::Serializers::JSON
    self.hydra.queue(request)
  end

  def self.find(id)
    result = nil
    async_find(id) { |i| result = i }
    self.hydra.run
    result
  end

end
class Person < LogicalModel
                                              create&update
  # ...

  def create
    return false unless valid?

    response = Typhoeus::Request.post( self.class.people_uri, :params => self.attributes )
    if response.code == 201
      log_ok(response)
      self.id = ActiveSupport::JSON.decode(response.body)["id"]
      return self
    else
      log_failed(response)
      return nil
    end
  end

  def update(attributes)
    self.attributes = attributes

    return false unless valid?
    response = Typhoeus::Request.put( self.class.person_uri(id), :params => self.attributes )
    if response.code == 200
      log_ok(response)
      return self
    else
      log_failed(response)         Normally data writes do not
      return nil                    need to occur in parallel
    end
  end
end                                                                        Or write to a messaging
                                                                           system asynchronously
delete&destroy
class Person < LogicalModel
  # ...

  def self.delete(id)
    response = Typhoeus::Request.delete( self.person_uri(id) )
    if response.code == 200
      log_ok(response)
      return self
    else
      log_failed(response)
      return nil
    end
  end

  def destroy
    self.class.delete(self.id)
  end

end
Service client Library
       packaging
• Write users.gemspec file
• gem build users.gemspec
• distribution
 • http://rubygems.org
 • build your local gem server
• http://ihower.tw/blog/archives/4496
About caching

• Internally
 • Memcached
• Externally: HTTP Caching
 • Rack-Cache,Varnish, Squid
The End
Service-Oriented Design and Implement with Rails3
References
•       Books&Articles:
    •     Service-Oriented Design with Ruby and Rails, Paul Dix (Addison Wesley)
    •     Enterprise Rails, Dan Chak (O’Reilly)
    •     RESTful Web Services, Richardson&Ruby (O’Reilly)
    •     RESTful WEb Services Cookbook, Allamaraju&Amundsen (O’Reilly)
•       Blog:
    •     Rails3: ActiveModel         http://ihower.tw/blog/archives/4940
    •     Rubygems                    http://ihower.tw/blog/archives/4496
    •     Rails3: Railtie   Plugins       http://ihower.tw/blog/archives/4873
•       Slides:
    •     Distributed Ruby and Rails
          http://ihower.tw/blog/archives/3589

More Related Content

What's hot (20)

PDF
CUST-2 New Client Configuration & Extension Points in Share
Alfresco Software
 
PDF
 Active Storage - Modern File Storage? 
Michael Yagudaev
 
PPTX
Scaling with swagger
Tony Tam
 
PDF
From Ruby on Rails to RubyMotion - Writing your First iOS App with RubyMotion
Michael Denomy
 
PDF
Apereo OAE - Bootcamp
Nicolaas Matthijs
 
PDF
OSDC 2013 | Introduction into Chef by Andy Hawkins
NETWAYS
 
PPTX
Getting started with rails active storage wae
Bishal Khanal
 
PDF
遇見 Ruby on Rails
Wen-Tien Chang
 
PDF
An Intense Overview of the React Ecosystem
Rami Sayar
 
PDF
Web Development using Ruby on Rails
Avi Kedar
 
PDF
CUST-10 Customizing the Upload File(s) dialog in Alfresco Share
Alfresco Software
 
PDF
A Day of REST
Scott Taylor
 
PDF
PLAT-8 Spring Web Scripts and Spring Surf
Alfresco Software
 
PDF
Extreme Web Performance for Mobile Devices - Velocity NY
Maximiliano Firtman
 
PPTX
RESTful Services
Jason Gerard
 
PDF
WCM-7 Surfing with CMIS
Alfresco Software
 
PPTX
Developing Complex WordPress Sites without Fear of Failure (with MVC)
Mike Schinkel
 
PDF
Riding the Edge with Ember.js
aortbals
 
PDF
Adobe AEM CQ5 - Developer Introduction
Yash Mody
 
PPTX
Containerdays Intro to Habitat
Mandi Walls
 
CUST-2 New Client Configuration & Extension Points in Share
Alfresco Software
 
 Active Storage - Modern File Storage? 
Michael Yagudaev
 
Scaling with swagger
Tony Tam
 
From Ruby on Rails to RubyMotion - Writing your First iOS App with RubyMotion
Michael Denomy
 
Apereo OAE - Bootcamp
Nicolaas Matthijs
 
OSDC 2013 | Introduction into Chef by Andy Hawkins
NETWAYS
 
Getting started with rails active storage wae
Bishal Khanal
 
遇見 Ruby on Rails
Wen-Tien Chang
 
An Intense Overview of the React Ecosystem
Rami Sayar
 
Web Development using Ruby on Rails
Avi Kedar
 
CUST-10 Customizing the Upload File(s) dialog in Alfresco Share
Alfresco Software
 
A Day of REST
Scott Taylor
 
PLAT-8 Spring Web Scripts and Spring Surf
Alfresco Software
 
Extreme Web Performance for Mobile Devices - Velocity NY
Maximiliano Firtman
 
RESTful Services
Jason Gerard
 
WCM-7 Surfing with CMIS
Alfresco Software
 
Developing Complex WordPress Sites without Fear of Failure (with MVC)
Mike Schinkel
 
Riding the Edge with Ember.js
aortbals
 
Adobe AEM CQ5 - Developer Introduction
Yash Mody
 
Containerdays Intro to Habitat
Mandi Walls
 

Similar to Service-Oriented Design and Implement with Rails3 (20)

PDF
Service-oriented architecture
Shalva Usubov
 
PDF
Microservices - opportunities, dilemmas and problems
Łukasz Sowa
 
PPTX
Scalable Web Architecture and Distributed Systems
hyun soomyung
 
PPTX
Inside Wordnik's Architecture
Tony Tam
 
PPTX
Digging deeper into service stack
cyberzeddk
 
KEY
Synchronous Reads Asynchronous Writes RubyConf 2009
pauldix
 
PDF
09-01-services-slides.pdf for educations
katariraju71
 
PPT
Service-oriented Architecture with Respect to Reusability
Yazd University
 
KEY
Rails services in the walled garden
Sidu Ponnappa
 
PPTX
Let's talk about... Microservices
Alessandro Giorgetti
 
PDF
Keynote-Service Orientation – Why is it good for your business
WSO2
 
PPTX
Service stack all the things
cyberzeddk
 
PPTX
The Big Picture - Integrating Buzzwords
Alessandro Giorgetti
 
PPTX
UNIT2_Cloud Computing - Cloud Enabling Technologies
Sathishkumar Jaganathan
 
KEY
Simple Services
twoerth
 
PDF
Modern Architectures with Spring and JavaScript
martinlippert
 
PDF
Microservices: Benefits, drawbacks and are they for me?
Marian Marinov
 
PPTX
SOA - Unit 2 - Service Oriented Architecture
hamsa nandhini
 
PPTX
Mykhailo Hryhorash: Архітектура IT-рішень (Частина 1) (UA)
Lviv Startup Club
 
PPTX
Mykhailo Hryhorash: Архітектура IT-рішень (Частина 1) (UA)
content75
 
Service-oriented architecture
Shalva Usubov
 
Microservices - opportunities, dilemmas and problems
Łukasz Sowa
 
Scalable Web Architecture and Distributed Systems
hyun soomyung
 
Inside Wordnik's Architecture
Tony Tam
 
Digging deeper into service stack
cyberzeddk
 
Synchronous Reads Asynchronous Writes RubyConf 2009
pauldix
 
09-01-services-slides.pdf for educations
katariraju71
 
Service-oriented Architecture with Respect to Reusability
Yazd University
 
Rails services in the walled garden
Sidu Ponnappa
 
Let's talk about... Microservices
Alessandro Giorgetti
 
Keynote-Service Orientation – Why is it good for your business
WSO2
 
Service stack all the things
cyberzeddk
 
The Big Picture - Integrating Buzzwords
Alessandro Giorgetti
 
UNIT2_Cloud Computing - Cloud Enabling Technologies
Sathishkumar Jaganathan
 
Simple Services
twoerth
 
Modern Architectures with Spring and JavaScript
martinlippert
 
Microservices: Benefits, drawbacks and are they for me?
Marian Marinov
 
SOA - Unit 2 - Service Oriented Architecture
hamsa nandhini
 
Mykhailo Hryhorash: Архітектура IT-рішень (Частина 1) (UA)
Lviv Startup Club
 
Mykhailo Hryhorash: Архітектура IT-рішень (Частина 1) (UA)
content75
 
Ad

More from Wen-Tien Chang (20)

PDF
評估驅動開發 Eval-Driven Development (EDD): 生成式 AI 軟體不確定性的解決方法
Wen-Tien Chang
 
PDF
⼤語⾔模型 LLM 應⽤開發入⾨
Wen-Tien Chang
 
PDF
Ruby Rails 老司機帶飛
Wen-Tien Chang
 
PDF
A brief introduction to Machine Learning
Wen-Tien Chang
 
PDF
淺談 Startup 公司的軟體開發流程 v2
Wen-Tien Chang
 
PDF
RSpec on Rails Tutorial
Wen-Tien Chang
 
PDF
RSpec & TDD Tutorial
Wen-Tien Chang
 
PDF
ALPHAhackathon: How to collaborate
Wen-Tien Chang
 
PDF
Git 版本控制系統 -- 從微觀到宏觀
Wen-Tien Chang
 
PDF
Exception Handling: Designing Robust Software in Ruby (with presentation note)
Wen-Tien Chang
 
PDF
Exception Handling: Designing Robust Software in Ruby
Wen-Tien Chang
 
PDF
從 Classes 到 Objects: 那些 OOP 教我的事
Wen-Tien Chang
 
PDF
Yet another introduction to Git - from the bottom up
Wen-Tien Chang
 
PDF
A brief introduction to Vagrant – 原來 VirtualBox 可以這樣玩
Wen-Tien Chang
 
PDF
Ruby 程式語言綜覽簡介
Wen-Tien Chang
 
PDF
A brief introduction to SPDY - 邁向 HTTP/2.0
Wen-Tien Chang
 
PDF
RubyConf Taiwan 2012 Opening & Closing
Wen-Tien Chang
 
PDF
從 Scrum 到 Kanban: 為什麼 Scrum 不適合 Lean Startup
Wen-Tien Chang
 
PDF
Git Tutorial 教學
Wen-Tien Chang
 
PDF
那些 Functional Programming 教我的事
Wen-Tien Chang
 
評估驅動開發 Eval-Driven Development (EDD): 生成式 AI 軟體不確定性的解決方法
Wen-Tien Chang
 
⼤語⾔模型 LLM 應⽤開發入⾨
Wen-Tien Chang
 
Ruby Rails 老司機帶飛
Wen-Tien Chang
 
A brief introduction to Machine Learning
Wen-Tien Chang
 
淺談 Startup 公司的軟體開發流程 v2
Wen-Tien Chang
 
RSpec on Rails Tutorial
Wen-Tien Chang
 
RSpec & TDD Tutorial
Wen-Tien Chang
 
ALPHAhackathon: How to collaborate
Wen-Tien Chang
 
Git 版本控制系統 -- 從微觀到宏觀
Wen-Tien Chang
 
Exception Handling: Designing Robust Software in Ruby (with presentation note)
Wen-Tien Chang
 
Exception Handling: Designing Robust Software in Ruby
Wen-Tien Chang
 
從 Classes 到 Objects: 那些 OOP 教我的事
Wen-Tien Chang
 
Yet another introduction to Git - from the bottom up
Wen-Tien Chang
 
A brief introduction to Vagrant – 原來 VirtualBox 可以這樣玩
Wen-Tien Chang
 
Ruby 程式語言綜覽簡介
Wen-Tien Chang
 
A brief introduction to SPDY - 邁向 HTTP/2.0
Wen-Tien Chang
 
RubyConf Taiwan 2012 Opening & Closing
Wen-Tien Chang
 
從 Scrum 到 Kanban: 為什麼 Scrum 不適合 Lean Startup
Wen-Tien Chang
 
Git Tutorial 教學
Wen-Tien Chang
 
那些 Functional Programming 教我的事
Wen-Tien Chang
 
Ad

Recently uploaded (20)

PDF
5 Things to Consider When Deploying AI in Your Enterprise
Safe Software
 
DOCX
Daily Lesson Log MATATAG ICT TEchnology 8
LOIDAALMAZAN3
 
PDF
LLM Search Readiness Audit - Dentsu x SEO Square - June 2025.pdf
Nick Samuel
 
PDF
UiPath Agentic AI ile Akıllı Otomasyonun Yeni Çağı
UiPathCommunity
 
PPTX
Enabling the Digital Artisan – keynote at ICOCI 2025
Alan Dix
 
PDF
EIS-Webinar-Engineering-Retail-Infrastructure-06-16-2025.pdf
Earley Information Science
 
PDF
Database Benchmarking for Performance Masterclass: Session 1 - Benchmarking F...
ScyllaDB
 
PDF
Cracking the Code - Unveiling Synergies Between Open Source Security and AI.pdf
Priyanka Aash
 
PDF
From Chatbot to Destroyer of Endpoints - Can ChatGPT Automate EDR Bypasses (1...
Priyanka Aash
 
PDF
“Scaling i.MX Applications Processors’ Native Edge AI with Discrete AI Accele...
Edge AI and Vision Alliance
 
PPTX
CapCut Pro Crack For PC Latest Version {Fully Unlocked} 2025
pcprocore
 
PDF
Darley - FIRST Copenhagen Lightning Talk (2025-06-26) Epochalypse 2038 - Time...
treyka
 
PPTX
Curietech AI in action - Accelerate MuleSoft development
shyamraj55
 
PPTX
Practical Applications of AI in Local Government
OnBoard
 
PDF
Optimizing the trajectory of a wheel loader working in short loading cycles
Reno Filla
 
PDF
FME as an Orchestration Tool with Principles From Data Gravity
Safe Software
 
PDF
2025_06_18 - OpenMetadata Community Meeting.pdf
OpenMetadata
 
PDF
Unlocking FME Flow’s Potential: Architecture Design for Modern Enterprises
Safe Software
 
PDF
Salesforce Summer '25 Release Frenchgathering.pptx.pdf
yosra Saidani
 
PDF
Hyderabad MuleSoft In-Person Meetup (June 21, 2025) Slides
Ravi Tamada
 
5 Things to Consider When Deploying AI in Your Enterprise
Safe Software
 
Daily Lesson Log MATATAG ICT TEchnology 8
LOIDAALMAZAN3
 
LLM Search Readiness Audit - Dentsu x SEO Square - June 2025.pdf
Nick Samuel
 
UiPath Agentic AI ile Akıllı Otomasyonun Yeni Çağı
UiPathCommunity
 
Enabling the Digital Artisan – keynote at ICOCI 2025
Alan Dix
 
EIS-Webinar-Engineering-Retail-Infrastructure-06-16-2025.pdf
Earley Information Science
 
Database Benchmarking for Performance Masterclass: Session 1 - Benchmarking F...
ScyllaDB
 
Cracking the Code - Unveiling Synergies Between Open Source Security and AI.pdf
Priyanka Aash
 
From Chatbot to Destroyer of Endpoints - Can ChatGPT Automate EDR Bypasses (1...
Priyanka Aash
 
“Scaling i.MX Applications Processors’ Native Edge AI with Discrete AI Accele...
Edge AI and Vision Alliance
 
CapCut Pro Crack For PC Latest Version {Fully Unlocked} 2025
pcprocore
 
Darley - FIRST Copenhagen Lightning Talk (2025-06-26) Epochalypse 2038 - Time...
treyka
 
Curietech AI in action - Accelerate MuleSoft development
shyamraj55
 
Practical Applications of AI in Local Government
OnBoard
 
Optimizing the trajectory of a wheel loader working in short loading cycles
Reno Filla
 
FME as an Orchestration Tool with Principles From Data Gravity
Safe Software
 
2025_06_18 - OpenMetadata Community Meeting.pdf
OpenMetadata
 
Unlocking FME Flow’s Potential: Architecture Design for Modern Enterprises
Safe Software
 
Salesforce Summer '25 Release Frenchgathering.pptx.pdf
yosra Saidani
 
Hyderabad MuleSoft In-Person Meetup (June 21, 2025) Slides
Ravi Tamada
 

Service-Oriented Design and Implement with Rails3

  • 1. Service-Oriented Design and Implement with Rails 3 ihower @ Ruby Tuesday 2010/12/15
  • 2. About Me • a.k.a. ihower • http://ihower.tw • http://twitter.com/ihower • Rails Developer since 2006 • The Organizer of Ruby Taiwan Community • http://ruby.tw • http://rubyconf.tw
  • 3. Agenda • What’s SOA • Why SOA • Considerations • The tool set overview • Service side implement • Client side implement • Library packaging • Caching
  • 4. What’s SOA Service oriented architectures • “monolithic” approach is not enough • SOA is a way to design complex applications by splitting out major components into individual services and communicating via APIs. • a service is a vertical slice of functionality: database, application code and caching layer
  • 5. a monolithic web app example request Load Balancer WebApps Database
  • 6. a SOA example request Load request Balancer WebApp WebApps for Administration for User Services A Services B Database Database
  • 7. Why SOA? Isolation • Shared Resources • Encapsulation • Scalability • Interoperability • Reuse • Testability • Reduce Local Complexity
  • 8. Shared Resources • Different front-web website use the same resource. • SOA help you avoiding duplication databases and code. • Why not only shared database? • code is not DRY WebApp for Administration WebApps for User • caching will be problematic Database
  • 9. Encapsulation • you can change underly implementation in services without affect other parts of system • upgrade library • upgrade to Ruby 1.9 • upgrade to Rails 3 • you can provide API versioning
  • 10. Scalability1: Partitioned Data Provides • Database is the first bottleneck, a single DB server can not scale. SOA help you reduce database load • Anti-pattern: only split the database • model relationship is broken WebApps • referential integrity • increase code complexity Database A Database B • Myth: database replication can not help you speed and consistency
  • 11. Scalability 2: Caching • SOA help you design caching system easier • Cache data at the right place and expire at the right times • Cache logical model, not physical • You do not need cache view everywhere
  • 12. Scalability 3: Efficient • Different components have different task loading, SOA can scale by service. WebApps Load Balancer Load Balancer Services A Services A Services B Services B Services B Services B
  • 13. Security • Different services can be inside different firewall • You can only open public web and services, others are inside firewall.
  • 14. Interoperability • HTTP is the most common interface, SOA help you integrate them: • Multiple languages • Internal system e.g. Full-text searching engine • Legacy database, system • External vendors
  • 15. Reuse • Reuse across multiple applications • Reuse for public APIs • Example: Amazon Web Services (AWS)
  • 16. Testability • Isolate problem • Mocking API calls • Reduce the time to run test suite
  • 17. Reduce Local Complexity • Team modularity along the same module splits as your software • Understandability: The amount of code is minimized to a quantity understandable by a small team • Source code control
  • 18. Design considerations • Partition into Separate Services • API Design • Which Protocol
  • 19. How to partition into Separate Services • Partitioning on Logical Function • Partitioning on Read/Write Frequencies • Partitioning on Minimizing Joins • Partitioning on Iteration Speed
  • 20. on Iteration Speed • Which parts of the app have clear defined requirements and design? • Identify the parts of the application which are unlikely to change. • For example: The first version data storage is using MySQL, but may change to NoSQL in the future without affecting front-app.
  • 21. on Logical Function • Higher-level feature services • articles, photos, bookmarks...etc • Low-level infrastructure services • a shared key-value store, queue system
  • 22. On Read/Write Frequencies • Ideally, a service will have to work only with a single data store • High read and low write: the service should optimize a caching strategy. • High write and low read: don’t bother with caching
  • 23. On Join Frequency • Minimize cross-service joins. • But almost all data in an app is joined to something else. • How often particular joins occur? by read/ write frequency and logical separation. • Replicate data across services (For example: a activity stream by using messaging)
  • 24. API Design Guideline • Send Everything you need • Unlike OOP has lots of finely grained method calls • Parallel HTTP requests • for multiple service requests • Send as Little as Possible • Avoid expensive XML
  • 25. Versioning • Be able run multiple versions in parallel: Clients have time to upgrade rather than having to upgrade both client and server in locks step. • Ideally, you won’t have to run multiple versions for very long • Two solutions: • Including a Version in URIs • Using Accept Headers for Versioning (disadvantage: HTTP caching)
  • 26. Physical Models & Logical Models • Physical models are mapped to database tables through ORM. (It’s 3NF) • Logical models are mapped to your business problem. (External API use it) • Logical models are mapped to physical models by you.
  • 27. Logical Models • Not relational or normalized • Maintainability • can change with no change to data store • can stay the same while the data store changes • Better fit for REST interfaces • Better caching
  • 28. Which Protocol? • SOAP • XML-RPC • REST
  • 29. RESTful Web services • Rails way • Easy to use and implement • REST is about resources • URI • HTTP Verbs: GET/PUT/POST/DELETE • Representations: HTML, XML, JSON...etc
  • 30. The tool set • Web framework • XML Parser • JSON Parser • HTTP Client • Model library
  • 31. Web framework • Ruby on Rails, but we don’t need afull features. (Rails3 can be customized because it’s lot more modular. We will discuss it later) • Sinatra: a lightweight framework • Rack: a minimal Ruby webserver interface library
  • 32. ActiveResource • Mapping RESTful resources as models in a Rails application. • Use XML by default • But not useful in practice, why?
  • 33. XML parser • http://nokogiri.org/ • Nokogiri ( ) is an HTML, XML, SAX, and Reader parser. Among Nokogiri’s many features is the ability to search documents via XPath or CSS3 selectors.
  • 34. JSON Parser • http://github.com/brianmario/yajl-ruby/ • An extremely efficient streaming JSON parsing and encoding library. Ruby C bindings to Yajl
  • 35. HTTP Client • How to run requests in parallel? • Asynchronous I/O • Reactor pattern (EventMachine) • Multi-threading • JRuby
  • 36. Typhoeus http://github.com/pauldix/typhoeus/ • A Ruby library with native C extensions to libcurl and libcurl-multi. • Typhoeus runs HTTP requests in parallel while cleanly encapsulating handling logic
  • 37. Typhoeus: Quick example response = Typhoeus::Request.get("http://www.pauldix.net") response = Typhoeus::Request.head("http://www.pauldix.net") response = Typhoeus::Request.put("http://localhost:3000/posts/1", :body => "whoo, a body") response = Typhoeus::Request.post("http://localhost:3000/posts", :params => {:title => "test post", :content => "this is my test"}) response = Typhoeus::Request.delete("http://localhost:3000/posts/1")
  • 38. Hydra handles requests but not guaranteed to run in any particular order HYDRA = Typhoeus::HYDRA.new a = nil request1 = Typhoeus::Request.new("http://example1") request1.on_complete do |response| a = response.body end HYDRA.queue(request1) b = nil request2 = Typhoeus::Request.new("http://example1") request2.on_complete do |response| b = response.body end HYDRA.queue(request2) HYDRA.run # a, b are set from here
  • 39. a asynchronous method def foo_asynchronously request = Typhoeus::Request.new( "http://example" ) request.on_complete do |response| result_value = ActiveSupport::JSON.decode(response.body) # do something yield result_value end self.hydra.queue(request) end
  • 40. Usage result = nil foo_asynchronously do |i| result = i end foo_asynchronously do |i| # Do something for i end HYDRA.run # Now you can use result1 and result2
  • 41. a synchronous method def foo result = nil foo_asynchronously { |i| result = i } self.hydra.run result end
  • 42. Physical Models mapping to database directly • ActiveRecord • DataMapper • MongoMapper, MongoId
  • 43. Logical Models • ActiveModel: an interface and modules can be integrated with ActionPack helpers. • http://ihower.tw/blog/archives/4940
  • 44. integrated with helper? • For example: • link_to post_path(@post) • form_for @post • @post.errors
  • 45. A basic model class YourModel extend ActiveModel::Naming include ActiveModel::Conversion include ActiveModel::Validations def persisted? false end end
  • 46. without validations class YourModel extend ActiveModel::Naming include ActiveModel::Conversion def persisted? false end def valid?() true end def errors @errors ||= ActiveModel::Errors.new(self) end end
  • 47. Many useful modules • MassAssignmentSecurity • Serialization • Callback • AttributeMethods • Dirty • Observing • Translation
  • 48. Serializers class Person include ActiveModel::Serializers::JSON include ActiveModel::Serializers::Xml attr_accessor :name def attributes @attributes ||= {'name' => 'nil'} end end person = Person.new person.serializable_hash # => {"name"=>nil} person.as_json # => {"name"=>nil} person.to_json # => "{"name":null}" person.to_xml # => "<?xml version="1.0" encoding="UTF-8"?> n<serial-person...
  • 49. Mass Assignment class YourModel # ... def initialize(attributes = {}) if attributes.present? attributes.each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") } end end end YourModel.new( :a => 1, :b => 2, :c => 3 )
  • 50. MassAssignmentSecurity class YourModel # ... include ActiveModel::MassAssignmentSecurity attr_accessible :first_name, :last_name def initialize(attributes = {}) if attributes.present? sanitize_for_mass_assignment(attributes).each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") } end end end
  • 51. Scenario we want to implement • an Users web service, which provide basic CRUD functions. • an web application with the Users client library
  • 53. Customized Rails3 • We don’t need some components. • We can customize ActionController • Building a fast, lightweight REST service with Rails 3 http://pivotallabs.com/users/jdean/blog/articles/1419-building-a-fast- lightweight-rest-service-with-rails-3
  • 54. # config/appliction.rb %w( active_record action_controller action_mailer ).each do |framework| begin require "#{framework}/railtie" rescue LoadError end end
  • 55. # config/application.rb [ Rack::Sendfile, ActionDispatch::Flash, ActionDispatch::Session::CookieStore, ActionDispatch::Cookies, ActionDispatch::BestStandardsSupport, Rack::MethodOverride, ActionDispatch::ShowExceptions, ActionDispatch::Static, ActionDispatch::RemoteIp, ActionDispatch::ParamsParser, Rack::Lock, ActionDispatch::Head ].each do |klass| config.middleware.delete klass end # config/environments/production.rb config.middleware.delete ActiveRecord::ConnectionAdapters::ConnectionManagement
  • 56. # /app/controllers/application_controller.rb class ApplicationController < ActionController::Base class ApplicationController < ActionController::Metal include AbstractController::Logger include Rails.application.routes.url_helpers include ActionController::UrlFor include ActionController::Rendering include ActionController::Renderers::All include ActionController::MimeResponds if Rails.env.test? include ActionController::Testing # Rails 2.x compatibility include ActionController::Compatibility end end http://ihower.tw/blog/archives/4561
  • 57. APIs design best practices (1) • Routing doesn't need Rails resources mechanism , but APIs design should follow RESTful. (This is because we don't have view in service and we don't need URL helpers. So use resources mechanism is too overkill) • RESTful APIs is stateless, each APIs should be independent. So, requests which have dependency relationship should be combined into one API request. (atomic)
  • 58. APIs design best practices (2) • The best format in most case is JSON. ( one disadvantage is we can’t return binary data directly. ) • Use Yajl as parser. # config/application.rb ActiveSupport::JSON.backend = "Yajl" • Don't convert data to JSON in Model, the converting process to JSON should be place in Controller.
  • 59. APIs design best practices (3) • I suggest it shouldn't include_root_in_json # config/application.rb ActiveRecord::Base.include_root_in_json = false • Please notice “the key is JSON must be string”. whether you use symbol or string in Ruby, after JSON encode should all be string. • related key format should be xxx_id or xxx_ids for example: { "user_id" => 4, "product_ids" => [1,2,5] }.to_json • return user_uri field in addition to the user_id field if need
  • 60. a return data example model.to_json and model.to_xml is easy to use, but not useful in practice. # one record { :name => "a" }.to_json # collection { :collection => [ { :name => "a" } , { :name => "b" } ], :total => 123 }.to_json If you want to have pagination, you need total number.
  • 61. APIs design best practices (4) • except return collection, we can also provide Multi-Gets API. through params : ids. ex. /users?ids=2,5,11,23 • client should sort ID first, so we can design cache mechanism much easier. • another topic need to concern is the URL length of GET. So this API can also use POST.
  • 62. an error message return example { :message => "faild", :error_codes => [1,2,3], :errors => ["k1" => "v1", "k2" => "v2" ] }.to_json
  • 63. APIs design best practices (5) • error_codes & errors is optional, you can define it if you need. • errors is used to put model's validation error : model.errors.to_json
  • 64. HTTP status code We should return suitable HTTP status code • 200 OK • 201 Created ( add success) • 202 Accepted ( receive success but not process yet, in queue now ) • 400 Bad Request ( ex. Model Validation Error or wrong parameters ) • 401 Unauthorized
  • 65. class PeopleController < ApplicationController def index @people = Person.paginate(:per_page => params[:per_page] || 20, :page => params[:page]) render :json => { :collection => @people, :total => @people.total_entries }.to_json end def show @person = Person.find( params[:id] ) render :json => @person.to_json end def create @person = Person.new( :name => params[:name], :bio => params[:bio], :user_id => params[:user_id] ) @person.save! render :json => { :id => @person.id }.to_json, :status => 201 end def update @person = user_Person.find( params[:id] ) @person.attributes = { :name => params[:name], :bio => params[:bio], :user_id => params[:user_id] } @person.save! render :status => 200, :text => "OK" end def destroy @person = Person.find( params[:id] ) @person.destroy render :status => 200, :text => "OK" end end
  • 67. Note • No active_record, we get data from service through HTTP client (typhoeus) • Model can include some ActiveModel, modules so we can develop more efficiently. • This model is logical model, mapping to the data from API, not database table. It's different to service's physical model ( ORM- based)
  • 68. # config/appliction.rb %w( action_controller action_mailer ).each do |framework| begin require "#{framework}/railtie" rescue LoadError end end
  • 69. Setup a global Hydry # config/initializers/setup_hydra.rb HYDRA = Typhoeus::Hydra.new
  • 70. An example you can inherited from (1) class LogicalModel extend ActiveModel::Naming include ActiveModel::Conversion include ActiveModel::Serializers::JSON include ActiveModel::Validations include ActiveModel::MassAssignmentSecurity self.include_root_in_json = false # continued... end
  • 71. class LogicalModel An example you can # continued... inherited from (2) def self.attribute_keys=(keys) @attribute_keys = keys attr_accessor *keys end def self.attribute_keys @attribute_keys end class << self attr_accessor :host, :hydra end def persisted? !!self.id end def initialize(attributes={}) self.attributes = attributes end def attributes self.class.attribute_keys.inject(ActiveSupport::HashWithIndifferentAccess.new) do |result, key| result[key] = read_attribute_for_validation(key) result end end def attributes=(attrs) sanitize_for_mass_assignment(attrs).each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") } end
  • 72. Model usage example class Person < LogicalModel self.attribute_keys = [:id, :name, :bio, :user_id, :created_at, :updated_at] self.host = PEOPLE_SERVICE_HOST self.hydra = HYDRA validates_presence_of :title, :url, :user_id # ... end
  • 73. class Person < LogicalModel # ... paginate def self.people_uri "http://#{self.host}/apis/v1/people.json" end def self.async_paginate(options={}) options[:page] ||= 1 options[:per_page] ||= 20 request = Typhoeus::Request.new(people_uri, :params => options) request.on_complete do |response| if response.code >= 200 && response.code < 400 log_ok(response) result_set = self.from_json(response.body) collection = result_set[:collection].paginate( :total_entries => result_set[:total] ) collection.current_page = options[:page] yield collection else log_failed(response) end end self.hydra.queue(request) end def self.paginate(options={}) result = nil async_paginate(options) { |i| result = i } self.hydra.run result end end
  • 74. will_paginate hack! • in order to use will_paginate's helper, we must set current_page manually, so we hack this way: # /config/initializers/hack_will_paginate.rb # This is because our search result via HTTP API is an array and need be paginated. # So we need assign current_page, unless it will be always 1. module WillPaginate class Collection def current_page=(s) @current_page = s.to_i end end end
  • 75. from_json & logging class LogicalModel # ... def self.from_json(json_string) parsed = ActiveSupport::JSON.decode(json_string) collection = parsed["collection"].map { |i| self.new(i) } return { :collection => collection, :total => parsed["total"].to_i } end def self.log_ok(response) Rails.logger.info("#{response.code} #{response.request.url} in #{response.time}s") end def self.log_failed(response) msg = "#{response.code} #{response.request.url} in #{response.time}s FAILED: #{ActiveSupport::JSON.decode (response.body)["message"]}" Rails.logger.warn(msg) end def log_ok(response) self.class.log_ok(response) end def log_failed(response) self.class.log_failed(response) end end
  • 76. class Person < LogicalModel # ... find def self.person_uri(id) "http://#{self.host}/apis/v1/people/#{id}.json" end def self.async_find(id) request = Typhoeus::Request.new( person_uri(id) ) request.on_complete do |response| if response.code >= 200 && response.code < 400 log_ok(response) yield self.new.from_json(response.body) else log_failed(response) end end This from_json is defined by ActiveModel::Serializers::JSON self.hydra.queue(request) end def self.find(id) result = nil async_find(id) { |i| result = i } self.hydra.run result end end
  • 77. class Person < LogicalModel create&update # ... def create return false unless valid? response = Typhoeus::Request.post( self.class.people_uri, :params => self.attributes ) if response.code == 201 log_ok(response) self.id = ActiveSupport::JSON.decode(response.body)["id"] return self else log_failed(response) return nil end end def update(attributes) self.attributes = attributes return false unless valid? response = Typhoeus::Request.put( self.class.person_uri(id), :params => self.attributes ) if response.code == 200 log_ok(response) return self else log_failed(response) Normally data writes do not return nil need to occur in parallel end end end Or write to a messaging system asynchronously
  • 78. delete&destroy class Person < LogicalModel # ... def self.delete(id) response = Typhoeus::Request.delete( self.person_uri(id) ) if response.code == 200 log_ok(response) return self else log_failed(response) return nil end end def destroy self.class.delete(self.id) end end
  • 79. Service client Library packaging • Write users.gemspec file • gem build users.gemspec • distribution • http://rubygems.org • build your local gem server • http://ihower.tw/blog/archives/4496
  • 80. About caching • Internally • Memcached • Externally: HTTP Caching • Rack-Cache,Varnish, Squid
  • 83. References • Books&Articles: • Service-Oriented Design with Ruby and Rails, Paul Dix (Addison Wesley) • Enterprise Rails, Dan Chak (O’Reilly) • RESTful Web Services, Richardson&Ruby (O’Reilly) • RESTful WEb Services Cookbook, Allamaraju&Amundsen (O’Reilly) • Blog: • Rails3: ActiveModel http://ihower.tw/blog/archives/4940 • Rubygems http://ihower.tw/blog/archives/4496 • Rails3: Railtie Plugins http://ihower.tw/blog/archives/4873 • Slides: • Distributed Ruby and Rails http://ihower.tw/blog/archives/3589