Created
May 27, 2015 17:50
-
-
Save janko/c3c7954510840d8df2fd to your computer and use it in GitHub Desktop.
Components of good application design (my presentation for a local Ruby meetup)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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