Slide 1

Slide 1 text

Good Object Oriented Design

Slide 2

Slide 2 text

Learned C++ and Java during CS studies

Slide 3

Slide 3 text

Objects! Interfaces! Messages! Encapsulation!

Slide 4

Slide 4 text

public class Person { private String name; private int age; public Person(String Name, int Age) { this.name = Name; this.age = Age; } }

Slide 5

Slide 5 text

(⊃。•́ ‿•̀。)⊃━☆ƅ.*ŋŇƅ

Slide 6

Slide 6 text

PHP

Slide 7

Slide 7 text

$query = sprintf("SELECT firstname, lastname, address, age FROM friends WHERE firstname='%s' AND lastname='%s'", mysql_real_escape_string($firstname), mysql_real_escape_string($lastname)); $result = mysql_query($query); if (!$result) { die('Invalid query: ' . mysql_error() . "\n"); } while ($row = mysql_fetch_assoc($result)) { echo $row['firstname']; echo $row['lastname']; echo $row['address']; echo $row['age']; } function function function function

Slide 8

Slide 8 text

This functional programming is ugly

Slide 9

Slide 9 text

class Person { public $age; public $name; public function setAge($age) { $this->age = $age; } public function setName($name) { $this->name = $name; } } $person = new Person(); $person.setAge(32); $person.setName("Piotr");

Slide 10

Slide 10 text

\ ʕ◕‿◕ʔ /

Slide 11

Slide 11 text

BUT

Slide 12

Slide 12 text

So much boilerplate code :( die(PHP ORMs) Inconsistency everywhere …

Slide 13

Slide 13 text

Not Good OOD

Slide 14

Slide 14 text

Ruby on Rails

Slide 15

Slide 15 text

Zero boilerplate!

Slide 16

Slide 16 text

class Person < ActiveRecord::Base belongs_to :group has_many :tasks end

Slide 17

Slide 17 text

This is *it* ୧| ⁰ ᴥ ⁰ |୨

Slide 18

Slide 18 text

Good OOD!

Slide 19

Slide 19 text

Data

Slide 20

Slide 20 text

Behavior

Slide 21

Slide 21 text

Ridiculously Powerful Interfaces

Slide 22

Slide 22 text

class Repository < ActiveRecord::Base include Gitorious::Messaging::Publisher include Watchable include Gitorious::Authorization include Gitorious::Protectable KIND_PROJECT_REPO = 0 KIND_WIKI = 1 KIND_TEAM_REPO = 2 KIND_USER_REPO = 3 KIND_TRACKING_REPO = 4 KINDS_INTERNAL_REPO = [KIND_WIKI, KIND_TRACKING_REPO] belongs_to :user belongs_to :project belongs_to :owner, :polymorphic => true has_many :repository_memberships, :as => :content has_many :content_memberships, :as => :content belongs_to :parent, :class_name => "Repository" has_many :clones, :class_name => "Repository", :foreign_key => "parent_id", :dependent => :nullify has_many :comments, :as => :target, :dependent => :destroy has_many :merge_requests, :foreign_key => "target_repository_id", :order => "status, id desc", :dependent => :destroy has_many :proposed_merge_requests, :foreign_key => "source_repository_id", :class_name => "MergeRequest", :order => "id desc", :dependent => :destroy has_many :cloners, :dependent => :destroy has_many :events, :as => :target, :dependent => :destroy has_many :services, :dependent => :destroy has_many :_committerships, :dependent => :destroy A data-structure connected to a database table …and can publish messages …and it’s watched by observers …and has some authorization logic …and 14 association definitions so that we can do joins

Slide 23

Slide 23 text

Let’s add some behavior on top of it

Slide 24

Slide 24 text

# encoding: utf-8 #-- # Copyright (C) 2012-2014 Gitorious AS # Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies) # Copyright (C) 2009 Fabio Akita # Copyright (C) 2008 David Chelimsky # Copyright (C) 2008 David A. Cuadrado # Copyright (C) 2008 Tim Dysinger # Copyright (C) 2008 David Aguilar # Copyright (C) 2008 Tor Arne Vestbø # Copyright (C) 2007, 2008 Johan Sørensen # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . #++ require "gitorious" require "gitorious/messaging" class Repository < ActiveRecord::Base include Gitorious::Messaging::Publisher include Watchable include Gitorious::Authorization include Gitorious::Protectable KIND_PROJECT_REPO = 0 KIND_WIKI = 1 KIND_TEAM_REPO = 2 KIND_USER_REPO = 3 KIND_TRACKING_REPO = 4 KINDS_INTERNAL_REPO = [KIND_WIKI, KIND_TRACKING_REPO] belongs_to :user belongs_to :project belongs_to :owner, :polymorphic => true has_many :repository_memberships, :as => :content has_many :content_memberships, :as => :content belongs_to :parent, :class_name => "Repository" has_many :clones, :class_name => "Repository", :foreign_key => "parent_id", :dependent => :nullify has_many :comments, :as => :target, :dependent => :destroy has_many :merge_requests, :foreign_key => "target_repository_id", :order => "status, id desc", :dependent => :destroy has_many :proposed_merge_requests, :foreign_key => "source_repository_id", :class_name => "MergeRequest", :order => "id desc", :dependent => :destroy has_many :cloners, :dependent => :destroy has_many :events, :as => :target, :dependent => :destroy has_many :services, :dependent => :destroy has_many :_committerships, :dependent => :destroy def committerships RepositoryCommitterships.new(self) end after_destroy :post_repo_deletion_message scope :by_users, :conditions => { :kind => KIND_USER_REPO } do def fresh(limit = 10) order("last_pushed_at DESC").limit(limit) end end scope :by_groups, :conditions => { :kind => KIND_TEAM_REPO } do def fresh(limit=10) order("last_pushed_at DESC").limit(limit) end end scope :clones, :conditions => ["kind in (?) and parent_id is not null", [KIND_TEAM_REPO, KIND_USER_REPO]] scope :mainlines, :conditions => { :kind => KIND_PROJECT_REPO } scope :regular, :conditions => ["kind in (?)", [KIND_TEAM_REPO, KIND_USER_REPO, KIND_PROJECT_REPO]] def open_merge_requests # merge_requests.open doesn't quite work, presumably related to the # issue of 'open': Object#open, "open" state and "open" scope. Overload! # TODO: Refactor MergeRequest merge_requests.where({}).open end def destroy merge_requests.each &:destroy reload super end def self.human_name I18n.t("activerecord.models.repository") end def self.find_by_name_in_project!(name, containing_project = nil) if containing_project find_by_name_and_project_id!(name, containing_project.id) else find_by_name!(name) end end def self.find_by_path(path) base_path = path.gsub(/^#{Regexp.escape(RepositoryRoot.default_base_path)}/, "") path_components = base_path.split("/").reject{|p| p.blank? } repo_name, owner_name = [path_components.pop, path_components.shift] project_name = path_components.pop repo_name.sub!(/\.git/, "") raise ActiveRecord::RecordNotFound unless owner_name owner = case owner_name[0].chr when "+" Group.find_by_name!(owner_name.sub(/^\+/, "")) when "~" User.find_by_login!(owner_name.sub(/^~/, "")) else Project.find_by_slug!(owner_name) end if owner.is_a?(Project) owner_conditions = { :project_id => owner.id } else owner_conditions = { :owner_type => owner.class.name, :owner_id => owner.id } end if project_name if project = Project.find_by_slug(project_name) owner_conditions.merge!(:project_id => project.id) end end Repository.where({ :name => repo_name }.merge(owner_conditions)).first end def self.delete_git_repository(path) git_backend.delete!(RepositoryRoot.expand(path).to_s) end def self.most_active_clones_in_projects(projects, limit = 5) key = "repository:most_active_clones_in_projects:#{projects.map(&:id).join('-')}:#{limit}" clone_ids = projects.map do |project| project.repositories.clones.map{|r| r.id } end.flatten select("distinct repositories.*, count(events.id) as event_count"). where("repositories.id in (?) and events.created_at > ? and kind in (?)", clone_ids, 7.days.ago, [KIND_USER_REPO, KIND_TEAM_REPO]). order("count(events.id) desc"). joins(:events). includes(:project). group("repositories.id"). limit(limit) end def self.most_active_clones(limit = 10) select("distinct repositories.id, repositories.*, count(events.id) as event_count"). where("events.created_at > ? and kind in (?)", 7.days.ago, [KIND_USER_REPO, KIND_TEAM_REPO]). order("count(events.id) desc"). group("repositories.id"). joins(:events). includes(:project). limit(limit) end # Finds all repositories that might be due for a gc, starting with # the ones who've been pushed to recently def self.all_due_for_gc(batch_size = 25) where("push_count_since_gc > 0"). order("push_count_since_gc desc"). limit(batch_size) end def gitdir "#{url_path}.git" end # The project/repo path segment is useful for more things than URLs def path_segment File.join(project.to_param_with_prefix, name) end def url_path path_segment end def real_gitdir "#{self.full_hashed_path}.git" end def browse_url Gitorious.url(url_path) end def default_clone_protocol return "git" if git_cloning? return "http" if http_cloning? "ssh" end def default_clone_url send(:"#{default_clone_protocol}_clone_url") end def clone_url if http_cloning? http_clone_url elsif ssh_cloning? ssh_clone_url else raise "cloning disabled" end end def ssh_clone_url Gitorious.ssh_daemon.url(gitdir) end def git_clone_url Gitorious.git_daemon.url(gitdir) end def http_clone_url Gitorious.git_http.url(gitdir) end def http_cloning? !Gitorious.git_http.nil? end def git_cloning? return !Gitorious.git_daemon.nil? && public? end def ssh_cloning? return !Gitorious.ssh_daemon.nil? end def push_url if ssh_cloning? ssh_clone_url elsif http_cloning? http_clone_url else raise "pushing disabled" end end def display_ssh_url?(user) return true if !http_cloning? && !git_cloning? && ssh_cloning? can_push?(user, self) end def full_repository_path RepositoryRoot.expand(real_gitdir).to_s end def git Grit::Repo.new(full_repository_path) end def has_commits? return false if new_record? || !ready? !git.heads.empty? end def self.git_backend Rails.env.test? ? MockGitBackend : GitBackend end def git_backend Rails.env.test? ? MockGitBackend : GitBackend end def to_param name end def to_xml(opts = {}) info_proc = Proc.new do |options| builder = options[:builder] builder.owner(owner.to_param, :kind => (owned_by_group? ? "Team" : "User")) builder.kind(["mainline", "wiki", "team", "user"][self.kind]) builder.project(project.to_param) end super({ :procs => [info_proc], :only => [:name, :created_at, :ready, :description, :last_pushed_at], :methods => [:clone_url, :push_url, :parent] }.merge(opts)) end def head_candidate return nil unless has_commits? @head_candidate ||= head || git.heads.first end def head_candidate_name return head.name if head = head_candidate "master" end def head git && git.head end def head=(head_name) if new_head = git.heads.find{|h| h.name == head_name } unless git.head == new_head git.update_head(new_head) end end end def last_commit(ref = nil) if has_commits? @last_commit ||= Array(git.commits(ref || head_candidate.name, 1)).first end @last_commit end def commit_for_tree_path(ref, commit_id, path) Rails.cache.fetch("treecommit:#{commit_id}:#{Digest::SHA1.hexdigest(ref+path)}") do git.log(ref, path, {:max_count => 1}).first end end # changes the owner to +another_owner+, removes the old owner as committer # and adds +another_owner+ as committer def change_owner_to!(another_owner) return if owned_by_group? transaction do committerships.destroy_for_owner self.owner = another_owner if self.kind != KIND_PROJECT_REPO # project_repo? case another_owner when Group self.kind = KIND_TEAM_REPO when User self.kind = KIND_USER_REPO end end committerships.update_owner(another_owner) save! reload end end def post_repo_deletion_message payload = { :target_class => self.class.name, :command => "delete_git_repository", :arguments => [real_gitdir] } publish("/queue/GitoriousRepositoryDeletion", payload) end def total_commit_count events.count(:conditions => {:action => Action::COMMIT}) end def git_derived_total_commit_count(ref) begin total = git.commit_count(ref) rescue Grit::Git::GitTimeout total = 2046 end end def paginated_commits(ref, page, per_page = 30) page = (page || 1).to_i total = git_derived_total_commit_count(ref) offset = (page - 1) * per_page commits = WillPaginate::Collection.new(page, per_page, total) commits.replace git.commits(ref, per_page, offset) end def cached_paginated_commits(ref, page, per_page = 30) page = (page || 1).to_i last_commit_id = last_commit(ref) ? last_commit(ref).id : nil total = Rails.cache.fetch("paglogtotal:#{self.id}:#{last_commit_id}:#{ref}") do begin git.commit_count(ref) rescue Grit::Git::GitTimeout 2046 end end Rails.cache.fetch("paglog:#{page}:#{self.id}:#{last_commit_id}:#{ref}") do offset = (page - 1) * per_page commits = WillPaginate::Collection.new(page, per_page, total) commits.replace git.commits(ref, per_page, offset) end end def count_commits_from_last_week_by_user(user) return 0 unless has_commits? commits_by_email = git.commits_since("master", "last week").collect do |commit| commit.committer.email == user.email end commits_by_email.size end # TODO: cache def commit_graph_data(head = "master") commits = git.commits_since(head, "24 weeks ago") commits_by_week = commits.group_by{|c| c.committed_date.strftime("%W") } # build an initial empty set of 24 week commit data weeks = [1.day.from_now-1.week] 23.times{|w| weeks << weeks.last-1.week } week_numbers = weeks.map{|d| d.strftime("%W") } commits = (0...24).to_a.map{|i| 0 } commits_by_week.each do |week, commits_in_week| if week_pos = week_numbers.index(week) commits[week_pos+1] = commits_in_week.size end end commits = [] if commits.max == 0 [week_numbers.reverse, commits.reverse] end # TODO: caching def commit_graph_data_by_author(head = "master") h = {} emails = {} data = self.git.git.shortlog({:e => true, :s => true }, head) data.each_line do |line| count, actor = line.split("\t") actor = Grit::Actor.from_string(actor) h[actor.name] ||= 0 h[actor.name] += count.to_i emails[actor.email] = actor.name end User.where("email in (?)", emails.keys).each do |user| author_name = emails[user.email] if h[author_name] # in the event that a user with the same name has used two different emails, he'd be gone by now h[user.login] = h.delete(author_name) end end h end # Returns a Hash {email => user}, where email is selected from the +commits+ def self.users_by_commits(commits) emails = commits.map { |commit| commit.author.email }.uniq users = User.where("email in (?)", emails) users_by_email = users.inject({}){|hash, user| hash[user.email] = user; hash } users_by_email end def cloned_from(ip, country_code = "--", country_name = nil, protocol = "git") cloners.create(:ip => ip, :date => Time.now.utc, :country_code => country_code, :country => country_name, :protocol => protocol) end def wiki? kind == KIND_WIKI end def project_repo? kind == KIND_PROJECT_REPO end def mainline? project_repo? end def team_repo? kind == KIND_TEAM_REPO end def user_repo? kind == KIND_USER_REPO end def tracking_repo? kind == KIND_TRACKING_REPO end def owned_by_group? owner.is_a?(Group) || owner.is_a?(LdapGroup) end def internal? wiki? || tracking_repo? end def title name end def owner_title mainline? ? project.title : owner.title end # returns the project if it's a KIND_PROJECT_REPO, otherwise the owner def project_or_owner project_repo? ? project : owner end # Returns a list of users being either the owner (if User) or each admin member (if Group) def owners result = if owned_by_group? owner.members.select do |member| if owner.respond_to?(:admin) admin?(member, owner) else admin?(member, owner) end end else [owner] end return result end def full_hashed_path self.hashed_path || set_repository_path end def set_repository_path if RepositoryRoot.shard_dirs? set_repository_hash else set_repository_plain_path end end def set_repository_plain_path self.hashed_path ||= repository_plain_path end def repository_plain_path if project "#{self.project.slug}/#{self.name}" else "#{self.name}" end end alias_method :slug, :repository_plain_path def set_repository_hash self.hashed_path ||= begin raw_hash = Digest::SHA1.hexdigest(owner.to_param + self.to_param + Time.now.to_f.to_s + SecureRandom.hex) sharded_hash = sharded_hashed_path(raw_hash) sharded_hash end end # Creates a block within which we generate events for each attribute changed # as long as it's changed to a legal value def log_changes_with_user(a_user) @updated_fields = [] yield log_updates(a_user) end # Replaces a value within a log_changes_with_user block def replace_value(field, value, allow_blank = false) old_value = read_attribute(field) return if !allow_blank && value.blank? || old_value == value self.send("#{field}=", value) validation = RepositoryValidator.call(self) if validation.errors[field].length == 0 @updated_fields << field end end # Logs events that occured within a log_changes_with_user block def log_updates(a_user) @updated_fields.each do |field_name| events.build(:action => Action::UPDATE_REPOSITORY, :user => a_user, :project => project, :body => "Changed the repository #{field_name.to_s}") end end def requires_signoff_on_merge_requests? mainline? && project.merge_requests_need_signoff? end def tracking_repository self.class.where(:parent_id => self, :kind => KIND_TRACKING_REPO).first end def has_tracking_repository? !tracking_repository.nil? end def next_merge_request_sequence_number last_merge_request_sequence_number + 1 end # Runs git-gc on this repository, and updates the last_gc_at attribute def gc! Grit::Git.with_timeout(nil) do if self.git.git.gc self.last_gc_at = Time.now self.push_count_since_gc = 0 return save end end end def register_push self.last_pushed_at = Time.now.utc self.push_count_since_gc = push_count_since_gc.to_i + 1 update_disk_usage end def update_disk_usage self.disk_usage = calculate_disk_usage end def calculate_disk_usage @calculated_disk_usage ||= `du -sb #{full_repository_path} 2>/dev/null`.chomp.to_i end def matches_regexp?(term) return user.login =~ term || name =~ term || (owned_by_group? ? owner.name =~ term : false) || description =~ term end def search_clones(term) self.class.title_search(term, "parent_id", id) end # Searches for term in # - title # - description # - owner name/login # # Scoped to column +key+ having +value+ # # Example: # title_search("foo", "parent_id", 1) # will find clones of Repo with id 1 # matching 'foo' # # title_search("foo", "project_id", 1) # will find repositories in Project#1 # matching 'foo' def self.title_search(term, key, value) sql = "SELECT repositories.* FROM repositories INNER JOIN users on repositories.user_id=users.id INNER JOIN groups on repositories.owner_id=groups.id WHERE repositories.#{key}=:id AND (repositories.name LIKE :q OR repositories.description LIKE :q OR groups.name LIKE :q) AND repositories.owner_type='Group' AND kind in (:kinds) UNION ALL SELECT repositories.* from repositories INNER JOIN users on repositories.user_id=users.id INNER JOIN users owners on repositories.owner_id=owners.id WHERE repositories.#{key}=:id AND (repositories.name LIKE :q OR repositories.description LIKE :q OR owners.login LIKE :q) AND repositories.owner_type='User' AND kind in (:kinds)" self.find_by_sql([sql, {:q => "%#{term}%", :id => value, :kinds => [KIND_TEAM_REPO, KIND_USER_REPO, KIND_PROJECT_REPO]}]) end alias :repo_public? :public? def public? repo_public? && project.public? end def self.private_on_create?(params = {}) return false if !Gitorious.private_repositories? params.fetch(:private, Gitorious.repositories_default_private?) end def uniq_name? repository = Repository.where("lower(name) = ? and project_id = ?", name, project_id).first repository.nil? || repository == self end def uniq_hashed_path? repository = Repository.where("lower(hashed_path) = ?", hashed_path).first repository.nil? || repository == self end def name=(name) self[:name] = name.respond_to?(:downcase) ? name.downcase : name end def kind=(kind) if kind == :project self[:kind] = Repository::KIND_PROJECT_REPO elsif kind == :tracking self[:kind] = Repository::KIND_TRACKING_REPO elsif kind == :wiki self[:kind] = Repository::KIND_WIKI elsif kind == :user self[:kind] = Repository::KIND_USER_REPO elsif kind == :team self[:kind] = Repository::KIND_TEAM_REPO else self[:kind] = kind end end def commit_comments(id) comments.where(:sha1 => id).includes(:user) end protected def sharded_hashed_path(h) first = h[0,3] second = h[3,3] last = h[-34, 34] "#{first}/#{second}/#{last}" end def self.reserved_names @reserved_names ||= [] end def self.reserve_names(names) @reserved_names ||= [] @reserved_names.concat(names) end end

Slide 25

Slide 25 text

BUT

Slide 26

Slide 26 text

We’ve seen that already

Slide 27

Slide 27 text

We’ve been discussing that for > 5 years

Slide 28

Slide 28 text

Not Good OOD ¯\_(⊙_ʖ⊙)_/¯

Slide 29

Slide 29 text

BLENDING FUNCTIONAL AND OO PROGRAMMING IN RUBY Piotr Solnica BaRuCo 2015

Slide 30

Slide 30 text

@_solnic_ github.com/solnic solnic.eu

Slide 31

Slide 31 text

Ruby Object Mapper rom-rb.org

Slide 32

Slide 32 text

My Path To Good OOD (through FP)

Slide 33

Slide 33 text

Functional Objects

Slide 34

Slide 34 text

Immutable

Slide 35

Slide 35 text

#call(input)

Slide 36

Slide 36 text

no side-effects

Slide 37

Slide 37 text

return some output

Slide 38

Slide 38 text

mutable objects ┻━┻ ヘ╰( •̀ε•́ ╰)

Slide 39

Slide 39 text

Immutability-Oriented Design

Slide 40

Slide 40 text

class UserQuery attr_accessor :query def initialize(query) @query = query end def call query.execute end end

Slide 41

Slide 41 text

user_query = UserQuery.new(some_query) user_query.call user_query.query = other_query user_query.call

Slide 42

Slide 42 text

Simply return a new instance class UserQuery attr_reader :query def initialize(query) @query = query end def with(query) self.class.new(query) end def call query.execute end end

Slide 43

Slide 43 text

user_query = UserQuery.new(some_query) user_query.call user_query.with(other_query).call

Slide 44

Slide 44 text

State (where does it go?)

Slide 45

Slide 45 text

Prefer stateless objects

Slide 46

Slide 46 text

No content

Slide 47

Slide 47 text

class PersistPerson attr_reader :person def initialize(person) @person = person end def call UserRepository.save(person) end end State :/ Tightly coupled with a concrete class

Slide 48

Slide 48 text

persist_person = PersistPerson.new(name: “Jane”) persist_person.call Throw-away object :(

Slide 49

Slide 49 text

class PersistPerson attr_reader :user_repository def initialize(user_repository) @user_repository = user_repository end def call(person) user_repository.save(person) end end Injected dependency == less coupling input passed in == more flexibility

Slide 50

Slide 50 text

persist_person = PersistPerson.new(UserRepository.new) persist_person.call(name: "Jane") Instantiate once, use many times

Slide 51

Slide 51 text

“person” structure becomes 1st class

Slide 52

Slide 52 text

Other objects provide functionality that depend on that structure

Slide 53

Slide 53 text

No need to mutate!

Slide 54

Slide 54 text

Easy to compose functional objects

Slide 55

Slide 55 text

input = { name: "Person" } persist_person.call( validate.call( coerce.call(input) ) )

Slide 56

Slide 56 text

Functional Interfaces

Slide 57

Slide 57 text

AutoCurry +

Slide 58

Slide 58 text

class LogEvent attr_reader :logger def initialize(logger) @logger = logger end def call(type, payload) logger.write("#{type} - #{payload}") end end

Slide 59

Slide 59 text

class LogEvent include Dry::AutoCurry attr_reader :logger def initialize(logger) @logger = logger end def call(type, payload) logger.write("#{type} - #{payload}") end auto_curry :call end Small extension #call becomes lazy

Slide 60

Slide 60 text

log_event = LogEvent.new(Logger.new) log_user_creation = log_event.call(:user_created) log_user_creation.call(name: "Jane") partially applied argument

Slide 61

Slide 61 text

let fn = succ . head fn [1, 2, 3] 2 Haskell

Slide 62

Slide 62 text

Functional Composition

Slide 63

Slide 63 text

class PersistUser include Dry::Pipeline::Mixin attr_reader :user_repository def initialize(user_repository) @user_repository = user_repository end def call(user) user_repository.save(user) end end Adds “>>” operator

Slide 64

Slide 64 text

class LogEvent include Dry::AutoCurry attr_reader :logger def initialize(logger) @logger = logger end def call(type, payload) logger.write([type, payload].join(' - ')) end auto_curry :call end

Slide 65

Slide 65 text

create_user = CreateUser.new(UserRepository.new) log_event = LogEvent.new(Logger.new) create_user_and_log_event = create_user >> log_event.call(:user_created) create_user_and_log_event.call(name: "Jane") composes a pipeline with a partially applied log_event call

Slide 66

Slide 66 text

iex> [1, [2], 3] |> List.flatten |> Enum.map(fn x -> x * 2 end) [2, 4, 6] Elixir

Slide 67

Slide 67 text

FP + OO = ?

Slide 68

Slide 68 text

Immutability is a virtue

Slide 69

Slide 69 text

class UserQuery attr_reader :query def initialize(query) @query = query end def with(query) self.class.new(query) end def call query.execute end end

Slide 70

Slide 70 text

Consistent and side-effect-less interfaces lead to great composability

Slide 71

Slide 71 text

class Validate def call(input) … end end class Coerce def call(input) … end end class Persist def call(input) … end end

Slide 72

Slide 72 text

(persist >> validate >> coerce).call(input)

Slide 73

Slide 73 text

Functional interfaces are a great foundation for high-level OO abstractions

Slide 74

Slide 74 text

transaction.call(name: 'Jane') transaction = coerce >> validation >> persist transaction.call(name: 'Jane') transaction = Transflow(container) do steps :coerce, :validate, :persist end

Slide 75

Slide 75 text

transaction = Transflow(container) do publish true steps :coerce, :validate, :persist end class EventListener attr_reader :log_event def initialize(log_event) @log_event = log_event end def persist_success(type, result) log_event.call(type, result) end end

Slide 76

Slide 76 text

event_listener = EventListener.new(LogEvent.new) transaction.subscribe(event_listener) transaction.call(name: 'Jane')

Slide 77

Slide 77 text

transaction = Transflow(container) do steps :coerce, :validate, :persist, :log_event end (coerce .>> validate .>> persist .>> log_event.call(:create_user) ).call(input) transaction.with(log_event: :create_user).call(name: ‘Jane')

Slide 78

Slide 78 text

FP + OO = ?

Slide 79

Slide 79 text

“seems like Piotr would much rather be programming in a statically typed, functional language”

Slide 80

Slide 80 text

True, but…

Slide 81

Slide 81 text

I program in Ruby

Slide 82

Slide 82 text

I want to write the best Ruby possible

Slide 83

Slide 83 text

FP + OO = Good OOD WOW

Slide 84

Slide 84 text

Thank You!

Slide 85

Slide 85 text

Mentioned Gems https://github.com/rom-rb/rom https://github.com/dryrb/dry-pipeline https://github.com/dryrb/dry-auto_curry (coming soon) https://github.com/solnic/transflow

Slide 86

Slide 86 text

Photo Credits <3 https://www.pinterest.com/romainbarbier/shocked-animals/