Skip to content

Instantly share code, notes, and snippets.

@rondy
Last active March 2, 2021 11:14
Show Gist options
  • Save rondy/a9f167cca216dfb51d1dc1fac56755b1 to your computer and use it in GitHub Desktop.
Save rondy/a9f167cca216dfb51d1dc1fac56755b1 to your computer and use it in GitHub Desktop.
class ReviseElixirRadarEntry
def call(entry)
result = Validation() do
check { check_url_points_to_an_existing_page(entry[:url]) }
check { check_domain_matches_url_host(entry[:domain], entry[:url]) }
check { check_entry_title_matches_page_title(entry[:url], entry[:title]) }
check { check_utm_campaign_is_valid(entry[:url]) }
end
review_result = {
title: entry[:title]
}
case result
when Success
review_result[:status] = 'valid'
when Failure
review_result[:status] = 'invalid'
review_result[:errors] = result
end
review_result
end
private
def check_url_points_to_an_existing_page(url)
if CheckUrlPointsToAnExistingPage.new.call(url)
Success(url)
else
Failure('page_not_found')
end
end
def check_domain_matches_url_host(domain, url)
# case CheckDomainMatchesUrlHost.new.call(domain, url)
# when true then Success(url)
# when false then Failure('domain_does_not_match')
# end
if CheckDomainMatchesUrlHost.new.call(domain, url)
Success(url)
else
Failure('domain_does_not_match')
end
end
def check_entry_title_matches_page_title(url, entry_title)
if CheckEntryTitleMatchesPageTitle.new.call(url, entry_title)
Success(url)
else
Failure('page_title_does_not_match')
end
end
def check_utm_campaign_is_valid(url)
if CheckUtmCampaignIsValid.new.call(url)
Success(url)
else
Failure('wrong_utm_campaign')
end
end
end
class CheckEntryTitleMatchesPageTitle
def initialize(get_page_title: nil)
@get_page_title = get_page_title
end
def call(url, entry_title)
page_title = get_page_title.call(url)
normalized_page_title =
normalize_page_title(page_title)
entry_title_matches_page_title =
entry_title_matches_page_title?(normalized_page_title, entry_title)
[entry_title_matches_page_title, normalized_page_title]
end
private
def normalize_page_title(page_title)
page_title
.gsub(/\s+/, ' ')
.strip
end
def get_page_title(url)
@get_page_title ||= begin
lambda do |url|
GetPageTitle
.new(with: :mechanize)
.call(url)
end
end
end
def entry_title_matches_page_title?(normalized_page_title, entry_title)
(normalized_page_title =~ Regexp.new(entry_title.strip))
end
end
class GetPageTitle
STRATEGIES = {
mechanize: lambda do |url|
Mechanize.new.get(url).title
end
}
def initialize(with: strategy)
@strategy = strategy
end
def call(url)
STRATEGIES.fetch[@strategy].call(url)
end
end
CheckEntryTitleMatchesPageTitle
.new(
get_page_title: lambda do |url|
'Elixir/Erlang Clustering in Kubernetes'
end
)
.call(
url: 'http://bitwalker.org/posts/2016-08-04-clustering-in-kubernetes',
entry_title: 'Elixir/Erlang Clustering in Kubernetes'
)
# => [true, 'Elixir/Erlang Clustering in Kubernetes']
class CheckUtmCampaignIsValid
def initialize(current_issue_number: nil)
@current_issue_number = current_issue_number
end
def call(url)
[
is_utm_campaign_valid?(extract_utm_campaign_value_from(url)),
given_utm_campaign_value
]
end
private
def is_utm_campaign_valid?(given_utm_campaign_value)
(given_utm_campaign_value == current_issue_number.call)
end
def extract_utm_campaign_value_from(url)
parse_query_strings(url)
.fetch('utm_campaign', [])
.first
end
def parse_query_strings(url)
CGI.parse(URI.parse(url).query)
end
def current_issue_number
@current_issue_number ||= lambda { $current_issue_number }
end
end

Functional objects on Ruby programming language

Benefits

  • Message passing is natural;
  • Emphasize what system does rather rhan what system is;
  • Usage of mocking frameworks can be dropped off;

Guidelines

  • Start class names with a verb;
  • Public contract is a #call method;
    • Enables SRP;
    • Enables composition;
    • Enables polymorphism;
    • Enables to receive Proc/callables objects;
  • Receive stateful/impure collaborators through the object initializer;
    • This permits easy mocking/substitution.
    • This resembles curry-like functions;
  • Receive "pure function" inputs through the #call method;
  • The #call calling always returns a value, preferably 'result'-like objects;
    • This avoids 'primitive obssesion' anti-pattern;
    • Consider monadics operations;
  • Define stateful/impure functions as default implementation, in case nothing is received;
  #...
  private

  def get_page_title
    @get_page_title ||= begin
      lambda do |url|
        mechanize = Mechanize.new
        mechanize.get(url).title
      end      
    end
  end
end
  • Object internal state is used only for stateful/impure collaborators.
  • Collaborators are also other functional objects (they're named as verbs);
  • Stateful/impure functions are always returned as closures/lambda;
    extract_utm_campaign_value_from(url) # is pure
    get_page_title.call(url)             # is impure

Quote from "Functional and Reactive Domain Modeling" book

# Why mixing domain logic and side effects is bad

### Entanglement of domain logic and side effects.
  Violates separation of concerns. Domain logic and side effects are orthogonal
  to each other—entanglement violates a basic software engineering principle.

### Difficult to unit test.
  If domain logic is entangled with side effects, unit testing becomes
  difficult. You need to resort to mocking stuff that leads to other
  complications in your source code.

### Difficult to reason about the domain logic.
  You can’t reason about the domain logic that’s entangled with the side effect.

### Side effects don’t compose and hinder modularity of your code.
  Your entangled code remains an island that can’t be composed with other
  functions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment