| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
A collection of lightweight, standardized, rails-oriented patterns used by RubyOnRails Developers @ Selleo
# Gemfile
#...
gem "rails-patterns"
#...Then bundle install
One should consider using query objects pattern when in need to perform complex querying on active record relation. Usually one should avoid using scopes for such purpose. As a rule of thumb, if scope interacts with more than one column and/or joins in other tables, it should be moved to query object. Also whenever a chain of scopes is to be used, one should consider using query object too. Some more information on using query objects can be found in this article.
Because of the fact, that QueryObject implements .call method, those can be used to construct scopes if required. (read more...)
class RecentlyActivatedUsersQuery < Patterns::Query
queries User
private
def query
relation.active.where(activated_at: date_range)
end
def date_range
options.fetch(:date_range, default_date_range)
end
def default_date_range
Date.yesterday.beginning_of_day..Date.today.end_of_day
end
endRecentlyActivatedUsersQuery.call
RecentlyActivatedUsersQuery.call(User.without_test_users)
RecentlyActivatedUsersQuery.call(date_range: Date.today.beginning_of_day..Date.today.end_of_day)
RecentlyActivatedUsersQuery.call(User.without_test_users, date_range: Date.today.beginning_of_day..Date.today.end_of_day)
class User < ApplicationRecord
scope :recently_activated, RecentlyActivatedUsersQuery
endService objects are commonly used to mitigate problems with model callbacks that interact with external classes (read more...). Service objects are also useful for handling processes involving multiple steps. E.g. a controller that performs more than one operation on its subject (usually a model instance) is a possible candidate for Extract ServiceObject (or Extract FormObject) refactoring. In many cases service object can be used as scaffolding for replace method with object refactoring. Some more information on using services can be found in this article.
A bit higher level of abstraction is provided by business_process gem.
class ActivateUser < Patterns::Service
def initialize(user)
@user = user
end
def call
user.activate!
NotificationsMailer.user_activation_notification(user).deliver_now
user
end
private
attr_reader :user
end user_activation = ActivateUser.call(user)
user_activation.result # <User id: 5803143, email: "tony@patterns.dev ...One should consider using collection pattern when in need to add a method that relates to the collection a whole. Popular example for such situation is for paginated collections, where for instance #current_page getter makes sense only in collection context. Also collections can be used as a container for mapping or grouping logic (especially if the mapping is not 1-1 in terms of size). Collection might also act as a replacement for models not inheriting from ActiveRecord::Base (e.g. StatusesCollection, ColorsCollection etc.). What is more, collections can be used if we need to encapsulate "flagging" logic - for instance if we need to render a separator element between collection elements based on some specific logic, we can move this logic from view layer to collection and yield an additional flag to control rendering in view.
class ColorsCollection < Patterns::Collection
AVAILABLE_COLORS = { red: "#FF0000", green: "#00FF00", blue: "#0000FF" }
private
def collection
AVAILABLE_COLORS
end
end
class CustomerEventsByTypeCollection < Patterns::Collection
private
def collection
subject.
events.
group_by(&:type).
transform_values{ |events| events.map{ |e| e.public_send(options.fetch(:label_method, "description")) }}
end
endColorsCollection.new
CustomerEventsByTypeCollection.for(customer)
CustomerEventsByTypeCollection.for(customer, label_method: "name")Form objects, just like service objects, are commonly used to mitigate problems with model callbacks that interact with external classes (read more...). Form objects can also be used as replacement for ActionController::StrongParameters strategy, as all writable attributes are re-defined within each form. Finally form objects can be used as wrappers for virtual (with no model representation) or composite (saving multiple models at once) resources. In the latter case this may act as replacement for ActiveRecord::NestedAttributes. In some cases FormObject can be used as scaffolding for replace method with object refactoring. Some more information on using form objects can be found in this article.
class UserForm < Patterns::Form
param_key "person"
attribute :first_name, String
attribute :last_name, String
attribute :age, Integer
attribute :full_address, String
attribute :skip_notification, Boolean
validate :first_name, :last_name, presence: true
private
def persist
update_user and
update_address and
deliver_notification
end
def update_user
resource.update_attributes(attributes.except(:full_address, :skip_notification))
end
def update_address
resource.address.update_attributes(full_address: full_address)
end
def deliver_notification
skip_notification || UserNotifier.user_update_notification(user, form_owner).deliver
end
end
class ReportConfigurationForm < Patterns::Form
param_key "report"
attribute :include_extra_data, Boolean
attribute :dump_as_csv, Boolean
attribute :comma_separated_column_names, String
attribute :date_start, Date
attribute :date_end, Date
private
def persist
SendReport.call(attributes)
end
endform = UserForm.new(User.find(1), params[:person])
form.save
form = UserForm.new(User.new, params[:person]).as(current_user)
form.save!
ReportConfigurationForm.new
ReportConfigurationForm.new({ include_extra_data: true, dump_as_csv: true })Calculation objects provide a place to calculate simple values (i.e. numeric, arrays, hashes), especially when calculations require interacting with multiple classes, and thus do not fit into any particular one. Calculation objects also provide simple abstraction for caching their results.
class AverageHotelDailyRevenue < Patterns::Calculation
set_cache_expiry_every 1.day
private
def result
reservations.sum(:price) / days_in_year
end
def reservations
Reservation.where(
date: (beginning_of_year..end_of_year),
hotel_id: subject.id
)
end
def days_in_year
end_of_year.yday
end
def year
options.fetch(:year, Date.current.year)
end
def beginning_of_year
Date.new(year).beginning_of_year
end
def end_of_year
Date.new(year).end_of_year
end
endhotel = Hotel.find(123)
AverageHotelDailyRevenue.result_for(hotel)
AverageHotelDailyRevenue.result_for(hotel, year: 2015)
TotalCurrentRevenue.calculate
AverageDailyRevenue.resultRule objects provide a place for dislocating/extracting conditional logic.
Use it when:
On some occasions there is a situation in which some condition should be overridable. Let's say we may want send shipping notification even though given order was not paid for and under regular circumstances such notification should not be sent. In this case, while regular logic with some automated process would not trigger delivery, an action triggered by user from UI could do it, by passing force: true option to #satisified? methods.
It might be good idea to test for #forceable? on the UI level to control visibility of such link/button.
Overriding #forceable can be useful to prevent some edge cases, i.e. ContactInformationProvidedRule might check if customer for given order has provided any contact means by which a notification could be delivered. If not, ruleset containing such rule (and the rule itself) would not be "forceable" and UI could reflect that by querying #forceable?.
While regular Ruleset can be satisfied or forced if any of its rules in not applicable, the StrongRuleset is not satisfied and not "forceable" if any of its rules is not applicable.
It might be surprising that is is the negated version of the #applicable? predicate methods that is overridable. However, from the actual usage perspective, it usually easier to conceptually define when condition makes no sense than other way around.
class OrderIsSentRule < Patterns::Rule
def satisfied?
subject.sent?
end
end
class OrderIsPaidRule < Patterns::Rule
def satisfied?
subject.paid?
end
def forceable?
true
end
end
OrderCompletedNotificationRuleset = Class.new(Patterns::Ruleset)
OrderCompletedNotificationRuleset.
add_rule(:order_is_sent_rule).
add_rule(:order_is_paid_rule)OrderIsPaidRule.new(order).satisfied?
OrderCompletedNotificationRuleset.new(order).satisfied?
ResendOrderNotification.call(order) if OrderCompletedNotificationRuleset.new(order).satisfied?(force: true)Software development teams with an entrepreneurial sense of ownership at their core delivering great digital products and building culture people want to belong to. We are a community of engaged co-workers passionate about crafting impactful web solutions which transform the way our clients do business.
All names and logos for Selleo are trademark of Selleo Labs Sp. z o.o. (formerly Selleo Sp. z o.o. Sp.k.)
| Back | FazBrowse Home | New Git URL |