What if we organized code by features?

I began asking myself this question when I was working on a large Rails project. Requirements changed fairly often and we needed to react quickly.

When trying to figure out how something could change, I had to backtrack through how it already works. This often meant tracing method calls through several different files and classes. A collection of methods and collaborators can be difficult to keep in your head.

Too many times, it was easier to search for the person on the team who implemented the original code to figure out how best to change it. Figuring out how it was all put together can be a distraction. I went from "let's make changes" to "how does this work?"

If we organized our implementation by what we look for when we change features rather than by the related class, could eliminate distraction? What if someone wanted to make a change to a feature, and I was able to pull up the code that represented that feature? Then I'd be able to feel confident that I could look in as few places as possible to get what I need.

I began experimenting with how Ruby could help me stay focused quite a lot. One of the results is Casting. Casting is a gem that allows you to apply behavior to an initialized object and to organize behavior around your features.

With Casting I could take the implementation details of an algorithm (the work to be done) out of individual classes of collaborating objects and move it into a centralized location. Rather than having 4 or 5 classes of things each contain a different part of the puzzle, I could put the puzzle together and have it ready for future changes.

Rather than each class knowing a lot about how a feature is put together, the classes could be small and focused on representing their data. A feature could grow or shrink within the context of every object it needed.

It's a bit different from other approaches where you wrap an object in another one to provide additional behavior, however.

Here's a simple example of what this means.

Applying new behavior

Rather than putting code in multiple different places, or putting code in a class merely because that type of object needed it at some point, we could put it into a a fetaure class.

Let's take a collection of objects and start up a game:

class Game
  def initialize(*players)
    @players = players
    # randomly select a leader
    @leader = players.sample
  end
end

This Game class expects to receive a collection of objects, one is selected as a leader and then what?

Well, if I put together the features for what the players and leader can do, or if I want to read and understand what they can do later so that I can make changes, I'll look first to the Game itself to understand.

I can put all the behavior I need inside this class. It makes a lot of sense to me to keep it there because it will be behavior specific to this feature. The Game won't exist without the behavior and the behavior won't exist without the Game.

class Game
  def initialize(*players)
    @players = players
    # randomly select a leader
    @leader = players.sample
  end

  module Player
    def build_fortress; end
    def plant_crops; end
  end

  module Leader
    def assemble_team; end
    def negotiate_trade; end
  end
end

When the game is initialized with the players, we can make those players become what we need.

Using Casting, we can allow the objects to have access to new behaviors by including Casting::Client and telling them to look for missing methods in their collection of behaviors.

class Account
  include Casting::Client
  delegate_missing_methods
end

With that change, any Account object (or whatever class you use) will keep a collection of delegates. In other words, these objects will keep track of the roles they play in a given context and have access to the behaviors for those roles. The object will receive a message and first run through its own methods before looking at its behaviors for a matching method.

The next step is to assign the roles:

class Game
  def initialize(*players)
    @players = players.map{|player| player.cast_as(Player) }
    # randomly select a leader
    @leader = players.sample.cast_as(Leader)
  end
end

Now each of these objects will have access to the behavior of the assigned modules.

The @leader has both Player behavior as well as Leader.

Later, if we decide to add a Guard role to our game or some other job for a player, any player may gain that behavior at any point we determine.

Adding Casting to my projects allows me to work with objects and apply their behaviors where I plan for them to be used. Then I am able to look for my implementation where the feature is defined and I'm not distracted searching through multiple files and classes to piece together my understand of how it all works.

Why not just use...

You might argue that you could simply create a wrapper using something like SimpleDelegator.

When using wrappers, we create new objects that maintain a reference to the original. We take those 2 objects and treat them as one.

Doing so might change our Game initializer like this:

class Game
  def initialize(*players)
    @players = players.map{|player| Player.new(player) }
    # randomly select a leader
    @leader = Leader.new(players.sample)
  end
end

One of the downsides of this is that we are working with a new set of objects. The self inside that Leader.new isn't the same object as players.sample. Any process which would need to search for an object in the collection of players might attempt to compare them for equality and get an unexpected result.

To reduce our mental stress as best we can, we want only the information which is neccessary to understand our system. With an additional layer wrapping our objects we could be making it more difficult to understand.

Here's a small example of how wrapper objects like this can lie to us:

class Account; end
require 'delegate'
class Player < SimpleDelegator; end

account = Account.new
player = Player.new(account)

account == player # => false
player == account # => true

account.object_id # => 70340130700420
player.object_id  # => 70340122853880

