Elegant Objects: Composable Decorators
There’s an interesting section in Elegant Objects Volume 1 on “Composable Decorators”.
The example, as written, is:
names = new Sorted(
new Unique(
new Capitalized(
new Replaced(
new FileNames(
new Directory(
"/var/users/*.xml"
)
),
"([^.]+)\\.xml",
"$1"
)
)
)
);
So, assuming my filesystem was case-sensitive and I had the following files:
gareth: /var/users
$ tree
.
├── alice.xml
├── ALICE.XML
└── bob.xml
The code above would set names
as a new Sorted
instance with elements "Alice"
and "Bob"
; something like:
names.to_a
# => ["Alice", "Bob"]
Yegor praises the code for being clean and purely declarative. I quite like the style, but in Ruby at least, it looks like a lot more effort to write than chaining a few methods.
Dir.glob("/var/users/*.xml").
map { |f| File.basename(f, '.xml').capitalize }.
uniq.
sort
# => ["Alice", "Bob"]
Here’s a full Ruby implementation of the snippet in the book:
class Directory
include Enumerable
def initialize(glob)
@glob = glob
end
def each
Dir.glob(@glob).each { |path| yield path }
end
end
class FileNames
include Enumerable
def initialize(files)
@files = files
end
def each
@files.each { |file| yield File.basename(file) }
end
end
# Replaced.new(%w[alice.xml bob.xml], /([^.]+)\.xml/, '\1').to_a
# => ["alice", "bob"]
class Replaced
include Enumerable
def initialize(list, regexp, replacement)
@list = list
@regexp = regexp
@replacement = replacement
end
def each
@list.each do |element|
yield element.to_s.gsub(@regexp, @replacement)
end
end
end
# Capitalized.new(%w[x y z]).to_a
# => ["X", "Y", "Z"]
class Capitalized
include Enumerable
def initialize(list)
@list = list
end
def each
@list.each { |element| yield element.to_s.capitalize }
end
end
# Unique.new([8,8,2,2]).to_a
# => [8, 2]
class Unique
include Enumerable
def initialize(list)
@list = list
end
def each
@list.uniq.each { |element| yield element }
end
end
# Sorted.new([8,5,2,7]).to_a
# => [2, 5, 7, 8]
class Sorted
include Enumerable
def initialize(list)
@list = list
end
def each
@list.sort.each { |element| yield element }
end
end
names = Sorted.new(
Unique.new(
Capitalized.new(
Replaced.new(
FileNames.new(
Directory.new('/var/users/*.xml')
),
/([^.]+)\.xml/,
'\1'
)
)
)
)
names.to_a
# => ["Alice", "Bob"]
That’s a lot of code! Even at the call-site, there’s just more that needs to be written.
def user_names
Dir.glob("/var/users/*.xml").
map { |f| File.basename(f, '.xml').capitalize }.
uniq.
sort
end
# vs…
def user_names
Sorted.new(
Unique.new(
Capitalized.new(
Replaced.new(
FileNames.new(
Directory.new('/var/users/*.xml')
),
/([^.]+)\.xml/,
'\1'
)
)
)
)
end
Where this approach shines, though, is where each of these classes make up part of a library.
Its a little hard to see in this example, as the decorators re-implement standard Ruby methods. However, if this was all largely custom domain logic, it would be incredibly hard to re-compose the chained approach.
There’d be a lot of re-implementation, changing or making new methods that return the values you need. With the composable decorators, it would be much easier to pick and choose from the classes you need – or add additional classes – to construct the new method.
There’s more about this chapter of the book at OOP Alternative to Utility Classes.