Receive Policies Instead of Data
Hi folks, today I wanna share something that drew my attention while reading Confident Ruby.
Avdi presents the following method:
def delete_files(files, ignore_errors=false, log_errors=false)
files.each do |file|
begin
File.delete(file)
rescue => error
puts error.message if log_errors
raise unless ignore_errors
end
end
end
And its invocation:
require 'fileutils'
FileUtils.touch 'does_exist'
delete_files(['does_not_exist', 'does_exist'], true, true)
This code is not good at all.
- the method worries to much in handling edge cases (error, logs)
- The
true, true
in calls todelete_files
does not help. We have to refer to the method definition to remember what those flags mean. - What if we wanted to log using a special format? Or to somewhere other than STDOUT? There is no provision for customizing how errors are logged.
- What do we do if we ever decide to handle permissions errors differently from
no such file or directory
? Add yet another flag?
The book point us the fundamental problem: both ignore_errors
and log_errors
attempt to specify policies using data.
Let's fix this by passing a block as argument:
def delete_files(files, &error_policy)
error_policy ||= ->(file, error) { raise error }
files.each do |file|
begin
File.delete(file)
rescue => error
error_policy.call(file, error)
end
end
end
delete_files(['does_not_exist', 'does_exist']) do |file, error|
puts error.message
end
Look how flexible it is now. We define a default behavior for error handling but also let it open for customization by calling a given block.
What if we need more control over the behavior? Just pass more policies:
def delete_files(files, options={})
error_policy = options.fetch(:on_error) { ->(file, error) { raise error } }
symlink_policy = options.fetch(:on_symlink) { ->(file) { File.delete(file) } }
files.each do |file|
begin
if File.symlink?(file)
symlink_policy.call(file)
else
File.delete(file)
end
rescue => error
error_policy.call(file, error)
end
end
end
delete_files(
['file1', 'file2'],
on_error: ->(file, error) { warn error.message },
on_symlink: ->(file) { File.delete(File.realpath(file)) }
)
Conclusion
Block and proc as argument help callers to specify policies, freeing us from hard-coded decisions which someone else could make.
Written on October 11, 2018