Objective Ways

What is Object

Intro

The object in the object-oriented paradigm is often described as data (named fields or properties) and procedures (named methods) bound together. Although it can be described this way technically, I do not like this definition because, in my opinion, it refers people to a wrong direction. When we hear "data and methods", we start thinking of them as of two equally important parts. But I believe objects are about methods in the first place, and about data in the last. Let me show you why. To demonstrate the idea, I am going to tell a little story. Remember, all the code written below serves only a demonstration purpose. I used an imaginary programming language, oversimplified all logic, and threw away as much as possible to keep the code blocks compact. It is not going to be an example of a real program, but only a visualisation of discussed things.

Chapter 1

Imagine you run a small bakery selling apple pies. There is only one product you offer, so the amount of your responsibilities is small enough to cope with them yourself: sorting the inbox, baking apple pies, communicating with clients, and delivering completed orders. Your job can be expressed in the following code.

email := read_last_email_in("contact@bakery.com");
if (email == "apple pie") {
  apples := get_apples_from(storage);
  sugar := get_sugar_from(storage);
  flour := get_flour_from(storage);
  if (enough_to_make_apple_pie(apples, sugar, flour)) {
    pie := make_apple_pie(apples, sugar, flour);
    package := pack_apple_pie(pie);
    deliver(package);
  } else {
    reply_to(email, "Sorry, out of ingredients");
  }
} else {
  reply_to(email, "Sorry, I make only apple pies");
}

As we can see, you are a smart person who is good at everything: reading emails, cooking pies, and delivering things. Your business is doing pretty well, and one day you decide to expand the assortment with cookies, and suddenly you feel that your job has become too difficult to do it by only one pair of working hands.

email := read_last_email_in("contact@bakery.com");
if (email == "apple pie") {
  apples := get_apples_from(storage, 300);
  sugar := get_sugar_from(storage, 120);
  flour := get_flour_from(storage, 500);
  if (enough_to_make_apple_pie(apples, sugar, flour)) {
    pie := make_apple_pie(apples, sugar, flour);
    package := pack_apple_pie(pie);
    deliver(package);
  } else {
    reply_to(email, "Sorry, out of ingredients");
  }
} else if (email == "cookie") {
  chocolate := get_chocolate_from(storage, 50);
  sugar := get_sugar_from(storage, 50);
  flour := get_flour_from(storage, 200);
  if (enough_to_make_cookie(chocolate, sugar, flour)) {
    cookie := make_cookie(chocolate, sugar, flour);
    package := pack_cookie(cookie);
    deliver(package);
  } else {
    reply_to(email, "Sorry, out of ingredients");
  }
} else {
  reply_to(email, "Sorry, I make only apple pies and cookies");
}
              

What to do? You look for your first employee, someone to take care of baking goods and let you focus on other things. Two candidates send their CVs: one is Lucy, she offers you her excellent cooking skills. The second one is Steven, he does not know how to cook, but, hey, he can hold a spoon or knife in his hands and give it by request while you will be working in the kitchen! What a hard choice, who would you hire? Are you, a modest businessman, would pay someone for just keeping things in hands? I would certainly not. Eventually you hire Lucy, and your code becomes simpler.

email := read_last_email_in("contact@bakery.com");
if (email == "apple pie") {
  if (lucy.can_make_apple_pie()) {
    pie := lucy.make_apple_pie();
    package := pack_apple_pie(pie);
    deliver(package);
  } else {
    reply_to(email, "Sorry, out of ingredients");
  }
} else if (email == "cookie") {
  if (lucy.can_make_cookie()) {
    cookie := lucy.make_cookie();
    package := pack_cookie(cookie);
    deliver(package);
  } else {
    reply_to(email, "Sorry, out of ingredients");
  }
} else {
  reply_to(email, "Sorry, I make only apple pies and cookies");
}

Motivation

The same principle is applicable to our code as well. A piece of code is not going to be simpler if you put user’s login and password (2 lines of trivial code declaring variables) into a separate object and keep an algorithm of encoding in place (50 lines of complex code requiring from the reader some knowledge). An object helps in reducing complexity only when it can do something for the rest of the program, in other words, when it has methods i.e. has behaviour. For instance, an object which can encode user’s credentials allows other classes to not care about subtleties of encryption and text encoding.

So, this is the first thing illustrating my idea that plain data is secondary when talking about objects.

Chapter 2

Time goes on, your kitchen grows, you hire another person, Peter, who can make apple pies, cookies, and also bread. And again you feel overloaded.

if (email == "apple pie") {
  if (bakers.can_make_apple_pie()) {
    idle_baker := first_free_baker();
    pie := idle_baker.make_apple_pie();
    package := pack_apple_pie(pie);
    deliver(package);
  } else {
    reply_to(email, "Sorry, out of ingredients");
  }
} else if (email == "cookie") {
  if (bakers.can_make_cookie()) {
    idle_baker := first_free_baker();
    cookie := idle_baker.make_cookie();
    package := pack_cookie(cookie);
    deliver(package);
  } else {
    reply_to(email, "Sorry, out of ingredients");
  }
} else if (email == "bread") {
  if (peter.can_make_bread()) {
    bread := peter.make_bread();
    package := pack_bread(bread);
    deliver(bread);
  } else {
    reply_to(email, "Sorry, out of ingredients");
  }
} else {
  reply_to(email, "Sorry, I make only apple pies, cookies, or bread");
}

It is again too much responsibilities on your shoulders. How does a good businessman solve it? Hires another person. Her name is Mary, she will spend all her time coping with emails.

email := mary.get_last_email_from_bakery_inbox();
if (email == "apple pie") {
  if (bakers.can_make_apple_pie()) {
    idle_baker := first_free_baker();
    pie := idle_baker.make_apple_pie();
    package := pack_apple_pie(pie);
    deliver(package);
  } else {
    mary.reply_to(email, "Sorry, out of ingredients");
  }
} else if (email == "cookie") {
  if (bakers.can_make_cookie()) {
    idle_baker := first_free_baker();
    cookie := idle_baker.make_cookie();
    package := pack_cookie(cookie);
    deliver(package);
  } else {
    mary.reply_to(email, "Sorry, out of ingredients");
  }
} else if (email == "bread") {
  if (peter.can_make_bread()) {
    bread := peter.make_bread();
    package := pack_bread(bread);
    deliver(bread);
  } else {
    mary.reply_to(email, "Sorry, out of ingredients");
  }
} else {
  mary.reply_to(email, "Sorry, I make only apple pies, cookies, or bread");
}

Your team has grown, expenses have grown too. However, you do not feel the problem is solved. Why? Apparently, because although Mary reads and writes emails for you, you are still totally involved in working with the inbox. You have to react to a given email and have to ask Mary to send a reply in case of an exceptional occasion. Although Lucy and Peter are responsible for baking things, you still need to engage yourself to every stage of their work: you decide what to bake, check whether an order can be made, ask the bakers to start working, pack up and deliver the ready product.

Like a wise businessman, you realise that it is time to completely rely on your team. The new process looks like the following.

mary.read_emails_from("contact@bakery.com");
mary.give_orders_to(lucy, peter);
for each baker in all_bakers() { // Peter and Lucy
  baker.report_to(mary);
  baker.give_ready_product_to(john);
}

As you can see, you hire another employee in your bakery, John, delivering orders to clients. The structure of your bakery has been designed from scratch, to release you from thinking about every aspect of the staff’s responsibilities. Now there is no reason for Mary to bother you every time she finds a new order, she talks to the bakers directly. In turn, the bakers do not need your intervention to reject an order or to send a baked pie to the waiting client.

In Code

When creating a simple utility or script doing one particular thing, it is easy to keep in one place everything: plain values, algorithms to process them, the control flow for every stage of the process. However, when the program grows in size and gets more functionality, attempts to keep control of all processes in one piece of code will turn it into the most complex and unmaintainable part of the program. Even extracting certain parts of those processes into separate places will not help because that piece of code still knows about all parts: the required order of performed actions, details of results at every stage, knowledge how those parts should be linked to each other.

The only way is to disengage from any low-level details of functionality and focus on managing other objects. And eventually this high-level code loses any mention of plain data processed in the program. This is the second reason why I believe that data is secondary in the OO world.

Chapter 3

Your bakery continues to gain more popularity, you hire more bakers to increase production, and one day it becomes obvious that Mary’s job is too complicated to handle it by one person. She needs not only to work with emails and give orders to the kitchen, but also decide whom to give a new order to.

And at this moment it has become a really complicated task because some of the bakers are better at cooking certain dishes, some of them may already have a queue of other orders, and it is better to take it into account to keep the work efficient.

class Mary {
  method work() {
    email := read_last_email_in(bakery_address);
    if (email == "apple pie") {
      first_idle_baker := first_idle_worker_making_apple_pies();
      first_idle_baker.make_apple_pie();
    } else if (email == "bread") {
      if (peter.is_waiting_for_order() OR peter.active_orders_count() < 2) {
        peter.make_bread() // It's better to assign him first when making bread
      } else {
        elisabeth.make_bread()
      }
    } else // other complex thinking
  }
}

To help her, you hire another employee, Harry. He is not a cook at all, he is scared of the kitchen stove, but is extremely good at planning and allocation. Now all orders are passed through his notebook and get assigned to the best suitable baker.

class Mary {
  method work() {
    email := read_last_email_in(bakery_address);
    if (email == "apple pie") {
      kitchen_coordinator.handle_apple_pie_order();
    } else if (email == "bread") {
      kitchen_coordinator.handle_bread_order();
    } else // ...
  }
}

mary.read_emails_from("contact@bakery.com");
mary.talk_to_kitchen_coordinator(harry);
harry.distribute_orders_between(lucy, peter, alex, elisabeth);
harry.delegate_emails_from_bakers_to(mary);
for baker in all_bakers() {
  baker.report_to(harry);
  baker.give_ready_product_to(john);
}
john.report_to(harry);

