Skip to content

Instantly share code, notes, and snippets.

@kurko
Last active December 14, 2015 16:48
Show Gist options
  • Save kurko/5117251 to your computer and use it in GitHub Desktop.
Save kurko/5117251 to your computer and use it in GitHub Desktop.
Object-Oriented Programming for defining what the system is and what it does

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.

The use case

Consider the user case where we have an inventory containing items and entries, items having many entries,

  1. Whenever a new entry is created
  2. I want to take all entries of the new entry's associated item
  3. I want to calculate the average moving cost of all these entries
  4. 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.

Data Objects centric design

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".

Service Objects

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.

Command objects centric design

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.

DCI

  • 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

@sogamoso
Copy link

sogamoso commented Mar 8, 2013

This is a scam!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment