GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS

Written by thoughtbot

it's all about behavior

How do you know when you need a class? What tells you, “I think I need to create another class”? The answer is: behavior. Not state, behavior.

Here’s an example:

class Order < ActiveRecord::Base
end

And our orders table:

  orders (id, customer, item, price, created_on)

That customer column is a varchar that contains the customer’s name. I want to keep it simple, so an Order contains an item name and its total price.

Our client then requests a feature, they want to be able to read and update their existing customers’ names. Ok, no problem:

class Order < ActiveRecord::Base

  def self.find_all_customers
  end

  def self.update_customer(new_name, old_name)
  end

end

Now our Order class is dealing with customer related behavior. This will get ugly. Obviously, we’re missing a class: Customer:

class Order < ActiveRecord::Base

  belongs_to :customer

end

class Customer < ActiveRecord::Base

  has_many :orders

end

We get the #find and #update methods for free from ActiveRecord::Base.

And our orders table is now:

  orders (id, item, price, created_on, customer_id)

And our new customers table is:

  customers (id, name)

This is a very simple example. The key thing is that we didn’t introduce that Customer class until our client asked for a featured related to customers. We had new behavior and instead of making the Order class handle both order and customer responsibilities we introduced a new class. Now our responsibilities are cleanly separated and each of our classes are responsible for 1 thing, Order for orders and Customer for customers.

Another example:

class Company < ActiveRecord::Base
end

And our companies table:

  companies (id, street, city, state)

Now our client asked that when viewing a Company, they’d like its address plotted on a Google map, no problem. Google maps, like any mapping API, needs the latitude and longitude of the address to plot. So we create a method on Company to handle that:

class Company < ActiveRecord::Base

  def geocode_address
    # return latitude and longitude
  end

end

Hmmm, why is the Company class doing address geocoding? Seems like I’m missing something. Of course, an Address class. Let’s change it:

class Company < ActiveRecord::Base

  composed_of :address,
    :mapping => [%w(street street),
                 %w(city city),
                 %w(state state)]

end

class Address

  attr_reader :street, :city, :state

  def initialize(street, city, state)
    @street, @city, @state = street, city, state
  end

  def geocode
    # return latitude and longitude
  end

end

That’s better. I don’t want my Company objects doing geocoding, that should be an Address object’s responsibility (even our method names are better company.geocode_address vs. company.address.geocode; 1 word, short and to the point).

Once again by reflecting on our class’s responsibilities we discovered missing key objects. By focusing on behavior we end up with small objects, each doing 1 responsibility and our responsibilities are distributed where they belong. When someone says “hey I think geocoding is not working right”, is a new developer going to look for the geocoding responsibility in the Company class? Of course not, the application is geocoding an address, so its logical to look for it in an Address class.

THIS IS OBJECT-ORIENTED (OO) PROGRAMMING. Objects are all about behavior, discovering objects and distributing responsibilies results in maintainable, easy to understand systems. Systems in which each object has very few methods, each method is short, each class has 1 responsibility and there is very little conditional logic. There are no ‘god’ classes – huge classes that do a large number of totally un-related responsibilities (like our Order handling Customer responsibilities or our Company doing geocoding). Many developers do not realize the fundamental concept of behavior in OO programming and as result spend their time progamming procedurally in languages that support classes and think they’re doing OO programming because they’re creating classes. They never make the ‘jump’ to objects and never get to realize that code can be a lot more elegant and programming can be a lot more enjoyable than wading through nested ‘if’ statements.

There is no problem or no domain complex enough that objects can not simplify. “It’s a complicated problem” is not an excuse for convoluted, long, conditional-logic laden code. You need to challenge yourself and think how to simplify the problem because your code is not going to be maintainable.

A common complaint among people new to objects is that its hard to track down where the actual work takes place. This is true, by distributing responsibilities, several messages may need to be sent to several different objects; this means you may have to open up more than 1 file. This is the beauty of objects, the hardest problem can be solved very easily by a group of objects each doing their 1 simple responsibility but collectively solving a complex problem.

it is not about state

State (i.e. instance variables, attributes, etc.) is not a reason to create a new class. For example:

class Patient < ActiveRecord::Base
end

class Age

  attr_reader :age

  def initialize(age)
    @age = age
  end

end

And our patients table:

  patients (id, name, age)

That Age class has no behavior, just state. This class is un-necessary and adds nothing to the system. These types of classes are usually referred to as “dumb data holders”, they’re just state and getters and setters – they have no interesting behavior.

This style of modeling is common among data-modelers/DBAs. What is in a database? State. That’s it, there’s no behavior in a database. There’s just tables and data. The database is usually controlled by a ‘god’ class called a database management system (DBMS). These are all the functions that handle query parsing and execution, transaction management, concurrency, etc. So a database is a collection of functions (DBMS) reading and writing a bunch of data. This sounds familiar. It’s called procedural programming, the kind you first learned in school using C or BASIC.

So when modeling a system a data-modeler/DBA will probably first attempt to be really DRY and normalize the schema. The schema for our above Order example becomes:

  orders (id, item, price, created_on, customer_id)

  customers (id, name, address_id)

  addresses (id, street, city_id)

  cities (id, name, state_id)

  states (id, name)

Then they’ll turn each of those into classes:

class Order < ActiveRecord::Base
 
  belongs_to :customer

end

class Customer < ActiveRecord::Base

  has_many :orders

  belongs_to :address

end

class Address < ActiveRecord::Base

  belongs_to :city

  def geocode
    # return latitude and longitude
  end

end

class City < ActiveRecord::Base

  belongs_to :state

end

class State < ActiveRecord::Base
end

Customer, Order and Address are nice and worth having, however City and State add nothing and are simply “dumb data holders”. “But wait City and State have all the interesting CRUD behavior they get for free from ActiveRecord::Base, they’re not dumb data holders”, they do have the interesting CRUD behavior but our client only asked to geocode an address, they did not ask to CRUD cities and states; therefore its un-necessary.

Data modeling and object modeling are 2 different things. Data modeling comes from the database world and tends to focus on state. Object modeling focuses on behavior.

Although these examples are in Ruby, the language makes no difference. Object modeling is object modeling, if a language supports classes then these examples would be just about the same in any language.

Only create classes when new behavior is asked for and there’s currently no logical place for it. Do not cram more and more behaviors into existing classes, soon these classes will be very large, with many methods and wildly different responsibilities; these ‘god’ classes should be a clear indication that thinking about behavior (i.e. thinking in objects) is not happening.

but what about join models in Rails

So I said not to create classes that don’t have any interesting behavior i.e. “dumb data holders”, so you explain the following example to me:

class User < ActiveRecord::Base

  has_many :memberships

end

class Membership < ActiveRecord::Base

  belongs_to :user
  
  belongs_to :group

end

class Group < ActiveRecord::Base

  has_many :memberships

end

And our memberships table:

  memberships (id, user_id, group_id, active)

active is a boolean allowing a User’s Membership to be activated/deactivated. It’s 1 attribute, no interesting behavior and there’s a class for it. I would argue that this is a limitation of Rails and its inability to update attributes in a join table i.e. if we had a groups_users join table instead of a Membership model. This is a trade-off Rails developers have to deal with.