Mary sighs in relief and can focus on her primary responsibility.

And, imagine, one day your team and you decide to provide a personal approach for every client by listening to their preferences. Now Mary adds much more detail to every order, such as amount of chocolate in cookies, a custom title upon a cake, information about allergies, etc.

mary.read_emails_from("contact@bakery.com");
mary.talk_to_kitchen_coordinator(harry);
harry.distribute_orders_between(lucy, peter, alex, elisabeth);
harry.delegate_emails_from_bakers_to(mary);
for baker in all_bakers() {
  baker.report_to(harry);
  baker.give_ready_product_to(john);
}
john.report_to(harry);

Nothing has changed in your work! It is a sign of a well-organised working process.

In Code

The same thing happens to your code. Changing in plain data (for instance, changes in phone formats) usually passes unnoticed for your high-level code. But when you add another object providing its own responsibilities (having behaviour, in other words), it can drastically affect the entire architecture of your program, as well as Harry changed the working process in the bakery.

Chapter 4

Your new structure of the team works so perfectly that we will skip moments when you hired more bakers and continued expanding the assortment even wider. But you also decided to give your courier (John) more days off, and have found the second deliverer, Daisy. And suddenly it caused problems. Here is how Lucy’s work looked like:

class Lucy {
  method make_apple_pie(order_info) {
    // baking a great tasty apple pie
    delivery_car := assigned_deliverer.personal_car;
    delivery_car.put_into_trunk(pie);
  }
}

John asks Lucy to put the ready product directly into John’s car, and she does so (and every other baker too). But Daisy does not have a car! She prefers her fast scooter, and there is no trunk. So, it is not enough for the bakers to know that there is a person delivering orders. It becomes necessary to know who exactly is this deliverer. It means that in the future (in a gigantic worldwide bakery corporation) every baker shall know every of hundreds of deliverers working in the company!

class Lucy {
  method make_apple_pie(client_comments) {
    // baking a great tasty apple pie
    if (assigned_deliverer == john) {
      assigned_deliverer.personal_car.put_into_trunk(pie);
    } else if (assigned_deliverer == daisy) {
      assigned_deliverer.personal_scooter.put_in_bag(pie);
    } else if (assigned_deliverer == malcolm_stevenson) {
      // ...
    } else // 97 more personal treating for other deliverers
  }
}

Thanks to you, you solve this problem by creating a unified protocol for working with deliverers. “Unified protocol” sounds serious, but in fact this document only says that all orders should be given directly to the assigned person, avoiding any details of how it will be delivered.

class Lucy {
  method make_apple_pie(client_comments) {
    // baking a great tasty apple pie
    assigned_deliverer.deliver(pie);
  }
}

After this unification, bakers do not have to think about it and can focus on their primary job.

In Code

It is quite a typical situation in OOP when there are multiple objects with different implementations (and different data, of course) and the single behaviour in a program: a mock class for testing purposes, separate implementations for each supported platform, classes for customisation of UI, etc. As a result, one piece of code may use an object and have no idea that this object each time is of a different class. Usually, it is achieved by interfaces or protocols.

If such the piece tries to work with the object’s fields directly, it leads to bloated and hard-to-maintain code because every implementation is likely to have its own unique set of fields, and requires a separate algorithm to work with them correctly. The more possible implementations, the bigger clients using them.

On the contrary, focusing on the object’s behaviour keeps the client small because the set of methods is the same. If tomorrow you add one more implementation for that service, it will have the same set of methods as the other implementations do, so all clients will be able to use the newly added class without any modification in their code.

This is the fourth reason why I believe behaviour is the one what defines every object.

Final

This weird little story is not about how the real business works; the main idea is that running your business starts from the smallest (you do all handwork) and leads you to become a manager instead of a worker (you manage people doing handwork), and it looks pretty similar to what happens in code. In the beginning, the entry point of your program is done pretty procedural way: everything is put in one place, manipulating raw data, and gradually transforms into a place of coordinating objects doing their job.

To summarise:

  • A good, useful object always offers you behaviour.
  • In a big program, the end result is determined by how objects interact with each other, not what data they use while interacting.
  • It is a rare situation when different classes have the same fields, but it is very common when different classes have the same responsibilities.

How I apply this idea

Start with behaviour

Think about behaviour at first. Question yourself what thing every object does. If you are having trouble determining what an object will do for the rest of code, there is a chance that the project does not need this object at all. It may not break or worsen anything, but it will not make your program more maintainable either.

Beware of fields

When you find yourself setting or reading the object’s fields, think: does that object fully handle the job it promises to do? Why do its clients need to do something more than invoking one of the object’s methods? Do they really not know anything about the details of how the work is done? There are exceptions, of course, but it can lead to problems with maintainability in the future.

Avoid type checking at run-time

Almost all places, where you try to identify the particular type of an object, are candidates for refactoring. If the object’s behaviour (its public interface) is not enough for its clients, it usually means the clients work with every implementation separately, therefore they are smarter than they should be, and therefore they are harder to maintain.