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
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
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
Order class is dealing with customer related behavior. This will get ugly. Obviously, we’re missing a class:
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
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.
class Company < ActiveRecord::Base end
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.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
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
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
patients (id, name, age)
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
Address are nice and worth having, however
State add nothing and are simply “dumb data holders”. “But wait
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
memberships (id, user_id, group_id, active)
active is a boolean allowing a
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.