What is a class and why would I need it?

Introduction

Occasionally I get asked this question by the people who are not a professional programmers and who are using a programming language as a tool to solve a certain problem without caring too much about what is going on behind the scenes. The programs they are writing are relatively small and therefore they are often wondering why would anyone need to use a class when they could program just fine without it.

When I was first learning about the object-oriented programming, I didn’t get it either. The explanations were often using examples from the animal kingdom: if a dog is a class that inherits from an animal class and a cat is a class that inherits from an animal class, then calling their speak() method will make the dog say “woof” while the cat will go “meow”.

Right, but why would I need to create an elaborate hierarchy of classes when a simple if statement will do just fine? The articles didn’t have a good answer to this question, and I thought I was missing on some important insight. Are the “real” programmers really adding all this class ceremony in their software? Am I programming wrong? What is going on here?

What those articles failed to mention was that object-oriented programming is just another tool in your toolbox and you are no lesser of a programmer if you stick to simple functions full of for loops and if statements like the elders used to do.

Hopefully, this article will provide a slightly better examples of what are the actual problems that we face in our programs and why sometimes a certain programming language features come in handy.

Note:

This post is aimed at beginners, hobbyists and other code dabblers who are wondering are they missing in their software by not using classes. If you are a seasoned programmer, you can move along as you won’t learn anything new here.

What is a class and why do I need it?

A class is a concept that is sometimes useful for organizing your code in a large program. I specifically wrote “sometimes,” because you don’t have to use a class if you don’t want to. In fact, the C programming language has no concept of a class, yet the entire Linux kernel is written in it.

But, let’s stop with the programming philosophy and take a look at the concrete example:

Imagine you are trying to write a small program for calculating the distance between the two points P1 and P2 in the Cartesian coordinate system. How would you write it?

Procedural way

This is the oldest approach that is still being used in C programs. The functions that you need in your program are declared at the top of the file, while the main logic that wires the functions together lives at the bottom. In a large program, however, these functions will be split into several files.

// A perfectly valid C++ program, with no class in sight 
#include <math.h>
#include <stdio.h>

float calculate_distance(float x1, float y1, float x2, float y2) {
    // we use Pythagorean theorem
    float distance = sqrt(pow(x1 - x2, 2) + pow(y1 - y2, 2));
    return distance;
}

int main() {
    // point 1
    float x1 = 1.0;
    float y1 = 2.0;

    // point 2
    float x2 = 3.0;
    float y2 = 4.0;

    float distance = calculate_distance(x1, y1, x2, y2);
    printf("Distance = %.2f\n", distance);
    // distance = 2.83
    return 0;
}

If you would like to add functionality to your program you can simply declare more functions and use them within your main block. For a small program, organizing your code this way works quite well. Once your program starts growing, you may occasionally find yourself passing the wrong arguments into your functions and consequently getting the wrong result. For example:

// a programmer made a mistake and swapped x2 and y1 arguments
float distance = calculate_distance(x1, x2, y1, y2);
// distance = 1.41 instead of 2.83

The compiler can only check if the arguments that you passed to the function are of the right type, but the compiler doesn’t know whether you passed those arguments in the right order. This problem could be avoided by adding some structure to our little program:

#include <math.h>
#include <stdio.h>

struct Point {
    float x = 0;
    float y = 0;
};

float calculate_distance(Point p1, Point p2) {
    float distance = sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2));
    return distance;
}

int main() {
    // p1, p2 are usually called Point instances
    Point p1 = {1.0, 2.0};
    Point p2 = {3.0, 4.0};
    float distance = calculate_distance(p1, p2);
    printf("Distance = %.2f\n", distance);
    // distance = 2.83
    return 0;
}

Now, regardless of how we pass our points we will get back the right result. Adding a bit of structure to our program slightly reduced the number of potential bugs that can happen in our software.

So, struct is a class and this class holds my data?

Well, sort of 1. Classes are mostly used for making your own abstractions, like the point example described above, but they aren’t only containers for holding your data. A class can also contain code in the form of procedures (methods) that can operate on the class’s data (member fields/variables of your class instance).

Object-oriented way

We are still trying to calculate the distance between the two points, but this time we’ll move the calculation code into our point class. Our new method now has the access to the class instance fields, thus we can get rid of the first point parameter:

#include <math.h>
#include <stdio.h>