The result of these 2 comparisons are not the same even though we would expect them to be. The player object just forwards the == message to the wrapped object; whereas the account object will do a direct comparison with the provided object in the == method.

This complicates how we may interact with the objects and we must be careful to perform things in the right order.

If the object merely gains new behavior and remains itself, the outcome will give us relief:

require 'casting'
class Account
  include Casting::Client
  delegate_missing_methods
end
module Player; end

account = Account.new
player = account.cast_as(Player)

account == player # => true
player == account # => true

account.object_id # => 70099874674300
player.object_id  # => 70099874674300

Here we can see that the account and the player are definitely the same object. No surprises.

Wrapping up my objects is easy but I've spent my fair share of time tracking down bugs from the wrappers behaving differently than I expected. Time spent tracking down bugs is a distraction from building what I need.

Wrapping my objects in additional layers can affect my program in unexpected ways, interrupting my work. Although the code with wrappers is easy to read, it subtly hides the fact that the objects I care about are buried beneath the surface of the objects I interact with. By keeping my objects what they are and applying behavior with modules, I ensure that I can stay focused on the feature.

Our code is a communication tool about our expectations of how a program should execute. The better we focus on the actual objects of concern and avoid layers, the easier it will be to avoid unintentional behavior.

This is why I'm glad to have a tool like Casting to help me build systems that limit unnecessary layers.

Creating the shortest path to understanding

When I began building and working with Casting, it allowed me to flatten the mental model I had of my programs.

It's easy for a programmer to see a wrapper style implementation or subclass and understand the consequences. Unfortunately that extra layer can and does lead to surprises that cost us time and stress.

I do still use tools like SimpleDelegator, but I often look to ways to make my programs better reflect the mental model of the end user. Sometimes SimpleDelegator-like tools work well, other times they don't.

If the ideas in my mind are closer to those in the user's mind, I'm much more likely to a program that communicates what it is and what it does more accurately.

Developers who work together need to communicate effectively to build the right thing. Our code can help or hinder our communication. Sometimes, when we want an object to gain new behavior, we introduce tools like SimpleDelegator and in doing so, we add layers to the program and more to understand.

Casting, although it too needs to be understood, provides us the ability to add behavior to an object without additional layers which might introduce distraction.

Attempting to meet requirements and build a product well, means we need to consider how our code reflects the shared understanding of what it should do.

When requirements change, and they often do, we'll look to our code to understand the features. The faster we can find and understand our features, the faster and more confidently we will be able to react to changing requirements.

When I needed to react to changing requirements and couldn't easily find all the pieces of the feature, it wasn't a confidence inspiring result for my other team members. Everyone should be able to find and understand how a feature works.

Where to look and where to understand

By placing code in every class related to a feature, I gave myself many different places to look to build up my understanding of how it worked. I treated individual data classes as the only place to look for behavior, rather than creating the world I need with a feature class.

Organizing by class vs. feature makes me think about my product differently.

When I think about features, I remain focused on the goals of the end user. Each user of the system is only using it to achieve a specific purpose. Often we can become distracted by our code and forget the goals of the end user. Building up features is a continual reminder of the reason that code needs to be written and updated.

Thinking about the end user will help us implement only what is necessary for her or him to complete their work. We may better avoid getting tripped up my technical concerns.

When we add behavior in our data classes, it often ends up including behavior from many unrelated features.

Think about what you have in your classes and what they should or could be.

class Account
  def build_fortress; end
  def plant_crops; end

  def assemble_team; end
  def negotiate_trade; end

  def renew; end
  def cancel; end
  def update_payment_info; end

  def send_friend_request; end
  def upload_photo; end

  # etcetera...
end

With the above Account class there are many behaviors for vastly different concerns. If we were to move those behaviors into an object that represented the feature where the behaviors were required the class would be freed to better describe the data it represents.

class Account
end

Defending my focus

Being able to focus on my immediate problem drives me to think about how I want to structure my code. When I write code, I know that the next person to read it and change it may not be me. Maintaining my own mental model isn't good enough when solving a problem; programmers need to create code that helps someone else pick up the same mental model.

Sometimes adding layers to your code can help separate parts that should be separate. Sometimes adding layers means introducing distraction and distraction leads to bugs and lost time.

These ideas lead me to build Casting and write about Object-oriented design in Clean Ruby.

Take a look at your application's features and ask yourself if you could organize differently. Can you remove distractions? Could Casting help you build cohesive features?

Get a FREE sample chapter

Get a sample chapter of Clean Ruby and you'll be added to my periodic newsletter of helpful Ruby tips.