Are you misusing Object Oriented Programming? Here’s how to fix it.

Marko Lazić
InterVenture Tech
Published in
9 min readJul 21, 2017

--

Object oriented programming (OOP) has been around for a long time, but it seems that there still are problems in understanding what it is, what one gets out of it and how to know if you’re reaping its fruits.

Object oriented programming is a paradigm that approaches the problem by describing it with objects, their states and operations that change these states. Since this may sound too abstract it is easy to disregard this as a theoretical thing that is hardly applicable in practice. That is exactly what happened in its beginnings and still is happening with every generation of programmers. On the other hand, many think that they’re practicing it because they’re using a language that has classes and objects.

This text aims to point out common pitfalls when practicing OOP. It will also emphasize the principles that could prevent these from happening and in the same time — let you enjoy many of its benefits.

What is OOP? Modeling.

During my university studies in computer science and engineering there was an important assumption made by professors that everyone is capable of the mental process of modeling. This refers to defining imaginary objects that are simplified versions of the real ones. These simplified objects would only have only the pieces of data and ways of changing important for solving the problem.

So what can be an object in OOP? It can be a model of the real-world object. If the software we’re building is trying to solve organizational problems in schools, then we might recognize objects such as Student or Professor.

Also, an object can be a representation of a process such as the process of writing a thesis. This does not exist as a real-world object but we can think of its state, data that describe it and operations that change it. So we could have enrolling, submitting a first version, evaluation and presentation and accepting or rejecting it.

Nothing Like an Example

Let’s try delving deeper with an example. This example would use a domain of geometry. While the example may seem simple, same problems can happen in any real software — more easily and they would be harder to detect. We will be building a core of an imaginary application and all the time analyze how other parts of the software would use our code.

Let’s say we have an intention to build a 3D design software and we got to work on its core — working with vectors. Vectors are those arrows from introductory geometry courses. They are described with their coordinates an (x,y) pair. For an example — let’s take a vector (3,5). This means that it starts in (0,0) and ends at (3,5).

2-dimensional vector

We have a first feature that our core vector engine needs to support — vector inversion. This should produce an arrow pointing in the opposite direction. With the previous exemplary vector the result should be (-3,-5).

Here’s an example of how some other code would use the one we are about to create. One should make a vector, initialize its data and use a vector manager to invert the original one.

private static void VectorInverting() {
Vector v1 = new Vector();
v1.setX(3);
v1.setY(5);
System.out.println(
"v1: (" + v1.getX() + ", " + v1.getY() + ")");

VectorManager vectorManager = new VectorManager();
vectorManager.invert(v1);
System.out.println(
"v1: (" + v1.getX() + ", " + v1.getY() + ")");
}

VectorManager class is responsible for doing the inversion and Vector class encapsulates the coordinates.

class VectorManager {

void invert(Vector v) {
v.setX(-v.getX());
v.setY(-v.getY());
}
}
class Vector {

private int x;

public int getX() {
return x;
}

public void setX(int x) {
this.x = x;
}

private int y;

public int getY() {
return y;
}

public void setY(int y) {
this.y = y;
}
}

Let’s analyze what we have here. We satisfied the requirements: we have a way to store the coordinates (Vector) and we can calculate the inverted vector (VectorManager).

So this is good then. Right? Well, not so much. We do have problems with our code, and these problems would really escalate if this code is going to be used a lot. And it is, since this is a core.

So what are the problems?

We’re breaking a lot of OOP good practices and principles. But let’s talk about the problems first. It is important to recognize the problems and then see how the principles prevent them. It is probably the hardest thing with old books on patterns — it’s not clear what they solve. So here goes:

  1. When a client code wants to create a Vector it is forced to set the coordinates one by one. This can produce errors like missing one of them. Also, until these 3 lines finish the object is not ready to be used. This can be fixed with the introduction of the constructor that needs to be used to create an object.
  2. When a client code wants to invert the Vector that it created, it needs to know that this functionality is offered through VectorManager, another class specifically for Vector transformations. It would be much more sensible to search the functionality in the Vector itself, offering it as an operation.
  3. In the VectorManager class, in the implementation of the inversion, we access the fields of the Vector class and change them. First problem with this is the same as with Vector creation — these to lines need to be executed together, so more possibility for bugs. The second one is that we need access to these properties only because VectorManager has the implementation of the inversion. These two would also be solved if we implemented the inversion in the Vector class itself.
  4. When we look at the Vector class, it looks as if it is only a container for the coordinates. If that is the intended way of using it, we might do better if we didn’t have it at all — perhaps use one of the generic containers like Tuple in .NET. This could be improved if we add more responsibility to Vector class.
  5. When a client code is inverting the Vector it is not clear if the result of the inversion is going to be put in the same instance or we get another one. Here we have a problem of what the operation name and the signature do not communicate about its usage. One needs to see the signature to understand that, since it returns void it must be changing the same instance. This could be improved by thinking about the public methods as means of communication — an API of the class.

So little code, yet quite a few possible problems when scaling the usage.

No better way to learn than fixing the bad code

Let’s try to fix the things we found previously based on the conclusions we had for each of the problems.

Creation

We could start with fixing the creation of a Vector. The code snippet bellow shows the modified version of the client code.

private static void VectorInverting() {
Vector v1 = new Vector(3, 5);
System.out.println(
"v1: (" + v1.getX() + ", " + v1.getY() + ")");

.....
}

And here’s the change in Vector class to support this:

class Vector {

public Vector(int x, int y) {
this.x = x;
this.y = y;
}

.....
}

So now we moved the responsibility of initialization to the Vector class. Signature of the constructor should communicate the meaning of the parameters in terms of what will they be used for.

Strengthening Vector and abandoning VectorManager

From the second problem we concluded that we should move the operation of inversion into the Vector class. OK, so here goes. The client code embracing the change:

private static void VectorInverting() {
Vector v1 = new Vector(3, 5);
System.out.println(
"v1: (" + v1.getX() + ", " + v1.getY() + ")");

v1.invert();
System.out.println(
"v1: (" + v1.getX() + ", " + v1.getY() + ")");
}

And the Vector class with the new responsibility:

class Vector {    .....

public void invert() {
x=-x;
y=-y;
}
}

And voilà, no more need for VectorManager class. One who uses Vector class is immediately capable of inverting one. Invert operation is now accessing Vector’s own coordinate properties so there is no need for passing in an instance of the Vector and it’s accessing fields directly instead of using getters and setters.

Name of the method and the fact it is defined in the Vector class is in accordance with the facts that are clear only when you inspect the signature — it has no parameters and returns void. This means that now it clearly communicates that this call changes the instance it was called on.

No need for setters

In the analysis of the third problem we realized it’s bad that some other class changes Vector’s internal state. We solved it mostly by moving the operation of inversion into the Vector class but we missed one thing — we don’t need public setters anymore. We accomplished that only Vector is either setting or changing its state, so we can fix this one too. Just remove the setters.

class Vector {

public Vector(int x, int y) {
this.x = x;
this.y = y;
}

private int x;

public int getX() {
return x;
}

private int y;

public int getY() {
return y;
}

public void invert() {
x=-x;
y=-y;
}
}

Although it may seem like a small thing it actually forbids misuse of the Vector class. We are tightening the public API of the Vector class and it is clear how it should be used, even without the documentation.

With the changes we made all of the problems we found are solved and domain is scale-proof. What we actually did in the few steps of refactoring the old code is:

  • Solving the anemic (weak) domain problem by enabling the Vector to control its own creation and change
  • Removing the artificial object of VectorManager which does not correspond to any concept from the original domain
  • Resolving the uncertainty in creation of a Vector
  • Respecting encapsulation principle by allowing only Vector to change itself
  • Resolving the unclear inversion operation by using the principles of clean code and treating public methods as an API for the client code

The final version of client code:

private static void VectorInverting() {
Vector v1 = new Vector(3, 5);
System.out.println(
"v1: (" + v1.getX() + ", " + v1.getY() + ")");

v1.invert();
System.out.println(
"v1: (" + v1.getX() + ", " + v1.getY() + ")");
}

The final version of Vector class code:

class Vector {

public Vector(int x, int y) {
this.x = x;
this.y = y;
}

private int x;

public int getX() {
return x;
}

private int y;

public int getY() {
return y;
}

public void invert() {
x=-x;
y=-y;
}
}

Too much at once? Let’s summarize!

As it emerges from this example, the most significant step could be to properly identify objects with their state and the operations that change them which is the main part of OOP.

One of the usual errors is to treat objects only as sets of data. If you start off like this, it yields weak domain object on one side, and artificial ones on the other side, like the manager object. It has no state of its own and only works with other objects. Linked to this is a problem of responsibility — who is responsible for what.

Does this ask for more effort? Not at all. Code produced is often smaller and easier to use and maintain and it requires less documentation. It is also more scalable and testable. It just requires a slight shift in perspective.

We also need to be aware that OOP is not a tool for everything. For example, while the domain of an application can be written in this way and it is arguably the best one, service or persistence layer, while still benefiting from some aspects, usually do not follow closely the principles.

All-in-all, OOP is one of the most powerful tools in a programmer’s toolkit. It must not be disregarded as it strongly helps programmers to write better, more stable and more maintainable software, which is what we all want.

--

--

Marko Lazić
InterVenture Tech

I have been curious about how things work all my life. Today, I use the same attitude in software development to make things good and then make them better.