struct Point {
    // member fields (variables defined within a class)
    float x = 0;
    float y = 0;
    
    // member functions (functions defined within a class)
    //
    // Note: we removed the p1, since the class method has the
    // access to the class fields (in this case x and y floats) 
    float distance_to(Point p2) {
        float distance = sqrt(pow(x - p2.x, 2) + pow(y - p2.y, 2));
        return distance;
    }
};

int main() {
    // create point "instances"
    Point point1 = {1.0, 2.0};
    Point point2 = {3.0, 4.0};

    float distance = point1.distance_to(point2);
    printf("Distance = %.2f\n", distance);
    // distance = 2.83
    return 0;
}

What did we gain by doing that? Apart from not having to pass two arguments to the distance calculation method, we didn’t gain much. In a large program, however, there is a small improvement; you avoid polluting the namespace with thousands of functions that have similar names.

Namespace pollution

In a large program, that was developed over the years by many programmers, you may encounter multiple functions that are all called calculate_distance. These functions may not all be related to calculating the distance between the two points in the 2D coordinate system. Some may be used for calculating the distance between the two points that are located on the arc, others for calculating the distance between different towns that are connected with a road and so on.

If defined functions accept different arguments (e.g: one function accepts 2 floats, while the other one accepts 2 point structs), you won’t encounter any problems because the compiler will use the function that matches with your passed arguments. This functionality is called function overload, but not all programming languages support it (C++ and Java supports function overload, while Go and Python do not).

Given sufficiently large program written by sufficiently large number of programmers 2, these functions will eventually clash; they will have the same name and accept the same arguments, despite calculating completely different things. The simplest way to avoid this problem is to add a prefix to the function name:

float point_calculate_distance(int x1, int y1, int x2, int y2); 

While this makes you safe from the naming clashes, you will also have to type the lengthy prefixes every time you want to use the function. If you would like to see an example from a real codebase, you can check out the GDK API reference.

Some programmers are annoyed by the long function names and will try to abbreviate the common prefixes and turn the point_calculate_distance into pt_calc_dist. I generally don’t like to use abbreviations in my code, because I have a hard time remembering what they mean. While the abbreviation approach may solve the problem of long function names, it introduces another problem right away. Sometimes, the function prefixes will only differ in one character and you may end up using the wrong function.

// made up example (not coming from the GDK, GTK codebase)
float gdk_calculate_distance(float x1, float y1, float x2, float y2); 
float gtk_calculate_distance(float x1, float y1, float x2, float y2); 

Isn’t this problem solved based on the includes in your file (also called imports in other programming languages)? Sure, but only if you are careful with what you include. If you are programming with a modern IDE, the autocomplete may suggest you hundreds of similarly named functions, which makes it really easy to miss the character and select the wrong function from the list 3.

One approach to this problem, is to put the methods that are operating on the same data into a class. This may reduce the clutter of the procedural approach, but you don’t have to take my word for it. Try it out and see for yourself if that is really true.

It’s worth pointing out here that how you structure your code is completely up to you, since most of the generally accepted “rules” that you will see in books or articles are based on the opinions of the programming influencers. Sometimes these opinions are reasonable and sometimes they aren’t and it’s up to you to decide whether following a particular idea makes any sense in your specific context.

Encapsulation

Apart from being a namespace for your methods, a class also allows you to hide parts of its functionality and data. Sometimes you don’t want the users of your abstraction to change its internal state, since they could get your abstraction into an invalid state and potentially crash their application.

In order to demonstrate the benefits of encapsulation, we will program a simple simulator of a power supply.

Huh? Why would anyone ever have to simulate a power supply? Well, power supplies come in all shapes and sizes and while the power supply of your mobile phone is trivial, there are others that demand lots and lots of code before they could be used. For example, a power supply for a particle accelerator consists of an entire hall full of hardware together with a complicated software that controls all that stuff.

#include <stdio.h>
#include <string>

// yay, we finally see the class keyword
class PowerSupply {

// fields and methods that are accessible to anyone
public:
    std::string name = "My giant power supply";

    bool turnOn() {
        if (turnedOn) {
            // power supply is already turned on
            return false;
        }

        turnedOn = true;
        return true;
    }

    bool turnOff() {
        if (turnedOn) {
            turnedOn = false;
            return true;
        }

        // power supply is already turned off
        return false;
    }


 // fields and methods that are accessible only within other methods
 // that are a part of this class.
private:
    bool turnedOn = false;

    void internalHelper() {
        printf("I could be used only within the methods of this class\n");
    }
};

What did we gain by hiding the power supply state and functionality (turnedOn field and internalHelper method)?

  1. Another programmer, that is not entirely familiar with how your abstraction works, can’t mess with the internal state of the power supply and potentially break the functionality:

    int main() {
        PowerSupply powerSupply; 
        bool ok = powerSupply.turnOn(); 
        if (!ok) {
            printf("Power supply is already on, this is a programming bug\n");
            return 1;  
        }
    
        // a compiler error will be raised here. The user can 
        // only call the public API (methods and fields that 
        // appear in the public section of the class)
        powerSupply.turnedOn = false; 
    
        // more code that relies on the power supply being turned on
    }
    
  2. You can change the implementation of your class without breaking the existing code that is already using your abstraction. For example: at one point you realize that simulating a power supply is much more complicated than having just on/off flag, so you replace that state with an enum:

    #include <stdio.h>
    
    enum PowerSupplyState : int {
       PS_INVALID_STATE = 0,
       PS_ON = 1,
       PS_OFF = 2,
       PS_INCREASING_VOLTAGE = 3,
    };
    
    class PowerSupply {
       public: 
          bool turnOn() {
            if (state == PS_OFF) {
               state = PS_ON;
               return true; 
            }
    
            state = PS_INVALID_STATE;
            return false; 
          }
    
       private: 
         PowerSupplyState state = PS_OFF; 
    };
    
    int main() {
       // look my code still works without changing anything
       PowerSupply powerSupply; 
       bool ok = powerSupply.turnOn(); 
       if (!ok) {
         std::cout << "Power supply is already on, this is a programming bug" << std::endl;
         return 1;  
       }
    }
    

Is hiding the internal state and methods useful? Sometimes it is and sometimes it’s not. Encapsulation is generally considered a “good practice”, but that doesn’t mean you have to religiously encapsulate everything. In Python there are not private fields and methods and they seem to live just fine with this approach.

Should I write getters and setters?

Sometimes you will encounter a class that holds some data and a lot of methods that don’t seem to do anything except for setting and retrieving the instance fields; the famous “getters and setters” pattern:

class PowerSupply {
    public:
        std::string getName() {
            return name; 
        }
        
        void setName(std::string &n) {
            name = n; 
        }
        
        int getYearOfCreation() {
            return yearOfCreation;  
        }
        
        void setYearOfCreation(int newYear) {
            yearOfCreation = newYear;  
        }
        
        // ... long chain of getters and setters
    
    private:
        std::string name = "My power supply";
        int yearOfCreation = 2010;
        // ... more fields 
}

When asked about this approach, the programmers will be generally divided into two groups:

  1. First group will defend this approach: due to encapsulation you can add additional logic to your get and set methods without changing anything in the rest of the code that is already calling your abstraction.

  2. Second group will attack this approach: if all you ever need is a container that holds some data, then why on earth would type all these useless methods. Just make the fields public and save yourself a lot of time in the process.

I prefer to make the fields public and only write getters and setters if I actually need them, although my decision is usually based on the surrounding code. If the codebase is large and strictly following the getters and setters pattern, I will also write them just to avoid the religious discussions about this topic.

In your own little codebase, you are free to structure it however you want.

Inheritance

In the same way you may inherit certain characteristics from your parents, a class may inherit data and member functions from their parents. Inheritance allows you to reuse and extend the functionality of the existing class. But, why would you want to extend an existing class?

Imagine you are writing a GUI and you are using a GUI framework that provides you with a common building blocks - buttons, text inputs, dropdowns, tables, etc… While modern GUI frameworks have lots of widgets, you will often find yourself yearning for the functionality that doesn’t exist in the provided GUI widgets. For example:

Let’s say you are trying to create a text input with an autocomplete; when you are typing in the input box, a dropdown with the relevant suggestions should appear below the text input. Despite this being a commonly used widget, the text input provided by the GUI framework does not have the desired autocomplete functionality. What do you do now?

  1. Open a bug report, complain about the missing functionality to the maintainers and wait for an eternity until this functionality is added.

  2. Copy the framework’s source code and modify the text input. If you don’t merge your changes back to the original repository, you will also have to maintain your own version of the framework which is usually a non-trivial amount of work.

  3. Extend the existing text input’s class and add the missing functionality in your own extension.

  4. Write your own text input from scratch. Depending on what you need this could take a long time to handle all the edge cases related to text manipulation.

While any choice mentioned above is viable, the number 3 seems like the least amount of work. Since laziness is next to godliness (for programmers at least), we will go with the number 3 and override the default behavior of the TextInput class 4:

#include <vector>
#include <gui_text_input.h>

// TextInput is a made up class that will be provided via the GUI
// framework that you are using to make up your GUI
class AutocompleteTextInput : TextInput {
   public:
   // by default the text input widget provides a "hook" that is triggered
   // whenever the text of the input has changed. Obviously this 
   // is a made up example, but a lot of GUI frameworks work like that. 
   void onTextChanged(std::string &oldText, std::string &newText) {
      // reuse the existing behavior as provided by the framework
      TextInput::onTextChanged(oldText, newText); 
      
      // our extension: custom code for filtering suggestions 
      // and showing them in the UI 
      filteredSuggestions.clear(); 
      for (std::string &s : allSuggestions) {
         bool foundSuggestion = s.find(newText) != std::string::npos; 
         if (foundSuggestion) {
            filteredSuggestions.push_back(s);  
         }
      }
      
      if (filteredSuggestions.size() > 0) {
         openDropdown(filteredSuggestions);
      } else {
         closeDropdown();  
      }
   }
   
   void openDropdown(std::vector<std::string> &suggestionsToShow) {
      // build the dropdown ui with suggestions
      // left as an exercise for the reader
   }
   
   void closeDropdown() {
      // left as an exercise for the reader
   }
   
   void populateSuggestions(std::vector<std::string> &newSuggestions) {
      allSuggestions = newSuggestions;
   }
   
   private:
      std::vector<std::string> filteredSuggestions; 
      std::vector<std::string> allSuggestions; 
}

Instead of writing our own text input from scratch, we were able to reuse the existing functionality provided by the framework. The best part of our solution is that whenever you upgrade your GUI framework to a newer version, your AutocompleteTextInput class will also inherit all the bugfixes that were done in the newer version of the TextInput implementation.

And that’s mostly what you have to know about inheritance. Sometimes you might encounter deep inheritance chains (class A inherits from class B which inherits from class C, etc…) and wonder how can anyone understand what is going on in that pile of layers.

In general, we try to avoid deep inheritance chains, since:

  • They are hard to understand.
  • Your classes may inherit undesired functionality.

But, how do we avoid this problem then? You may have heard the ol’ “Favor composition over inheritance,” but what does that even mean? If you are curious, you can read more about this in Why is composition often better than inheritance post.

On structuring your code

Programmers are usually very dogmatic about structuring their code and there are nearly infinite online discussions on this topic that never reach any conclusion. Why?

Every programmer eventually develops their own taste for structuring their code. This taste, however, is a subjective thing; some people like sea-food while others can’t stand it. In software, we don’t have a generally accepted metrics for code quality and we don’t even have any measurements that would prove one approach being superior to the other.

It’s all based on opinions and as the old adage points out: opinions are like assholes; everybody has one. As such, I wouldn’t care too much about these discussions, since there is no one true way to solve your problems. Do the simplest thing that could possibly work and revise your code as needed.

Notes


  1. In C programming language, a struct can only hold your data and you can’t define a method for your struct. If you want to write your helper functions, you have to use the procedural approach, where you always pass the data that you need through the arguments of your functions.

    In C++ a struct is the same thing as a class, except all the member fields are public (whereas in class they are private by default). Additionally, C++ allows you to define methods on your structs.

    In other programming languages (Java, Python), only the “class” keyword is used for defining such structure, but the idea is the same - you want a bunch of data and methods that operate on that data close together. ↩︎

  2. Given enough monkeys and enough time they could write any text, such as the entire work of William Shakespeare. See also Infinite monkey theorem↩︎

  3. I am not advocating for writing your code with a notepad. Large programs are very complex and you should use any help that you can get to handle that complexity. ↩︎

  4. The text input does not have to be a class, but in most modern GUI frameworks the GUI components are built with classes. See also The complexity that lives in the GUI post. ↩︎