Skip to content

Commit

Permalink
Use a mixin for common .call class interface
Browse files Browse the repository at this point in the history
This creates a new mixin, Callable, which can be used to provide a class
with the boilerplate so that it can have a public class method of
`.call` which calls `#initialize` with the arguments and then `#call`.

The creation of this class is a port of the same approach applied in
Content Publisher [1] (and most of this commit message is a port too in
case you're feeling a touch of déjà vu).

This mixin replaces the inheritance approach used in builders,
presenters and services.

The change was prompted by a linting violation with one of rubocop's newer
rules: `Lint/MissingSuper`, which is an offence when a class inherits but
does not call `super` in it's initialize method. This violation seemed
to only affect some of GOV.UK's newer code, such as this service
pattern, which led to some debate [2] about what the most
appropriate means to share this behaviour is, whether our
inheritance approach was actually idiomatic and how using super in every
`initialize` would be unnecessary boilerplate.

The Rubocop thread [3] regarding this rule raised an interesting point
"I'm curious of examples where there are good reasons to explicitly not
call super.", which led to me considering why we didn't think `super`
would be appropriate in these classes. My conclusion to this was that we
weren't actually intending to create a subtype with inheritance, we
actually just wanted standardisation with the interface, and that the
inherited classes have very little behavioural consistency, breaking
Liskov substitution principle [4]. When looking at the Ruby standard
library a module seems the most appropriate way to implement it,
considering similarities with Singleton and Enumerable.

[1]: alphagov/content-publisher@5933f87
[2]: alphagov/rubocop-govuk#125
[3]: rubocop/rubocop#8506
[4]: https://thoughtbot.com/blog/back-to-basics-solid#liskov-substitution-principle
  • Loading branch information
kevindew committed May 17, 2021
1 parent 1714681 commit 2d06d6d
Show file tree
Hide file tree
Showing 26 changed files with 80 additions and 43 deletions.
7 changes: 0 additions & 7 deletions app/builders/application_builder.rb

This file was deleted.

4 changes: 3 additions & 1 deletion app/builders/bulk_subscriber_list_email_builder.rb
@@ -1,4 +1,6 @@
class BulkSubscriberListEmailBuilder < ApplicationBuilder
class BulkSubscriberListEmailBuilder
include Callable

BATCH_SIZE = 5000

def initialize(subject:, body:, subscriber_lists:)
Expand Down
4 changes: 3 additions & 1 deletion app/builders/digest_email_builder.rb
@@ -1,4 +1,6 @@
class DigestEmailBuilder < ApplicationBuilder
class DigestEmailBuilder
include Callable

def initialize(content:, subscription:)
@content = content
@subscription = subscription
Expand Down
4 changes: 3 additions & 1 deletion app/builders/immediate_email_builder.rb
@@ -1,4 +1,6 @@
class ImmediateEmailBuilder < ApplicationBuilder
class ImmediateEmailBuilder
include Callable

def initialize(content, subscriptions)
@content = content
@subscriptions = subscriptions
Expand Down
4 changes: 3 additions & 1 deletion app/builders/subscriber_auth_email_builder.rb
@@ -1,4 +1,6 @@
class SubscriberAuthEmailBuilder < ApplicationBuilder
class SubscriberAuthEmailBuilder
include Callable

def initialize(subscriber:, destination:, token:)
@subscriber = subscriber
@destination = destination
Expand Down
4 changes: 3 additions & 1 deletion app/builders/subscription_auth_email_builder.rb
@@ -1,4 +1,6 @@
class SubscriptionAuthEmailBuilder < ApplicationBuilder
class SubscriptionAuthEmailBuilder
include Callable

def initialize(address:, token:, subscriber_list:, frequency:)
@address = address
@token = token
Expand Down
4 changes: 3 additions & 1 deletion app/builders/subscription_confirmation_email_builder.rb
@@ -1,4 +1,6 @@
class SubscriptionConfirmationEmailBuilder < ApplicationBuilder
class SubscriptionConfirmationEmailBuilder
include Callable

def initialize(subscription:)
@subscription = subscription
@subscriber = subscription.subscriber
Expand Down
7 changes: 0 additions & 7 deletions app/presenters/application_presenter.rb

This file was deleted.

4 changes: 3 additions & 1 deletion app/presenters/bulk_email_body_presenter.rb
@@ -1,4 +1,6 @@
class BulkEmailBodyPresenter < ApplicationPresenter
class BulkEmailBodyPresenter
include Callable

def initialize(body, subscriber_list)
@body = body
@subscriber_list = subscriber_list
Expand Down
4 changes: 3 additions & 1 deletion app/presenters/content_change_presenter.rb
@@ -1,6 +1,8 @@
require "redcarpet/render_strip"

class ContentChangePresenter < ApplicationPresenter
class ContentChangePresenter
include Callable

EMAIL_DATE_FORMAT = "%l:%M%P, %-d %B %Y".freeze

def initialize(content_change, subscription)
Expand Down
4 changes: 3 additions & 1 deletion app/presenters/footer_presenter.rb
@@ -1,4 +1,6 @@
class FooterPresenter < ApplicationPresenter
class FooterPresenter
include Callable

def initialize(subscriber, subscription)
@subscription = subscription
@subscriber = subscriber
Expand Down
4 changes: 3 additions & 1 deletion app/presenters/message_presenter.rb
@@ -1,4 +1,6 @@
class MessagePresenter < ApplicationPresenter
class MessagePresenter
include Callable

def initialize(message, _subscription = nil)
@message = message
end
Expand Down
4 changes: 3 additions & 1 deletion app/presenters/source_url_presenter.rb
@@ -1,4 +1,6 @@
class SourceUrlPresenter < ApplicationPresenter
class SourceUrlPresenter
include Callable

def initialize(url, utm_source:, utm_content:)
@url = url
@utm_source = utm_source
Expand Down
7 changes: 0 additions & 7 deletions app/services/application_service.rb

This file was deleted.

4 changes: 3 additions & 1 deletion app/services/auth_token_generator_service.rb
@@ -1,4 +1,6 @@
class AuthTokenGeneratorService < ApplicationService
class AuthTokenGeneratorService
include Callable

CIPHER = "aes-256-gcm".freeze
OPTIONS = { cipher: CIPHER, serializer: JSON }.freeze

Expand Down
4 changes: 3 additions & 1 deletion app/services/content_change_handler_service.rb
@@ -1,4 +1,6 @@
class ContentChangeHandlerService < ApplicationService
class ContentChangeHandlerService
include Callable

def initialize(params:, govuk_request_id:, user: nil)
@params = params
@govuk_request_id = govuk_request_id
Expand Down
4 changes: 3 additions & 1 deletion app/services/create_subscriber_list_service.rb
@@ -1,4 +1,6 @@
class CreateSubscriberListService < ApplicationService
class CreateSubscriberListService
include Callable

def initialize(title:, url:, matching_criteria:, user:)
@title = title
@url = url
Expand Down
4 changes: 3 additions & 1 deletion app/services/create_subscription_service.rb
@@ -1,4 +1,6 @@
class CreateSubscriptionService < ApplicationService
class CreateSubscriptionService
include Callable

attr_reader :subscriber_list, :subscriber, :frequency, :current_user

def initialize(subscriber_list, subscriber, frequency, current_user)
Expand Down
4 changes: 3 additions & 1 deletion app/services/digest_initiator_service.rb
@@ -1,4 +1,6 @@
class DigestInitiatorService < ApplicationService
class DigestInitiatorService
include Callable

def initialize(date:, range:)
@range = range
@date = date
Expand Down
4 changes: 3 additions & 1 deletion app/services/immediate_email_generation_service.rb
@@ -1,4 +1,6 @@
class ImmediateEmailGenerationService < ApplicationService
class ImmediateEmailGenerationService
include Callable

BATCH_SIZE = 5000

def initialize(content, **)
Expand Down
4 changes: 3 additions & 1 deletion app/services/matched_content_change_generation_service.rb
@@ -1,4 +1,6 @@
class MatchedContentChangeGenerationService < ApplicationService
class MatchedContentChangeGenerationService
include Callable

def initialize(content_change, **)
@content_change = content_change
end
Expand Down
4 changes: 3 additions & 1 deletion app/services/matched_message_generation_service.rb
@@ -1,4 +1,6 @@
class MatchedMessageGenerationService < ApplicationService
class MatchedMessageGenerationService
include Callable

def initialize(message, **)
@message = message
end
Expand Down
4 changes: 3 additions & 1 deletion app/services/message_handler_service.rb
@@ -1,4 +1,6 @@
class MessageHandlerService < ApplicationService
class MessageHandlerService
include Callable

def initialize(params:, govuk_request_id:, user: nil)
@params = params
@govuk_request_id = govuk_request_id
Expand Down
4 changes: 3 additions & 1 deletion app/services/send_email_service.rb
@@ -1,4 +1,6 @@
class SendEmailService < ApplicationService
class SendEmailService
include Callable

class NotifyCommunicationFailure < RuntimeError; end

def initialize(email:, metrics: {})
Expand Down
4 changes: 3 additions & 1 deletion app/services/unsubscribe_all_service.rb
@@ -1,4 +1,6 @@
class UnsubscribeAllService < ApplicationService
class UnsubscribeAllService
include Callable

attr_reader :subscriber, :reason

def initialize(subscriber, reason, **)
Expand Down
14 changes: 14 additions & 0 deletions lib/callable.rb
@@ -0,0 +1,14 @@
# This mixin is to provide the boilerplate for classes that implement a single
# public class method of `.call` - typically used by objects that are entirely
# to perform a business transaction.
module Callable
extend ActiveSupport::Concern

included do
def self.call(...)
new(...).call
end

private_class_method :new
end
end

0 comments on commit 2d06d6d

Please sign in to comment.