Skip to content

Abstract Chain

Vladimir Kalinkin edited this page Jun 5, 2018 · 16 revisions

Dandy Architecture

Basics

Model-View-Controller (MVC) now is a sign of bad taste. Everybody who built more or less serious production web-application know that controllers become to be fat. Splitting controllers by context can a bit improve the situation but it's a visual trick, it still looks like:

class UserController
  def index
    # ... 
  end
  
  def show
    # ... 
  end

  def update
    # ... 
  end

  # ...  
end

It roughly violates one of main OOP rules - Single Responsibility Principle. Somebody can say "controller is only intended to control the process of request handling", but it's too large responsibility. Actually it's responsible for creating, updating and deleting models, showing views, services invocation and so on. And it's an ideal picture. Usual situation is when domain code is located in controllers. Dandy replaces regular controllers with a chain of atomic actions. It doesn't protect you to use bad practices but allows to easily separate [controller-]actions. Every action is a simple brick:

class ShowUsers
  def call
    # ...
  end
end

class UpdateUser
  def call
    # ...
  end
end

A route can represent much more complicated logic containing several steps. For that purpose Dandy provides an ability to chain actions:

    -> user@load_user -> update_user -> notify_admin -> log_action

Every of enumerated above actions are classes respond to method "call". Each action should be as small as it possible: ideally they should do only one thing.

Data flow

An action can return a result using "@" operator. The value declared by "@" can be used by the next actions. For example:

    -> user@load_user -> update_user
class LoadUser
  def initialize(id)
    @id = id
  end
  def call
    @user_repo.find(@id)
  end
end

Action "load_user" automatically registers "user" object in DI container. You can easily use it in "update_user":

class UpdateUser
  def initialize(user, dandy_data)
    @user = user
    @data = dandy_data
  end
  def call
    @user.update(@data)
  end
end

Direct object calls

You don't need to create an action just for a trivial delegation. Let's say we have User entity:

class User
  ...
  def full_name
    "#{first_name} #{last_name}"
  end
end

you can easily access members of registered objects ("@") directly in route definition:

 /user/what_is_my_full_name ->
   GET -> user@load_current_user -> user.full_name

You can use a parameters for such calls. They cannot be declared directly in route definition in order to keep it clean. But all you need - just enumerate desired parameters in method signature, Dandy will "inject" required dependencies into the method:

 /user/$id/change_role ->
   PATCH -> user@load_user -> role@load_role -> user.change_role
class User
  ...
  def change_role(role)
    @role = role
  end
end

Dandy Request Context

As described above you can inject request parameters directly to an action. Also you can access other request values like query parameters, form data, etc.

dandy_params - is a params of the request like user/1/post/12 represented as Ruby hash. Usage example:

class LoadPost
 def initialize(post_repo, dandy_params)
   @dandy_params = dandy_params
   @post_repo = post_repo
 end 
 
 def call
   @post_repo.get_by_id(@dandy_params[:id])
 end
end

dandy_data - is a JSON-object sent from client in request body and represented as Ruby hash. Usage example:

class UpdatePost
 def initialize(post, dandy_data)
   @post = post
   @dandy_data = dandy_data
 end 
 
 def call
   @post.update(@dandy_data)
 end
end

dandy_query - is a query of the request like ?user_id=1&post_id=12 represented as Ruby hash.

class FindPosts
 def initialize(post_repo, dandy_query)
   @dandy_query = dandy_query
   @post_repo = post_repo
 end 
 
 def call
   @post_repo.find_posts(type: dandy_query[:type])
 end
end

dandy_files - is an array of files extracted from multipart data.

class ImportPost
 def initialize(post_repo, dandy_files, current_user)
   @dandy_files = dandy_files
   @post_repo = post_repo
   @current_user = current_user
 end 
 
 def call
   post = extracted_post.merge(user: @current_user)
   @post_repo.create(post)
 end
 
 private
 def extracted_post
   json = @dandy_files[0][:tempfile].read
   ...
 end 
end

Keys Format

If your frontend application uses camelCase keys then Dandy converts them automatically to snake_case. In example:

You send POST request with data:

{
  "authorName": "Author Name"
}

In Dandy action you receive a hash:

{:author_name => 'Author Name'}

By default Dandy sends back a JSON in snake_case. If you wanna receive JSON in camelCase you need specify the format in HTTP headers. For that purpose just add header 'Keys-Format': 'camel' to your request.

Response Format

You should explicitly specify header 'Accept' for your requests. For JS-applications it should be 'Accept': 'application/json'.

Clone this wiki locally