{"id":14955770,"url":"https://github.com/selleo/pattern","last_synced_at":"2025-04-11T09:29:15.907Z","repository":{"id":19839728,"uuid":"88041981","full_name":"Selleo/pattern","owner":"Selleo","description":"A collection of lightweight, standardized, rails-oriented patterns.","archived":false,"fork":false,"pushed_at":"2024-08-16T15:26:29.000Z","size":108,"stargazers_count":707,"open_issues_count":6,"forks_count":40,"subscribers_count":44,"default_branch":"master","last_synced_at":"2024-10-29T14:15:47.367Z","etag":null,"topics":["design-patterns","rails-oriented-patterns","ruby-gem","ruby-on-rails"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Selleo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-04-12T10:58:00.000Z","updated_at":"2024-10-27T03:25:36.000Z","dependencies_parsed_at":"2024-01-13T02:58:13.479Z","dependency_job_id":"2f392e4b-2646-494e-a3b2-bd20fd9f87ef","html_url":"https://github.com/Selleo/pattern","commit_stats":{"total_commits":95,"total_committers":13,"mean_commits":"7.3076923076923075","dds":0.6105263157894737,"last_synced_commit":"fda5fdf0ef29ec81789388dcc26b261af436816b"},"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Selleo%2Fpattern","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Selleo%2Fpattern/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Selleo%2Fpattern/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Selleo%2Fpattern/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Selleo","download_url":"https://codeload.github.com/Selleo/pattern/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248367996,"owners_count":21092282,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["design-patterns","rails-oriented-patterns","ruby-gem","ruby-on-rails"],"created_at":"2024-09-24T13:11:44.609Z","updated_at":"2025-04-11T09:29:15.879Z","avatar_url":"https://github.com/Selleo.png","language":"Ruby","readme":"![](https://github.com/Selleo/pattern/workflows/Ruby/badge.svg)\n\n# Pattern\n\nA collection of lightweight, standardized, rails-oriented patterns used by [RubyOnRails Developers @ Selleo](https://selleo.com/ruby-on-rails)\n\n- [Query - complex querying on active record relation](#query)\n- [Service - useful for handling processes involving multiple steps](#service)\n- [Collection - when in need to add a method that relates to the collection as whole](#collection)\n- [Form - when you need a place for callbacks, want to replace strong parameters or handle virtual/composite resources](#form)\n- [Calculation - when you need a place for calculating a simple value (numeric, array, hash) and/or cache it](#calculation)\n- [Rule and Ruleset - when you need a place for conditional logic](#rule-and-ruleset)\n\n## Installation\n\n```ruby\n# Gemfile\n\n#...\ngem \"rails-patterns\"\n#...\n```\n\nThen `bundle install`\n\n## Query\n\n### When to use it\n\nOne should consider using query objects pattern when in need to perform complex querying on active record relation.\nUsually one should avoid using scopes for such purpose.\nAs 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.\nAlso whenever a chain of scopes is to be used, one should consider using query object too.\nSome more information on using query objects can be found in [this article](https://medium.com/@blazejkosmowski/essential-rubyonrails-patterns-part-2-query-objects-4b253f4f4539).\n\n### Assumptions and rules\n\n* Query objects are always used by calling class-level `.call` method\n* Query objects require `ActiveRecord::Relation` or `ActiveRecord::Base` as constructor argument\n* Default relation (see above) can be defined by using `queries` macro\n* Query objects have to implement `#query` method that returns `ActiveRecord::Relation`\n* Query objects provide access to consecutive keyword arguments using `#options` hash\n\n### Other\n\nBecause of the fact, that QueryObject implements `.call` method, those can be used to construct scopes if required. ([read more...](http://craftingruby.com/posts/2015/06/29/query-objects-through-scopes.html))\n\n### Examples\n\n#### Declaration\n\n```ruby\nclass RecentlyActivatedUsersQuery \u003c Patterns::Query\n  queries User\n\n  private\n\n  def query\n    relation.active.where(activated_at: date_range)\n  end\n\n  def date_range\n    options.fetch(:date_range, default_date_range)\n  end\n\n  def default_date_range\n    Date.yesterday.beginning_of_day..Date.today.end_of_day\n  end\nend\n```\n\n#### Usage\n\n```ruby\nRecentlyActivatedUsersQuery.call\nRecentlyActivatedUsersQuery.call(User.without_test_users)\nRecentlyActivatedUsersQuery.call(date_range: Date.today.beginning_of_day..Date.today.end_of_day)\nRecentlyActivatedUsersQuery.call(User.without_test_users, date_range: Date.today.beginning_of_day..Date.today.end_of_day)\n\nclass User \u003c ApplicationRecord\n  scope :recently_activated, RecentlyActivatedUsersQuery\nend\n```\n\n## Service\n\n### When to use it\n\nService objects are commonly used to mitigate problems with model callbacks that interact with external classes ([read more...](http://samuelmullen.com/2013/05/the-problem-with-rails-callbacks/)).\nService 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](https://sourcemaking.com/refactoring/replace-method-with-method-object). Some more information on using services can be found in [this article](https://medium.com/selleo/essential-rubyonrails-patterns-part-1-service-objects-1af9f9573ca1).\n\n### Assumptions and rules\n\n* Service objects are always used by calling class-level `.call` method\n* Service objects have to implement `#call` method\n* Calling service object's `.call` method executes `#call` and returns service object instance\n* A result of `#call` method is accessible through `#result` method\n* It is recommended for `#call` method to be the only public method of service object (besides state readers)\n* It is recommended to name service object classes after commands (e.g. `ActivateUser` instead of `UserActivation`)\n\n### Other\n\nA bit higher level of abstraction is provided by [business_process gem](https://github.com/Selleo/business_process).\n\n### Examples\n\n#### Declaration\n\n```ruby\nclass ActivateUser \u003c Patterns::Service\n  def initialize(user)\n    @user = user\n  end\n\n  def call\n    user.activate!\n    NotificationsMailer.user_activation_notification(user).deliver_now\n    user\n  end\n\n  private\n\n  attr_reader :user\nend\n```\n\n#### Usage\n\n```ruby\n  user_activation = ActivateUser.call(user)\n  user_activation.result # \u003cUser id: 5803143, email: \"tony@patterns.dev ...\n```\n\n## Collection\n\n### When to use it\n\nOne should consider using collection pattern when in need to add a method that relates to the collection a whole.\nPopular example for such situation is for paginated collections, where for instance `#current_page` getter makes sense only in collection context.\nAlso collections can be used as a container for mapping or grouping logic (especially if the mapping is not 1-1 in terms of size).\nCollection might also act as a replacement for models not inheriting from ActiveRecord::Base (e.g. `StatusesCollection`, `ColorsCollection` etc.).\nWhat 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.\n\n### Assumptions and rules\n\n* Collections include `Enumerable`\n* Collections can be initialized using `.new`, `.from` and `.for` (aliases)\n* Collections have to implement `#collection` method that returns object responding to `#each`\n* Collections provide access to consecutive keyword arguments using `#options` hash\n* Collections provide access to first argument using `#subject`\n\n### Examples\n\n#### Declaration\n\n```ruby\nclass ColorsCollection \u003c Patterns::Collection\n  AVAILABLE_COLORS = { red: \"#FF0000\", green: \"#00FF00\", blue: \"#0000FF\" }\n\n  private\n\n  def collection\n    AVAILABLE_COLORS\n  end\nend\n\nclass CustomerEventsByTypeCollection \u003c Patterns::Collection\n  private\n\n  def collection\n    subject.\n    events.\n    group_by(\u0026:type).\n    transform_values{ |events| events.map{ |e| e.public_send(options.fetch(:label_method, \"description\")) }}\n  end\nend\n```\n\n#### Usage\n\n```ruby\nColorsCollection.new\nCustomerEventsByTypeCollection.for(customer)\nCustomerEventsByTypeCollection.for(customer, label_method: \"name\")\n```\n\n## Form\n\n### When to use it\n\nForm objects, just like service objects, are commonly used to mitigate problems with model callbacks that interact with external classes ([read more...](http://samuelmullen.com/2013/05/the-problem-with-rails-callbacks/)).\nForm objects can also be used as replacement for `ActionController::StrongParameters` strategy, as all writable attributes are re-defined within each form.\nFinally form objects can be used as wrappers for virtual (with no model representation) or composite (saving multiple models at once) resources.\nIn the latter case this may act as replacement for `ActiveRecord::NestedAttributes`.\nIn some cases FormObject can be used as scaffolding for [replace method with object refactoring](https://sourcemaking.com/refactoring/replace-method-with-method-object). Some more information on using form objects can be found in [this article](https://medium.com/selleo/essential-rubyonrails-patterns-form-objects-b199aada6ec9).\n\n### Assumptions and rules\n\n* Forms include `ActiveModel::Validations` to support validation.\n* Forms include `Virtus.model` to support `attribute` static method with all [corresponding capabilities](https://github.com/solnic/virtus).\n* Forms can be initialized using `.new`.\n* Forms accept optional resource object as first constructor argument.\n* Forms accept optional attributes hash as latter constructor argument.\n* Forms have to implement `#persist` method that returns falsey (if failed) or truthy (if succeeded) value.\n* Forms provide access to first constructor argument using `#resource`.\n* Forms are saved using their `#save` or `#save!` methods.\n* Forms will attempt to pre-populate their fields using `resource#attributes` and public getters for `resource`\n* Form's fields are populated with passed-in attributes hash reverse-merged with pre-populated attributes if possible.\n* Forms provide `#as` builder method that populates internal `@form_owner` variable (can be used to store current user).\n* Forms allow defining/overriding their `#param_key` method result by using `.param_key` static method. This defaults to `#resource#model_name#param_key`.\n* Forms delegate `#persisted?` method to `#resource` if possible.\n* Forms do handle `ActionController::Parameters` as attributes hash (using `to_unsafe_h`)\n* It is recommended to wrap `#persist` method in transaction if possible and if multiple model are affected.\n\n### Examples\n\n#### Declaration\n\n```ruby\nclass UserForm \u003c Patterns::Form\n  param_key \"person\"\n\n  attribute :first_name, String\n  attribute :last_name, String\n  attribute :age, Integer\n  attribute :full_address, String\n  attribute :skip_notification, Boolean\n\n  validate :first_name, :last_name, presence: true\n\n  private\n\n  def persist\n    update_user and\n      update_address and\n      deliver_notification\n  end\n\n  def update_user\n    resource.update_attributes(attributes.except(:full_address, :skip_notification))\n  end\n\n  def update_address\n    resource.address.update_attributes(full_address: full_address)\n  end\n\n  def deliver_notification\n    skip_notification || UserNotifier.user_update_notification(user, form_owner).deliver\n  end\nend\n\nclass ReportConfigurationForm \u003c Patterns::Form\n  param_key \"report\"\n\n  attribute :include_extra_data, Boolean\n  attribute :dump_as_csv, Boolean\n  attribute :comma_separated_column_names, String\n  attribute :date_start, Date\n  attribute :date_end, Date\n\n  private\n\n  def persist\n    SendReport.call(attributes)\n  end\nend\n```\n\n#### Usage\n\n```ruby\nform = UserForm.new(User.find(1), params[:person])\nform.save\n\nform = UserForm.new(User.new, params[:person]).as(current_user)\nform.save!\n\nReportConfigurationForm.new\nReportConfigurationForm.new({ include_extra_data: true, dump_as_csv: true })\n```\n\n## Calculation\n\n### When to use it\n\nCalculation 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.\nCalculation objects also provide simple abstraction for caching their results.\n\n### Assumptions and rules\n\n* Calculations have to implement `#result` method that returns any value (result of calculation).\n* Calculations do provide `.set_cache_expiry_every` method, that allows defining caching period.\n* When `.set_cache_expiry_every` is not used, result is not being cached.\n* Calculations return result by calling any of following methods: `.calculate`, `.result_for` or `.result`.\n* First argument passed to calculation is accessible by `#subject` private method.\n* Arguments hash passed to calculation is accessible by `#options` private method.\n* Caching takes into account arguments passed when building cache key.\n* To build cache key, `#cache_key` of each argument value is used if possible.\n* By default `Rails.cache` is used as cache store.\n\n### Examples\n\n#### Declaration\n\n```ruby\nclass AverageHotelDailyRevenue \u003c Patterns::Calculation\n  set_cache_expiry_every 1.day\n\n  private\n\n  def result\n    reservations.sum(:price) / days_in_year\n  end\n\n  def reservations\n    Reservation.where(\n      date: (beginning_of_year..end_of_year),\n      hotel_id: subject.id\n    )\n  end\n\n  def days_in_year\n    end_of_year.yday\n  end\n\n  def year\n    options.fetch(:year, Date.current.year)\n  end\n\n  def beginning_of_year\n    Date.new(year).beginning_of_year\n  end\n\n  def end_of_year\n    Date.new(year).end_of_year\n  end\nend\n```\n\n#### Usage\n\n```ruby\nhotel = Hotel.find(123)\nAverageHotelDailyRevenue.result_for(hotel)\nAverageHotelDailyRevenue.result_for(hotel, year: 2015)\n\nTotalCurrentRevenue.calculate\nAverageDailyRevenue.result\n```\n\n## Rule and Ruleset\n\n### When to use it\n\nRule objects provide a place for dislocating/extracting conditional logic.\n\nUse it when:\n- given complex condition is duplicated in multiple places in your codebase\n- part of condition logic can be reused in some other place\n- there is a need to instantiate condition itself for some reason (i.e. to represent it in the interface)\n- responsibility of your class is blurred by complex conditional logic, and as a result...\n- ...tests for your class require multiple condition branches / nested contexts\n\n### Assumptions and rules\n\n* Rule has `#satisfied?`, `#applicable?`, `#not_applicable?` and `#forceable?` methods available.\n* Rule has to implement at least `#satisfied?` method. `#not_applicable?` and `#forceable?` are meant to be overridable.\n* `#forceable?` makes sense in scenario where condition is capable of being force-satisfied regardless if its actually satisfied or not. Is `true` by default.\n* Override `#not_applicable?` when method is applicable only under some specific conditions. Is `false` by default.\n* Rule requires a subject as first argument.\n* Multiple rules and rulesets can be combined into new ruleset as both share same interface and can be used interchangeably (composite pattern).\n* By default empty ruleset is satisfied.\n\n#### Forcing rules\n\nOn some occasions there is a situation in which some condition should be overridable.\nLet'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.\nIn 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.\n\nIt might be good idea to test for `#forceable?` on the UI level to control visibility of such link/button.\n\nOverriding `#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.\nIf not, ruleset containing such rule (and the rule itself) would not be \"forceable\" and UI could reflect that by querying `#forceable?`.\n\n#### Regular and strong rulesets\n\nWhile regular `Ruleset` can be satisfied or forced if any of its rules in not applicable, the\n`StrongRuleset` is not satisfied and not \"forceable\" if any of its rules is not applicable.\n\n#### `#not_applicable?` vs `#applicable?`\n\nIt might be surprising that is is the negated version of the `#applicable?` predicate methods that is overridable.\nHowever, from the actual usage perspective, it usually easier to conceptually define when condition makes no sense than other way around.\n\n### Examples\n\n#### Declaration\n\n```ruby\nclass OrderIsSentRule \u003c Patterns::Rule\n  def satisfied?\n    subject.sent?\n  end\nend\n\nclass OrderIsPaidRule \u003c Patterns::Rule\n  def satisfied?\n    subject.paid?\n  end\n\n  def forceable?\n    true\n  end\nend\n\nOrderCompletedNotificationRuleset = Class.new(Patterns::Ruleset)\nOrderCompletedNotificationRuleset.\n  add_rule(:order_is_sent_rule).\n  add_rule(:order_is_paid_rule)\n```\n\n#### Usage\n\n```ruby\nOrderIsPaidRule.new(order).satisfied?\nOrderCompletedNotificationRuleset.new(order).satisfied?\n\nResendOrderNotification.call(order) if OrderCompletedNotificationRuleset.new(order).satisfied?(force: true)\n```\n\n## Further reading\n\n* [7 ways to decompose fat active record models](http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/)\n\n## About Selleo\n\n![selleo](https://raw.githubusercontent.com/Selleo/selleo-resources/master/public/github_footer.png)\n\nSoftware 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.\n\nAll names and logos for [Selleo](https://selleo.com/about) are trademark of Selleo Labs Sp. z o.o. (formerly Selleo Sp. z o.o. Sp.k.)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fselleo%2Fpattern","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fselleo%2Fpattern","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fselleo%2Fpattern/lists"}