In a software, what matters for the end users is what the system does, not what the system is. A DVD has no value for the user unless you put it into the machine.
Architecture represents what the system is, while what it does is represented by the messages between objects, which is captured via use cases. Architecture is the form, the shape of something, in this case, the objects and the mechanisms to support them.
A good definition of use case is described as the solution to a particular problem. In Jerry Weinberg's words, "problem is the definition between the current state and the desired state". It's safe to assume that state is what the system is, which can be modified by the interaction between objects.
Objects relate to the end user, its business and its components. Classes don't. Classes are a way to represent objects, the state, but they do very poorly when trying to represent what the system does. Once you hit runtime, objects morph into total obscure forms due to the endless interactions between them.
I'll describe in this paper how we can express what the system does in different paradigms, starting with ActiveRecord centric design, going to services and finally trying DCI.
Consider the user case where we have an inventory containing items and entries, items having many entries,
- Whenever a new entry is created
- I want to take all entries of the new entry's associated item
- I want to calculate the average moving cost of all these entries
- I want to save the new cost into the entry's item
Here, the objects are item and entry. This represents what the system is. The cost calculation is not an object, but rather the interaction between objects.
There are basically two schools of thought. The first is based on the premise that data objects should not have any behavior. The second, which is the standard in the Ruby on Rails community, for example, defines powerful data objects, capable of acting on its own.
Consider the following image. The controller is the entry point of the system, the one which receives input from end users and dispatches the flow to the correct object. In this case, it's calling a data object, which is commonly an ActiveRecord object. It could be a User object, a Post, Order, all of which represent data, not action.
Now, use your empathy and picture the programmer, looking at the controller's code. The first question he'd make is, "what does it do next?" However, what's actually being called next? A data object. The programmer is not able to know, upfront, what the system does, but what it is. As he steps into the next layer, he sees new layers of data objects.
Within this much code, the coder is not presented with a clear description of what the system does. To be able to understand that, he has to jump from one class to the next, memorizing all the calls that are being made, imagining what's going to happen when the app hits runtime.
Besides that, the data object, which could be anything other than a persistence object, is the entry point for the representation of the use cases. This goes against the basic principle of Object-Oriented Programming, which is to put the message between objects as the first-class citizen. That's crucial.
The real value of the system is in the arrows between the objects, not the objects. We need to find a way to describe these arrows, or messages, in terms of code in such a way that will make it easy for programmers to read and understand the use cases. As is written in the Gang of Four book, "It’s clear that code won’t reveal everything about how a system will work".
A better approach is to use Service objects. They serve the purpose of coordinating the various objects that will result in a given action. They commonly interact with data objects, which means that data objects have action methods. An example of that is Rails ActiveRecord's find()
or update_attributes()
.
This is the common approach developers take to design their objects in the code. This is big improvement over the Data object centric design, but it isn't a perfect solution either.
Let's start describing the simplest problems. The first is that Data objects are still going to have too many actions. Consider reading a User's class and finding methods such as create
and full_name
. In this case, you'd open the class to understand what it is, but instead you're presented with what it does. [needs improvement]
If you create a UserCreation
class, you're going into a better direction. [needs improvement]
is that you add new levels of indirection and
The deeper problem, though, lies in the paradigm most languages fit today: your classes do not describe the objects functioning in runtime. The moment you put the system to run, you can't reason about the objects anymore. This is one problem in the OO world: people are doing Class-Oriented Programming and calling it Object-Oriented Programming.
Consider these words from Alan Kay [3]:
In computer terms, Smalltalk is a recursion on the notion of computer itself. Instead of dividing “computer stuff” into things each less strong than the whole--like data structures, procedures, and functions which are the usual paraphernalia of programming languages--each Smalltalk object is a recursion on the entire possibilities of the computer. Thus its semantics are a bit like having thousands and thousands of computers all hooked together by a very fast network.
# model
class InventoryEntry < ActiveRecord::Base
belongs_to :inventory_item
accepts_nested_attributes_for :inventory_item
before_save :define_new_balance_values
def define_new_balance_values
past_balances = InventoryEntry.where(inventory_item_id: inventory_item_id)
.where("quantity > 0")
.all
balance = Store::Inventory::MovingAverageCost.new([self] + past_balances)
self.inventory_item.moving_average_cost = balance.moving_average_cost
end
# ... other endless methods
end
If the programmer wants to understand the current use case, though, he'll have to dig further down one layer.
# lib/store/inventory/moving_average_cost.rb
require "bigdecimal"
module Store
module Inventory
class MovingAverageCost
def initialize(entries)
@entries = entries
end
def moving_average_cost
if total_cost > 0
total_cost / total_quantity
else
BigDecimal("0.0")
end
end
def total_quantity
@entries.reduce(BigDecimal("0.0")) { |sum, e| sum + e.quantity }
end
def total_cost
@entries.reduce(BigDecimal("0.0")) { |sum, e| sum + e.quantity * e.cost_per_unit }
end
end
end
end
Here, the ActiveRecord object, which is basically a data object, has methods that add behavior to it. When the programmer has to read this file to understand what it's doing, the first notion he'll have is that this object does a lot of things.
Now he understands what's going on. The scenario only gets uglier with the additional intertwined use and edges cases.
Takeaway: with an Data objects centric domain logic, we don't get to see all the use cases easily, unless you follow the program runtime calls yourself. In other words, the Data centric design describes classes, not objects. If you want to understand the objects interactions (use cases), go do that yourself.
In the realms of Rails, the developer is forced to see a model as purely an ActiveRecord object, with classes such as MovingAverageCost
as service objects. If we go back to the original concept of MVC, the model "manages the behavior and data of the application domain, responds to requests for information about its state (usually from the view), and responds to instructions to change state (usually from the controller)" [1]. This includes persistence objects and wrappers, such as ActiveRecord, and service objects, such as the one extracted above for doing math calculations.
In the realms of Object-Oriented Programming, it's all about the interactions between objects. Interactions are not triggered by data objects, but by objects responsible for behavior. In this regard, OO will not take InventoryEntry as the point of reference from where interactions will sparkle.
- You describe classes in the code
- You can't describe objects in the code
- We don't have an understanding of the use case just by looking at the code
- By the time we put objects in runtime, everything we put into the code is lost
- The code can't be trusted anymore to understand
- Gang of Four: not everything you need to understand about the program can be found in the source code
- Smalltalk thinking: Just trust the objects to do the right thing and everything will be fine
- Stevie Wonder: You believe in things you don't understand, you may suffer
- Where's the use case then? Consider the following image (if you want to find it, it's there, but good luck)
DCI
- roles: it's not a class, not an object
- roles makes sense only in a context
class InventoryEntry < ActiveRecord::Base
belongs_to :inventory_item
accepts_nested_attributes_for :inventory_item
before_save :define_new_balance_values
def define_new_balance_values
Context::ItemMovingAverageCostDefinition.new(self).define
end
end
Then comes the use case.
module Context
class ItemMovingAverageCostDefinition
def initialize(new_entry)
@new_entry = new_entry
end
def define
# 1. takes all item entries
entries = item_entries << new_entry
# 2. calculates moving average cost
entries.extend(AverageCostCalculator)
new_cost = entries.calculate_cost
# 3. saves the new cost into the item
item = new_entry.inventory_item
item.extend(AverageCostUpdater)
item.update_cost(new_cost)
end
private
attr_accessor :new_entry
def item_entries
new_entry.inventory_item.entries.where("quantity > 0").all
end
module AverageCostCalculator
def calculate_cost
total_cost > 0 ? total_cost / total_quantity : BigDecimal("0.0")
end
private
def total_quantity
self.reduce(BigDecimal("0.0")) { |sum, e| sum + e.quantity }
end
def total_cost
self.reduce(BigDecimal("0.0")) { |sum, e| sum + e.quantity * e.cost_per_unit }
end
end
module AverageCostUpdater
def update_cost(cost)
update_attributes(moving_average_cost: cost)
end
end
end
end
[1] http://st-www.cs.illinois.edu/users/smarch/st-docs/mvc.html [2] http://en.wikipedia.org/wiki/Yo-yo_problem [3] Alan Kay:The Early History of Smalltalk; ACM SIGPLAN Notices archive; 28, 3 (March 1993);pp 69 - 95
This is a scam!