Skip to content

Instantly share code, notes, and snippets.

@janko
Created May 27, 2015 17:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save janko/c3c7954510840d8df2fd to your computer and use it in GitHub Desktop.
Save janko/c3c7954510840d8df2fd to your computer and use it in GitHub Desktop.
Components of good application design (my presentation for a local Ruby meetup)
# 1. Models (Entities)
#
# * The nouns of your business logic
# * Usually persisted in the database
# * Usually contain validations
# * Usually expose associations with other models
# * Usually have scopes (to encapsulate the query logic)
################
# ActiveRecord #
################
class User < ActiveRecord::Base
has_many :posts
scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :active, -> { where(id: Post.uniq.pluck(:creator_id)) }
validates_presence_of :email, :first_name, :last_name
validates_uniqueness_of :email
end
##########
# Sequel #
##########
Sequel::Model.plugin :nested_attributes
class Post < Sequel::Model
many_to_one :user
nested_attributes :comments
dataset_module do
def newest
order{created_at.desc}
end
def recent(count = 10)
newest.limit(count)
end
end
end
#######
# ROM #
#######
class Comment
attr_reader :content, :post, :author
def initialize(attributes)
@content, @post, @author = attributes.values_at(:content, :post, :author)
end
end
# 2. Finders (Query objects, Repositories)
#
# * Encapsulate querying
# * Allow more control over grouping of scopes
# * You can mix-and-match, some scopes in models, some in finders
# * If you only use finder methods, you can easily stub which makes testing easier
class User < ActiveRecord::Base
# ...
scope :post_authors, -> { where(id: Post.pluck(:author_id)) }
scope :commenters, -> { where(id: Comment.pluck(:author_id)) }
scope :invitees, -> { User.where.not(inviter_id: nil) }
scope :inviters, -> { where(id: User.pluck(:inviter_id)) }
scope :invited_by ->(user) { where(inviter_id: user.id) }
scope :customers, -> { where(id: Purchases.pluck(:user_id)) }
# ...
end
#===========================================================================================
class User < ActiveRecord::Base
scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :active, ->(time) { where("last_active_at > ?", time) }
end
class InvitationFinder
def self.invitees
User.where.not(inviter_id: nil)
end
def self.inviters
User.where(id: User.pluck(:inviter_id))
end
def self.invited_by(user)
User.where(inviter_id: user.id)
end
end
class PostFinder
def self.find(identifier)
case identifier
when Integer then find_by_id(identifier)
when String then find_by_slug(identifier)
end
end
def self.find!(identifier)
finde(identifier) or not_found!(identifier)
end
def self.find_by_id(identifier)
Post.where(id: identifier)
end
def self.find_by_slug(identifier)
Post.where(slug: identifier)
end
def self.authors
User.where(id: Post.pluck(:author_id))
end
def self.drafts
Post.where(published: false)
end
def self.best_post_ever
popularity.limit(1)
end
def self.popularity
Post.order(:popularity)
end
end
# 3. Mediators (Interactors, Managers, Commands, Service Objects)
#
# * The verbs of your business logic
# * They make your app actually do shit
# * Interact with other parts of your application (entities, other mediators)
# * Usually extracted from controllers
class RegistrationsController < ActionController::Base
def create
@user = User.new(params[:user])
if user.valid?
@user.token = SecureRandom.hex
@user.save
UserMailer.confirmation(user).deliver
redirect_to dashboard_path
else
render :new
end
end
end
#=================================================
class RegistrationsController < ActionController::Base
def create
@user = Registration.create(params[:user])
if @user.valid?
redirect_to dashboard_path
else
render :new
end
end
end
class Registration
def self.create(params[:user])
user = User.new(params[:user])
new(user).save
end
def initialize(user)
@user = user
end
def save
if valid?
@user.token = generate_token
@user.save
UserMailer.confirmation(user).deliver
end
@user
end
private
def generate_token
SecureRandom.hex
end
def valid?
@user.validates_presence [:email, :first_name, :last_name]
@user.validates_uniqueness [:email]
@user.errors.any?
end
end
# 4. Policies
#
# * Mediators do, Policies know
# * Command-Query separation (Mediator-Policies)
# * They encapsulate questions about parts of the system
class WeddingController < ActionController::Base
before_filter :check_permissions
def show
end
private
def check_permissions
WeddingPermissions.new(current_user).allowed?
end
end
class WeddingPermissions
def initialize(user)
@user = user
end
def allowed?
groom? || bride? || relative? || organizer?
end
private
def groom?
# ...
end
def bride?
# ...
end
def relative?
# ...
end
def organizer?
# ...
end
end
# 5. Serializers (or Presenters/Decorators/Exhibits/View Objects)
#
# * Format your data for the client
# * JSON (XML/SOAP/CSV)
# * Can also have complex logic (e.g. when to include which associations)
# * JBuilder, ActiveModel::Serializers, RABL, JSONAPI::Resources, RestPack::Serializer
class PostsController < ActionController::Base
def show
render json: Post.find(params[:id])
end
end
class PostSerializer
attribute :title, :body, :slug, :author_name, :guest_post, :created_at, :updated_at
has_many :comments
def author_name
author.first_name
end
def guest_post
author.guest?
end
def slug
"#{id}-#{title.parameterize}"
end
end
# 6. Controllers (Routing)
#
# * Handle request/response logic
# - HTTP statuses
# - Redirecting
# - Middlewares
# - Mounting
# - Caching
# - Cookies
# - Authorization headers
# - API versioning
#
# * Interact through mediators, finders, policies, serializers
# - The goal is to never reference entities directly
#########
# Rails #
#########
# config/routes.rb
resources :posts
# app/controllers/posts_controller.rb
class PostsController < ActionController::Base
def create
post = Mediators::Posts.create(params[:post])
if post.valid?
render json: post, status: :created
else
render json: {errors: post.errors}, status: :bad_request
end
end
rescue_from ActiveRecord::RecordNotFound do |error|
render json: {error: "Post wasn't found: #{error.record_id}"}, status: :not_found
end
end
#########
# Grape #
#########
class App < Grape:::API
use Rack::Protection
mount Refile::App => "/attachments"
post "/posts" do
post = Mediators::Posts.create(params[:post])
if post.valid?
status 201
post
else
status 400
{errors: post.errors}
end
end
rescue_from ActiveRecord::RecordNotFound do |error|
status 404
{error: "Post wasn't found: #{error.record_id}"}
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment