Posts tagged with oop

Object Orientation in Ruby and Elixir

When talking about mainstream programming languages, we often put them into two major buckets: object oriented programming and functional programming. There are other programming paradigms but we act like OOP and FP are oil and water. In this article I'll be blurring the lines of these two paradigms.

There's been a bit of discussion in the Erlang and Elixir world around this topic. It's recently been stated that Elixir is "the most object oriented language." Avdi Grimm has implemented exercises from Brian Marick's Functional Programming for the Object-Oriented Programmer, originally written in Clojure. There's even a hilarious library and lightning talk from Wojtek Mach for out-of-the-box object orientation in Elixir.

These resources shed light on OOP in Elixir, but they don't demonstrate the building blocks that comprise a working model. In this article, we will build an object system in Elixir from scratch and as bare bones as possible. To do this, all of the Elixir code will be based off Ruby examples. The goal is to highlight some of the core concepts of Elixir and liken them to core concepts of Ruby. (Some familiarity with Elixir is assumed.)

Though we'll build a working model, it's not meant to be an ideal object system nor am I advocating for this style of programming. In fact, the Erlang community relies on vastly different patterns. This is merely an exercise in learning about message passing and state management in Elixir. There are likely several ways to accomplish the same effect and I'd love to hear about other techniques in the comments.

Primary Concepts in Object Orientation

Object orientated and functional programming have coexisted since the dawn of modern computing. Functional programming originated from mathematics and was naturally conceived first. Object orientation came shortly after with Simula, whose goal was to make state management more straightforward.

Most literature on object orientation rehashes the same core programming concepts: state, behavior and polymorphism. In Ruby, this translates to instance variables, methods, and duck typing. These rudiments can be expanded to other OO concepts, like encapsulation and the SOLID principles.

But modern object orientated languages are unnecessarily limiting. It's almost as though they're missing the forest for the trees. The great techniques in object orientation can be written in a functional style, should you want them, while retaining purity and safe concurrency, which we all want.

A Basic Object

An object conjoins state and behavior. It encapsulates state internally and exposes behavior externally. Encapsulation is important because it is an effective means of organizing and controlling state. Behavior is the means by which we change state.

Consider the following example of a car in Ruby. The car has a position, x, and the ability to drive forward. When it drives, it increments x by 1.

# ruby
class Car
  DEFAULT_ATTRS = {
    x: 0
  }

  def initialize(attrs = {})
    @attrs = DEFAULT_ATTRS.merge(attrs)
  end

  def drive
    old_x = @attrs[:x]
    @attrs[:x] += 1

    puts "[#{self.class.name}] [#{@attrs[:color]}] X .. #{old_x} -> #{@attrs[:x]}"
  end
end

Notice in the above class, when #drive is called, it prints the name of the class, its color attribute, and the change in the state of x.

The variable @attrs is the encapsulated state. The #drive method is the behavior. What's beautiful about this class is that once instantiated, we can just call the #drive method and we don't have to think about the internal state of x. Let's do that now.

# ruby
car = Car.new({color: "Red"})
car.drive #=> [Car] [Red] X .. 0 -> 1
car.drive #=> [Car] [Red] X .. 1 -> 2

We call #drive twice and the internal state of x changes. Encapsulation is simple and powerful.

We can write this "class" and this "object" in Elixir. The primary difference is how we encapsulate state and invoke behavior. Instead of encapsulation being a first class citizen like it is in Ruby, we use recursion to represent state. Instead of calling methods like we do in Ruby, we pass a message.

# elixir
defmodule Car do
  @default_state %{
    type: "Car",
    x: 0
  }

  def new(state \\ %{}) do
    spawn_link fn ->
      Map.merge(@default_state, state) |> run
    end
  end

  defp run(state) do
    receive do
      :drive ->
        new_x = state.x + 1
        new_state = Map.put(state, :x, new_x)

        IO.puts "[#{state.type}] [#{state.color}] X .. #{state.x} -> #{new_x}"

        run(new_state)
    end
  end
end

Here's how we'd use it:

# elixir
car = Car.new(%{color: "Red"})
send(car, :drive) #=> [Car] [Red] X .. 0 -> 1
send(car, :drive) #=> [Car] [Red] X .. 1 -> 2

Let's break this Elixir code down.

The new/1 function is named "new" to mirror Ruby's .new method, but this could be named anything. This function creates a new Elixir process using spawn_link/1. We can consider spawn_link/1 the equivalent to Ruby's special .new method. By spawning a new process, we have now created an area to encapsulate state.

If you're not familiar with Elixir processes, you can think of them the same way you think of operating system processes - in fact, they're modeled after OS processes but are extremely lightweight and significant faster. They run independent of each other, have their own memory space that doesn't bleed, and can fail in isolation.

Inside the newly created process, the default attributes are merged with the attributes passed as an argument. Then, the recursive function run/1 is called.

# elixir
Map.merge(@default_state, state) |> run

The run/1 function is the core of our "object" and the state is recursively passed to run/1 over and over, encapsulating the state as an argument to the function. When we want to update the state, we call the run/1 function with the new state.

Let's look more closely at the run/1 function.

# elixir
defp run(state) do
  receive do
    :drive ->
      new_x = state.x + 1
      new_state = Map.put(state, :x, new_x)

      IO.puts "[#{state.type}] [#{state.color}] X .. #{state.x} -> #{new_x}"

      run(new_state)
  end
end

One key component of getting this to work is the call to the receive function. When receive is called, it will block the current process and wait until the process receives a message. Remember, this code is running in a new process all to its own. When a message is passed to the process, it will unblock itself and run the code declared in the proceeding block.

This proceeding block calculates a new state by incrementing x into a new variable, updating x in the map that represents the state, and then recursively calling the run/1 function. After calling run/1 recursively, the process again blocks on receive. It continues to recursively do this indefinitely until the run/1 function decides not to call itself anymore. When we no longer recurse, the process dies and the state is garbage collected. (This non-recursive case is not represented in this code.)

Let's see again what "instantiation" and message passing looks like. The built-in function, send/2 is used to send a :drive message to the process twice.

# elixir
car = Car.new(%{color: "Red"})
send(car, :drive) #=> [Car] [Red] X .. 0 -> 1
send(car, :drive) #=> [Car] [Red] X .. 1 -> 2

In the above code, calling Car.new/1 spawns the process and returns a process ID, or "pid." It then sends a message to this pid using send/2. send/2 is in essence the same as calling a method in Ruby. The originator of the term object orientation, Alan Kay, feels remissed that message passing has been displaced by method invocation. The major difference between message passing and method invocation is that message passing is asynchronous - more on that later.

That's our basic object. We have encapsulated state and provided behavior that changes the state. The Ruby version hides state in an instance variable and the Elixir version makes state explicit as a recursive function argument. The Ruby version calls methods, the Elixir version passes messages.

Inheritance

Beyond state and behavior, inheritance is another core tenant of object oriented programming. Inheritance allows us to extend types (classes) with new state and behavior.

Inheritance is a first class citizen in Ruby, making it easy to categorize state and behavior into subtypes. The following code should be palpable to all Rubyists. This code creates a new Truck type as a subtype of Car and adds an #offroad method only available to trucks.

# ruby
class Truck < Car
  def offroad
    puts "Going offroad."
  end
end

Since we inherited from the Car class, we can call both the #drive and the #offroad methods on an instance of the Truck class.

# ruby
truck = Truck.new({color: "Blue"})
truck.drive #=> [Truck] [Blue] X .. 0 -> 1
truck.offroad #=> Going offroad.

That's the Ruby version. Elixir doesn't have classes. Inheritance in Elixir is not a first class citizen. It's going to require more setup and ceremony to accomplish.

First, how do we represent types and subtypes without classes? The observant reader would have noticed that upon defining the Car module in Elixir, one of the default values was a field named type with a value of "Car." In Elixir, classes and types can be represented as plain data, as binaries (string). The concept of using data to represent types, and not concrete classes like with Ruby, is pervasive in functional programming - take for example records and tagged tuples.

To model inherited types in Elixir we'll use data to represent the Car type and the Truck subtype. To mimic the inheritance of behavior (methods) that a subtype derives from a parent type, we'll maintain an instance of Car that we delegate message to.

# elixir
defmodule Truck do
  def new(state \\ %{}) do
    spawn_link fn ->
      typed_state = Map.merge(%{type: "Truck"}, state)
      parent = Car.new(typed_state)

      Map.merge(%{parent: parent}, typed_state) |> run
    end
  end

  def run(state) do
    receive do
      :offroad ->
        IO.puts "Going offroad."
        run(state)
      message ->
        send(state.parent, message)
        run(state)
    end
  end
end

Let's break down the above code.

In the new/1 function, we're overriding the value of the type property and instantiating a new Car. This becomes our typed data. We then spawn our parent process.

# elixir
typed_state = Map.merge(%{type: "Truck"}, state)
parent = Car.new(typed_state)

We need to keep our parent process around so that we can delegate messages when the subtype doesn't directly respond. Then, we call our run/1 function.

# elixir
Map.merge(%{parent: parent}, typed_state) |> run

The run/1 function in the Truck module should look familiar. We've added a new :offroad message that we respond to. When the Truck process receives a message it doesn't understand, it forwards it on to the parent Car process.

Let's see how it runs.

# elixir
truck = Truck.new(%{color: "Blue"})
send(truck, :drive) #=> [Truck] [Blue] X .. 0 -> 1
send(truck, :offroad) #=> Going offroad.

You can see that the Truck type has inherited all of the behavior of the Car type.

Polymorphism

Polymorphism is one of object orientation's strongest qualities. It is to programming what interchangeable parts is to manufacturing. It allows us to substitute subtypes for their parent type wherever the parent type is used. In addition in Ruby and Elixir, it allows us to substitute any type for another type as long as it responds to the right method or message.

Just like inheritance, polymorphism adheres to the Liskov substitution principle, a well established characteristic of good object oriented design and part of the SOLID design principles.

First, polymorphism in Ruby. We'll use the Car and Truck instances to show that they are interchangeable with regards to the #drive method. We'll randomly select either instance and call #drive.

# ruby
car = Car.new(%{color: "Red"})
truck = Truck.new(%{color: "Blue"})

[car, truck].sample.drive #=> [Car] [Red] X .. 0 -> 1
                          #=> OR
                          #=> [Truck] [Blue] X .. 0 -> 1

The Array#sample method will return either a Car or a Truck instance, but since these are duck typed objects we can successfully call #drive on either. If we had another class that didn't inherit from Car but also contained a #drive method, we could substitute an instance of that class here as well. Polymorphism at its finest.

It's equally as easy in Elixir. Instead of calling methods, we'll pass messages. The only major difference between the Ruby and Elixir version is how we select the random object or process. The rest is virtually identical.

# elixir
car = Car.new(%{color: "Red"})
truck = Truck.new(%{color: "Blue"})

Enum.random([car, truck])
  |> send(:drive) #=> [Car] [Red] X .. 0 -> 1
                  #=> OR
                  #=> [Truck] [Blue] X .. 0 -> 1

Polymorphism is an inherent part of Elixir, though it's seldom thought of this way. An Elixir process will gladly receive any message you pass it, regardless of whether it can do something with that message. There are no restrictions on which messages can be passed. A phantom message will simply sit in the process's mailbox, but it behooves me to mention that unhandled messages can cause memory leaks.

Asynchrony

For most intents and purposes, we've built the major components of an object oriented system in a functional language. It has some flaws and doesn't use the most sophisticated Elixir tools, but it showcases that it's possible to represent these patterns in Elixir.

There's one not-so-subtle nuance hidden in these code examples that would rear its head immediately upon actual implementation. Calling methods in Ruby is synchronous and passing messages in Elixir is asynchronous. In other words, calling a Ruby method will pause the program, execute the body of that method, and return the result of that method to the caller. It's a blocking, synchronous activity. Passing a message in Elixir is a non-blocking, asynchronous activity. Elixir will send a process a message and immediately return without waiting for the message to be received.

This can make trivial things in Ruby more cumbersome in Elixir. Take for example simply trying to return a value from a passed message. In Ruby this is straightforward.

# ruby
class Car
  def color
    "Red"
  end
end

Car.new.color #=> Red

We can do the same thing in Elixir when we're not talking about message passing. Below, we're calling a function that has a return value and everything works as expected.

# elixir
defmodule Car do
  def color do
    "Red"
  end
end

Car.color #=> Red

But once we start working with processes, this becomes more challenging. Here is an intuitive but broken piece of Elixir code.

# elixir
defmodule Car do
  def new do
    spawn_link(&run/0)
  end

  def run do
    receive do
      :color -> "Red"
    end
  end
end

car = Car.new
send(car, :color) #=> :color

Did you expect that sending the car process a :color message would return the value "Red"? Instead, the return value is :color. send/2 returns the message that was sent to a process, not the value that was returned once the message has been handled.

Message passing in Elixir is asynchronous, but if we want to model the synchronous behavior of Ruby's method invocation we'll have to get a little creative.

Since receive blocks the process and waits for a message, we can use that in the context of our caller. So, whoever calls :color would need to block and wait for a response in order to continue the program, just like Ruby.

Unlike Ruby, there's a bit more ceremony in getting this to work. We'll need to send the caller's pid into the callee. The callee will then send a message back to the caller with the final return value.

# elixir
defmodule Car do
  def new do
    spawn_link(&run/0)
  end

  def run do
    receive do
      {:color, caller} ->
        send(caller, {:color, "Red"})
    end
  end
end

car = Car.new
send(car, {:color, self})
receive do
  {:color, response} => response
end #=> Red

In the above code, we are passing the caller's pid into the callee, which can be accessed by calling self/0. The caller then waits for a message from the callee containing the response. In the caller, the response is pattern matched to extract the value. The return value from the caller's receive block is the final response of "Red".

That's a lot of ceremony. Luckily, Elixir has nice abstractions to avoid the litany. Here, we'll look at Agents. Using Agents, we can treat our code synchronously again and eliminate the low-level send and receive functions.

# elixir
defmodule Car do
  def new do
    {:ok, car} = Agent.start_link fn -> %{color: "Red"} end
    car
  end

  def color(car) do
    Agent.get(car, fn attrs -> attrs.color end)
  end
end

car = Car.new
Car.color(car) #=> Red

Elixir has a variety of tools that help keep our code clean while programming synchronously. One such tool is GenServer.call/3. GenServers are a very useful abstraction around processes that allow us to implement state and behavior in a simplified form, much like Ruby.

As a final thought around asynchrony, I'd like to mention two things.

Message passing in Elixir is slower than method invocation in Ruby. This is due to the delay between when the message is sent and when the receiving process handles it.

Message passing in Elixir is the primitive concurrency construct. It's the actor model of concurrency. This is not an option in Ruby unless you're using a library like Celluloid. Concurrency in Ruby is usually threaded. The actor model is an abstraction on threads, baked into Elixir, that provides a level of concurrency not attainable in Ruby.

Wrapping Up

We've blended object orientation and functional programming throughout the course of this article. Whether you prefer the Ruby version or the Elixir version, they both have their place.

Object orientation in Ruby is simple, elegant, and makes me happy. It doesn't provide the concurrency controls that Elixir offers, but the programming model is pleasant to use. On the other hand, Elixir allows us to model a system in an object oriented fashion while leveraging more powerful concurrency controls.

Object orientation in Elixir may or may not be a viable approach. I don't have enough data yet to draw conclusions. It is worth mentioning again that the functional community uses different patterns. The creator of Erlang, Joe Armstrong, has lamented over OOP due to the blending of state and behavior, though I find this mixture inevitable with processes. So while it may not be commonplace to use functional languages in an objected oriented style, it's certainly possible and may be more graceful when modeling some domains.

Happy coding!

Posted by Mike Pack on 09/04/2016 at 02:00PM

Tags: actors, concurrency, fp, oop, elixir, ruby


DCI and the Single Responsibility Principle

The single responsibility principle (SRP) is one of the most widely followed ideals in object oriented programming. For decades, developers have been striving to ensure their classes take on just enough, but not too much, responsibility. A valiant effort and by far one of the best ways to produce maintainable code.

SRP is hard, though. Of all the SOLID design principles, it is the most difficult to embrace. Due to the abstract nature if its definition, based purely on example instead of directive process, it's hard to concretize. More specifically, it's difficult to define "responsibility", in general or in context. There are some rules-of-thumb to help, like reasons for change, but even these are enigmatic and hard to apply.

Simply put, SRP says a class should be comprised of just one responsibility, and only a single reason should force modification.

Let's take a look at the following class which clearly has three responsibilities and therefore breaks SRP:

class User < ActiveRecord::Base
  def check_in(location); ... end
  def solr_search(term); ... end
  def request_friendship(other_user); ... end
end

This class would require churn for a variety of reasons, some of which include:

1. The algorithm for checking in changes.
2. The fields used to search SOLR are renamed.
3. Additional information needs to be stored when requesting a friendship.

So, based on the rules of SRP, this class needs to be broken out into three different classes, each with their own responsibility. Awesome, we're done, right? This is often the extent of discussions around SRP, because it's extremely difficult to provide solutions beyond contrived, minute examples. Theoretically, SRP is very easy to follow. In practice, it's much more opaque. It's too pie in the sky for my taste; like most OOP principles, I think SRP should be more of a guideline than a hard-and-fast rule.

The real difficulty of SRP surfaces when your project grows beyond 100 lines of code. SRP is easy if you're satisfied with single method classes or decide to think about responsibility exclusively in terms of methods. In my opinion, neither of these are suitable options.

DCI provides more robust guidelines for following SRP, but we need to redefine responsibility. That's the focus of this article.

Identity and Responsibility

OK, let's try to elucidate responsibility, but first, let's talk about object orientation.

The word "object" can be defined as a resource that contains state, behavior and identity. Its state is the value of the class's attributes. Its behavior is the actions it can perform. And its identity is...well...it's object id? It feels strange to narrow the definition of identity into a mere number. I certainly don't identify myself by my social security number. My identity is derived from my name, the things I enjoy doing, and potentially my environment. More importantly, my identity is always changing.

While building a class, when was the last time you thought about the forthcoming object ids of the instances of that class? I hope never. We don't program classes with identity in mind, yet if we're trying to model the world, it's an intrinsic component. Identity means nothing to us while building classes, yet everything to us in the real world.

Therefore, it's appropriate to say that the mental model of the programmer is to map identity to state and behavior, rather than to object id. Object id is a quality of uniqueness.

Identity is closely related to responsibility. As expressed above, I don't identify by my social security number, but by my state and behavior. When we attempt to find the appropriate location for a method definition, we look at the responsibility of the prospective classes. "Does this method fit into this class's single responsibility?" If we consider that identity should truly be a representation of an object's state and behavior, we can deduce that identity is a derivative of responsibility.

An example of this observation is polymorphism; probably the most predominant and powerful object-oriented technique. When we consider the possible duck-typed objects in a scenario, we don't think, "this object will work if its object id is within the following set..." We think "this object will work if it responds to the right methods." We rarely care about the object id. What's important is the true identity of an object: the methods it responds to.

DCI Roles

Object ids mean nothing to programmers. Defining identity by the memory location of an object is very rarely a means for writing effective software. Computers care about memory locations, not programmers. Programmers care about state and behavior, hence, the reason we build classes.

SRP is about acutely defining state and behavior, thus, identity. In DCI, we define state through data objects and behavior through roles. Generally speaking, behavior changes state so the primary focus is on behavior. If we ask, "what can an object do?", we can subsequently ask, "how does its state change?"

We still haven't really defined responsibility. As a human being, my responsibilities change on a regular basis. At one point, I'm responsible for writing an email. At another, I'm responsible for mentoring a developer. Rarely, although occasionally, am I responsible for doing both. Enter role-based programming.

We can reprogram the example class above using DCI data objects and roles:

# Data
class User < ActiveRecord::Base; end

# Roles
module Customer
  def check_in(location); ... end
end

module Searcher
  def solr_search(term); ... end
end

module SocialButterfly
  def request_friendship(other_user); ... end
end

Now, each role has a single responsibility. If we define responsibility by a role played by a data object, it becomes obvious where new methods should go and when you're breaking responsibility. Let's give it a shot.

Say we want to add functionality that allows users to accept a requested friendship. First ask the question in terms of business objectives, "As a social butterfly, can I accept a friendship?" By converting our expectations into English, we can then easily map business rules to code. It would clearly be wrong if we ask, "As a searcher, can I accept a friendship?" Therefore, #accept_friendship should belong in SocialButterfly:

module SocialButterfly
  def request_friendship(other_user); ... end
  def accept_friendship(other_user); ... end
end

By defining responsibility as a role, we can converge on contextual identity, the true essence of object orientation. While building a role, we are building identity, a crucial part of a programmer's mental model. Roles are the inverse abstraction of classes. While classes focus on abstracting away identity, roles focus on highlighting it. It's no wonder it's so difficult to define responsibility when we're programming classes, not object.

DCI and SRP

It's hard to define responsibility. It's even harder to program for it. As an artifact, the responsibility of a class often ends up being either too narrowly or too broadly defined. Define responsibility too narrowly, and it's daunting to wrap your head around 1000 classes. Define responsibility too broadly, and it's arduous to maintain and refactor.

By defining responsibility as a role, we have a clear notion of behavioral locality. We can ask questions like, "as a customer, can I add an item to my cart?" If the answer is yes and we've appropriately named our roles, the method belongs in that role. This gives us a means for defining responsibility, and we can refactor accordingly.

Roles won't alleviate potential clutter, but they can give us a structure for defining responsibility. With DCI, we can talk about responsibility in terms of directive process instead of contrived examples.

First, understand the business objectives of the system and subsequently understand the roles. Understand the roles and subsequently understand the responsibilities.

Posted by Mike Pack on 03/12/2013 at 08:28AM

Tags: dci, oop, solid, srp


DCI and the Open/Closed Principle

The open/closed principle (OCP) is a fundamental "run of thumb" in object-oriented languages. It has hands in proper inheritance, polymorphism, and encapsulation amongst other core properties of object-oriented programming.

The open/closed principle says that we should refine classes to the point at which we eliminate churn. In other words, the less times we need to open a file for modification, the better. With DCI, we can compose objects while still following OCP.

Extension is Inheritance

Wikipedia's definition of inheritance:

Inheritance is a way to reuse code of existing objects, or to establish a subtype from an existing object, or both, depending upon programming language support.

By using #extend to modify objects at runtime, we are both reusing code from data objects while also forming a new subtype of the data object.

A typical DCI context might look like this:

app/use_cases/customer_purchases_book.rb

customer = User.find(1) # customer is a data object
customer.extend(Customer) # inject the Customer role
customer.purchase(book) # invoke Customer#purchase

After calling #extend, the user object can be used as both a data object and a purchasing customer. The #purchase method likely uses attributes of the user object to create joins between him and his book. We're reusing code from its former self.

Similarly, the new object is now a subtype of its former self. That is, the Customer version of user can be used polymorphically in place of the data object itself.

The Open/Closed Principle (OCP)

The open/closed principle is often discussed in the context of inheritance; we use inheritance to adhere to the "closed" aspect of OCP. In order to follow OCP, a class can be open for extension, but closed for modification. Let's look at how the principle could be applied with classical inheritance to reimplement the above scenario.

We have a dumb data object:

class User
  # A dumb data object
end

To abide by the "closed" aspect of OCP, we define a subtype of the User class; we do not modify the class itself:

class Customer < User
  def purchase(book)
    # Update the system to record purchase
  end
end

Somewhere else in our codebase, we tell a user to purchase his book:

customer = Customer.new
customer.purchase(book)

This is great, we've accomplished OCP by ensuring that any customer related aspects of a User are neatly tucked away in the Customer class. In order to change the behavior of a user, we formed a new class while leaving the User class alone.

This is the guts of the open/closed principle. We want to structure our classes in such a way as to ensure they never need to change. Guaranteeing the classes don't change is also a function of the method bodies.

DCI and OCP

Following OCP without incorporating the Data, Context and Interaction architecture has proven to lead to looser coupling, stronger encapsulation, and higher cohesion. Just apply it and your world will be rainbows and ponies!

Wrong. While OCP has absolutely helped in producing higher quality code, it's just another lofty object oriented principle. It's very difficult to adhere to all principles, and some may be entirely inappropriate in various scenarios. The SOLID principles (of which OCP is one) are a great frame-of-reference when discussing software design, however, heeding to them 100% of the time is frankly, impossible.

I often find it very difficult to ensure the first iteration of my core software, test suite, and ancillary code meet the qualifications of the SOLID principles. Not because I don't understand or refuse to apply them, but because I'm human and I'm working with frequently-varying business rules. Principles in general end up being this pie-in-the-sky goal; I prefer to just write software.

One of the reasons I love DCI so much is because it forces you to work in an orthogonal way. It breaks the cemented programming models we've seen for over 20 years. Models which, in my opinion, do not lend themselves towards these principles. DCI acts much like a lighthouse: guiding you towards proper object orientation.

DCI enables you to automatically apply many best practice principles in object oriented programming. The open/closed principle is one.

Closed for Modification

The whole point of DCI is to decouple what changes from what remains constant. In DCI, our data objects are strictly persistence related, and as such, do not change frequently. The way in which we use data objects is often what changes.

So, when we build out a data object...

app/models/user.rb

class User < ActiveRecord::Base
  # A dumb data object
end

...it's closed for business.

DCI tells us that if we want to add behavior to this class, we should be doing so within a role. A deliberate effect of this is that our class remains closed. OCP is telling us to optimize our classes so that we never need to modify them. This aspect of OCP is baked into the core of DCI.

Open for Extension

The name says everything. The best way to accomplish DCI in Ruby is to use #extend. We seek to inject roles into objects at runtime to accomplish our behavioral needs. Let's create our Customer role:

app/roles/customer.rb

module Customer
  def purchase(book)
    # Update the system to record purchase
  end
end

We would then join our data and roles within a context:

app/use_cases/customer_purchases_book.rb

class CustomerPurchasesBook
  def initialize(user, book)
    @customer, @book = user, book
    @customer.extend(Customer)
  end

  def call
    @customer.purchase(@book)
  end
end

The open/closed principle states that a class should be open for extension. Within the above context, we extend our user object with the Customer role. Our DCI code adheres to this rule.

OCP talks a lot about extension of classes via inheritance. Demonstrations of OCP are usually forged with classes, instead of objects. In the above paragraph, I say that classes are open for extension, but the user object is extended. When we define a class, it's simply a container in which methods live. That container then becomes part of an object's lookup hierarchy. So, behaviorally speaking, there's no semantic difference between composing an object from scratch with DCI and creating an instance of a class.

In the customer example above, we use #extend as a means of composing the customer object to include its necessary behavior. We do this in lieu of classical inheritance. As I mentioned earlier in this article, extension is inheritance.

The Silver Bullet

By applying DCI, you are ever-so-nicely nudged into following OCP. DCI is a paradigm shift, but it's coated with reward. By simply working in objects and extending them at runtime, you are guided towards many well-respected, object-oriented principles. The strong emphasis DCI puts on decoupling static classes from dynamic behavior means that your classes remain closed for modification.

DCI contexts are naturally built for OCP. Use cases rarely change. If a user is buying a book, the use case of that purchase remains relatively constant. Since contexts act as simple glue between data and roles, if a use case changes, it's likely to be a new context. In this regard, contexts remain closed for modification.

DCI won't help you properly construct your roles, but it does guide you in the right direction. Since roles are actor-based, their methods tend to be use case specific. This means that role methods don't need to accomodate for drastic variations. If variation increases, I tend to reach for service objects to abstract that complexity.

There is no silver bullet to following object-oriented principles. We're always making tradeoffs. Managing complexity is inherently complex. DCI can help you cope by ensuring your objects remain open for extension, yet closed for modification.

Posted by Mike Pack on 12/18/2012 at 08:14AM

Tags: dci, oop, solid, ocp