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.
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)
In this case 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.