Object orientation

In object-oriented programming, the key idea is to split code into several small, manageable parts or objects. Each object has its own identity, data (or attributes), and logic (or behavior). For example, consider modeling an elephant in software.

Attributes are the properties of the object. For example, in the case of the elephant, things such as these:

  • Weight
  • Color
  • Type
  • Location

The collection of all these attributes describes the current state of an object. The state of one object is generally independent of another. Behavior is things that the object can do; in the case of an elephant, it can trumpet. Behavior is the object's interface with the outside world. The individual constructs (or functions) by which you can invoke behavior on the object are called methods.

A class is a blueprint, or a template for objects that share the same behavior and properties. Being a template, it can be used as a specification to create objects. It is usually correct to say that objects instantiated from a class are of the same type. So, our Elephant class can be something such as the following:

Encapsulation is the key guiding principle for class design. It implies exposing a contract for the behavior of objects and hiding volatile implementation details. The private attributes and methods are hidden inside a capsule according to a need-to-know basis.

Instead of having a long procedural program, the design paradigm is to decompose behavior into small manageable (and ideally reusable) components (objects), each with a well-defined contract (interface). Doing this effectively allows me, as the class developer, to change implementation while not affecting my clients. Also, we can ensure safer behavior in the system, as we don't need to worry about the clients misusing implementation constructs, thereby reducing complexity of the overall system.

Many times, we come across a set of objects/classes that are very similar, and it helps to reason about these classes as a group. For example, let's say we are designing a zoo, where there are multiple animals. There is some behavior that we expect from all animals, and it simplifies code immensely if we use the abstract interface of the animal, rather than worry about specific animals. This type of relationship is normally modeled as inheritance, as shown in the following diagram:

Here, we have an animal interface and multiple animal classes implementing the contract defined by the interface. The child classes can have extra attributes or methods, but they cannot omit methods specified by the parent. This inheritance modeling implies an is a relationship (for example, a tiger is an animal). Now a feature such as zoo roll call, where we want to find names of all the animals in the zoo, can be built without worrying about individual animals, and will work even as new animals enter the zoo or some types of animals move out.

You might notice that each animal has a unique sound—tigers roar, elephants trumpet, and so on and so forth. During the roll call, however, we don't care what the sound is as long as we can prompt the animal to speak. The how of speaking can be different for each animal and is not relevant to the feature in question. We can implement this using a Speak method on the Animal interface, and depending on the Animal, what Speak does will be different. This ability of an interface method to behave differently based on the actual object is called polymorphism and is key to many design patterns:

Inheritance, though useful, has its pitfalls. It often leads to a hierarchy of classes, and sometimes the behavior of the final object is spread across the hierarchy. In an inheritance hierarchy, super classes can often be fragile, because one little change to a superclass can ripple out and affect many other places in the application's code. One of the better things that can happen is a compile time error (compiled languages), but the really tricky situations are those where there are no compile time errors, but, subtle behavior changes leading to errors/bugs in fringe scenarios. Such things can be really hard to debug; after all, nothing changed in your code! There is no easy way to catch this in processes such as code review, since, by design, the base class (and the developers who maintain that) don't care (or know) about the derived classes.

An alternative to inheritance is to delegate behavior, also called composition. Instead of an is a, this is a has a relationship. It refers to combining simple types to make more complex ones. The preceding Animal relationship is modeled as the following:

Here, instead of a hierarchy, there are only two constructs:

  • Classes implement an interface—which is the contract the base class offers.
  • Functionality reuse happens through having references to objects, rather than deriving from classes.

This is why many people, including people who code in Go, have the Composition Over Inheritance principle.

Before closing this topic, there is another key advantage of composition that needs to be called out. Building objects and references through compositions allows you to delay the creation of objects until and unless they are needed, thereby improving the memory footprint of the program. Objects can also mutate the state of the referenced objects dynamically, allowing you to express complex behavior through simple constructs. One example is the state design pattern as detailed in Chapter 4, Scaling Applications. When efficiency and dynamism is a requirement, composition is super-key!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset