Composition Root

Design patterns / Clean architecture patterns
image

# entity
class Order
    attr_reader :id, :total

    def initialize(id, total)
        @id = id
        @total = total
    end

    def payable
        total >= 200 ? total - total / 10 : total
    end
end

# gateway
module Catalog
    def price(sku)
        raise NotImplementedError, "#{self.class}#price"
    end
end

# repository
module Orders
    def save(order)
        raise NotImplementedError, "#{self.class}#save"
    end

    def count
        raise NotImplementedError, "#{self.class}#count"
    end
end

# output port
module Report
    def say(text)
        raise NotImplementedError, "#{self.class}#say"
    end
end

# use case: asks for its parts, never builds them
class PlaceOrder
    def initialize(catalog, orders, report)
        @catalog = catalog
        @orders = orders
        @report = report
        @next_id = 1001
    end

    def place(items)
        total = 0
        items.each do |sku, count|
            price = @catalog.price(sku)
            unless price
                @report.say("no #{sku}")
                return
            end
            total += price * count
        end
        order = Order.new(@next_id, total)
        @next_id += 1
        @orders.save(order)
        text = "order #{order.id} for #{order.payable}"
        @report.say(text)
    end
end

# adapters
class MemoryCatalog
    include Catalog

    def price(sku) = {"pen" => 30, "lamp" => 190}[sku]
end

class MemoryOrders
    include Orders

    def initialize
        @rows = []
    end

    def save(order) = @rows << order

    def count = @rows.size
end

class ConsoleReport
    include Report

    def say(text) = puts(text)
end

class SilentReport
    include Report

    def say(text) = nil
end

# composition root: the only place that sees every layer
# at once, and the only place where an adapter is built.
# the two ready builds are class methods, because the
# root also hands out the parts it has put together
class ShopApp
    attr_reader :use_case, :orders

    def initialize(catalog, orders, report)
        @orders = orders
        @use_case = PlaceOrder.new(catalog, orders, report)
    end

    def self.loud
        new(MemoryCatalog.new, MemoryOrders.new,
            ConsoleReport.new)
    end

    def self.quiet
        new(MemoryCatalog.new, MemoryOrders.new,
            SilentReport.new)
    end
end

# client code
app = ShopApp.loud
app.use_case.place({"pen" => 2, "lamp" => 1})
app.use_case.place({"chair" => 1})
puts "saved #{app.orders.count}"

# a second root builds the same use case differently,
# and neither the scenario nor the entity knows it
night = ShopApp.quiet
night.use_case.place({"lamp" => 1})
night.use_case.place({"pen" => 2})
puts "saved at night #{night.orders.count}"

# no adapter is built outside the root: the client asks
# for a ready application and gets one, and the scenario
# it got is the same class in both builds
puts "use case #{app.use_case.class}"
same = app.use_case.class == night.use_case.class
puts "same class #{same}"
# printed: order 1001 for 225
#          no chair
#          saved 1
#          saved at night 2
#          use case PlaceOrder
#          same class true


A composition root is the only place where the layers are connected and the adapters are built; the rest of the application only asks for its parts.
Read more

The problem. A use case needs a catalogue of prices, a store for orders and a way to report. If the use case creates these parts itself, it names adapters of the outer layer and depends on them. A test cannot give it other parts. If every part of the program builds its own parts, nobody sees how the application is connected.

The idea. A composition root is the only place where the layers are connected and the adapters are built. An adapter is outer code that implements a port, the interface that the inner layer declares. The use case only asks for its parts through ports and never builds them. The root knows every layer at once, and that is its job. All other code keeps the dependency rule: inner code never knows outer code. No framework or container is needed: the root is plain code at the start of the program.

How to read the example. The use case asks for three ports: a gateway for prices, a repository for orders and an output port for reports. The outer layer has adapters for them: a catalogue and a store in memory, a report that prints and a report that says nothing. The example builds the application twice. In the first build the report prints: the client code places order 1001, with 225 to pay, and an order for an unknown item. The second build has the silent report, so its orders print nothing. Only the count of saved orders shows that they were saved. The use case is the same in both builds.

When to use it. Every application has one composition root, at its start; a test may have its own small root with fake adapters. A short script without ports needs no root.

A common mistake. Creating an adapter inside a use case, even once. The use case then depends on the outer layer, and the root no longer shows how the application is connected.