Skip to content

Commit

Permalink
Use a mixin for shared behaviour in services
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`.

This mixin replaces the inheritance approach used in services with an
ApplicationService class. These classes now all utilise this mixin
instead.

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 [1] 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 [2] 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 [3]. 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/rubocop-govuk#125
[2]: rubocop/rubocop#8506
[3]: https://thoughtbot.com/blog/back-to-basics-solid#liskov-substitution-principle
  • Loading branch information
kevindew committed Apr 26, 2021
1 parent 0dfe2e9 commit 5933f87
Show file tree
Hide file tree
Showing 23 changed files with 76 additions and 28 deletions.
7 changes: 0 additions & 7 deletions app/services/application_service.rb

This file was deleted.

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

def initialize(edition,
state:,
user: nil,
Expand Down
4 changes: 3 additions & 1 deletion app/services/create_document_service.rb
@@ -1,4 +1,6 @@
class CreateDocumentService < ApplicationService
class CreateDocumentService
include Callable

def initialize(document_type_id:,
content_id: SecureRandom.uuid,
locale: "en",
Expand Down
4 changes: 3 additions & 1 deletion app/services/create_file_attachment_blob_service.rb
@@ -1,4 +1,6 @@
class CreateFileAttachmentBlobService < ApplicationService
class CreateFileAttachmentBlobService
include Callable

def initialize(file:, filename:, user: nil)
@file = file
@filename = filename
Expand Down
4 changes: 3 additions & 1 deletion app/services/create_image_blob_service.rb
@@ -1,6 +1,8 @@
require "mini_magick"

class CreateImageBlobService < ApplicationService
class CreateImageBlobService
include Callable

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

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

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

def initialize(edition, user, **)
@edition = edition
@user = user
Expand Down
4 changes: 3 additions & 1 deletion app/services/discard_path_reservations_service.rb
@@ -1,4 +1,6 @@
class DiscardPathReservationsService < ApplicationService
class DiscardPathReservationsService
include Callable

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

def initialize(edition, user, **attributes)
@edition = edition
@user = user
Expand Down
4 changes: 3 additions & 1 deletion app/services/failsafe_draft_preview_service.rb
@@ -1,4 +1,6 @@
class FailsafeDraftPreviewService < ApplicationService
class FailsafeDraftPreviewService
include Callable

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

def initialize(edition, title:, max_repeated_titles: 1000)
@edition = edition
@title = title.to_s
Expand Down
3 changes: 2 additions & 1 deletion app/services/generate_unique_filename_service.rb
@@ -1,4 +1,5 @@
class GenerateUniqueFilenameService < ApplicationService
class GenerateUniqueFilenameService
include Callable
MAX_LENGTH = 65

def initialize(filename:, existing_filenames:)
Expand Down
4 changes: 3 additions & 1 deletion app/services/preview_asset_service.rb
@@ -1,4 +1,6 @@
class PreviewAssetService < ApplicationService
class PreviewAssetService
include Callable

def initialize(edition, asset, **)
@edition = edition
@asset = asset
Expand Down
4 changes: 3 additions & 1 deletion app/services/preview_draft_edition_service.rb
@@ -1,4 +1,6 @@
class PreviewDraftEditionService < ApplicationService
class PreviewDraftEditionService
include Callable

def initialize(edition, republish: false)
@edition = edition
@republish = republish
Expand Down
4 changes: 3 additions & 1 deletion app/services/publish_assets_service.rb
@@ -1,4 +1,6 @@
class PublishAssetsService < ApplicationService
class PublishAssetsService
include Callable

def initialize(edition, superseded_edition: nil)
@edition = edition
@superseded_edition = superseded_edition
Expand Down
4 changes: 3 additions & 1 deletion app/services/publish_draft_edition_service.rb
@@ -1,4 +1,6 @@
class PublishDraftEditionService < ApplicationService
class PublishDraftEditionService
include Callable

def initialize(edition, user, with_review:)
@edition = edition
@user = user
Expand Down
4 changes: 3 additions & 1 deletion app/services/remove_document_service.rb
@@ -1,4 +1,6 @@
class RemoveDocumentService < ApplicationService
class RemoveDocumentService
include Callable

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

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

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

def initialize(edition, user, scheduling, **)
@edition = edition
@user = user
Expand Down
4 changes: 3 additions & 1 deletion app/services/withdraw_document_service.rb
@@ -1,4 +1,6 @@
class WithdrawDocumentService < ApplicationService
class WithdrawDocumentService
include Callable

def initialize(edition, user, public_explanation:)
@edition = edition
@public_explanation = public_explanation
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(*args, **kwargs, &block)
new(*args, **kwargs, &block).call
end

private_class_method :new
end
end

0 comments on commit 5933f87

Please sign in to comment.