Programming practice has gone through several evolutions in its lifespan. The first phase might be considered the “exploratory” phase, where there were no rules but a lot of imagination. People wrote code that was simultaneously amazing and terrible—amazing at what people got their slow computers to do, but terrible in that no one but the author would ever be able to maintain the programs.
The lessons learned from the exploratory phase led to what is known as “structured” programming. The goal of structured programming was to be able to write programs that someone else had a chance of reading and understanding. Structured programming favored having really well-documented inputs and outputs to every function, very clear entry and exit points to each function, and minimization of the number of global variables that the programmer had to keep track of.
Distinct Patterns Emerging
As people programmed more, we recognized that there was a distinct pattern that emerged with almost every program. Programs always had structured data types—records that had a lot of related data on them. Additionally, each type of record had functions that were very specific to that type of record. Therefore, if you were a bank, you would have a record type for your accounts, which would have functions to deposit, withdraw, etc. The record would be called the Account record, and the functions would have names like account_withdraw, account_deposit, account_create_statement, etc. Eventually, programmers recognized that the data and the operations on the data were so closely tied that they essentially belonged together as a unit. Additionally, rather than having to name each function with the name of the type it was operating on, it was both easier and clearer to attach the functions directly to the data type itself. So, if your Account was in the variable my_account, instead of writing account_withdraw(my_account, 150), you would just write my_account.withdraw(150). When functions are attached to record types, the record types are called classes and the individual records are called objects. The functions that are attached to the record types are called methods. This combining of data and functionality is known as encapsulation.
Further, it was recognized that, from a project management point of view, that programmers shouldn’t try to mess with the internals of what the data in the objects looked like. The internal representation of a class often changed, and trying to communicate those changes to the entire team or organization spent a lot of unnecessary time. Instead, the individual or team managing the class would define which functions could be used by other members of the team, and which functions were internal to the class itself. This allowed the development of stable interfaces which allowed programmers to only worry about what they wanted to do and not how it worked internally. In other words, if I wanted to work with accounts, I didn’t have to know all the internals about how they were implemented, I only needed to know the public interface that the programmer in charge of the class defined. Having that defined interface left both of us free to do our jobs. This is known as abstraction, as it forces programmers to think in more higher-level ways about how it is working with the system.
Interface and Polymorphism
However, one thing that became quickly obvious in object-oriented programming was that many different record types essentially “did the same thing” to some degree. For instance, let’s say that I have a class to read a pressure gauge over USB, and another class to read a pressure gauge over Bluetooth. These gauges might be from completely different manufacturers with completely different protocols and implementations. But, from the perspective of a programmer who needs to read a pressure, those are things they don’t care about. The programmer just cares that there is some function that retrieves the pressure for them. How it is done is relatively unimportant. Historically, one would have to be given a code for what type of device it was, and then, everywhere in the code where you wanted to get a pressure, you would have to check the type code, and then use that to figure out whether to ask for the data view the USB function or the Bluetooth function. However, to simplify things, object-oriented programming developed the concept of an interface. Interfaces are predefined suites of functions that can be implemented by any class whatsoever. Most parts of the program in our example just want a pressure, they don’t care about the details of how it is done. Therefore, one can define a PressureAccess interface which defines the functions necessary for reading pressures (probably something like getPressureInPSI()). Then, any class can implement this interface, and then be passed to a function that just cares that it is getting something of type PressureAccess. The programming language’s type system will make sure that the proper version of getPressureInPSI is called for the right object class. This ability for the type of system to call the right function based on the underlying class is known as polymorphism.
This concept of interfaces allowed programmers quite a bit of freedom to structure their programs in a way to be flexible and evolve easily over time. By thinking through how both present and future features interact with the system, a programmer can define a set of interfaces that makes it straightforward to add new features in a way that works well with the existing system, and does not require new features to cause heavy rewrites of the rest of the system.
Another thing that programmers noticed was that many classes were highly similar to other classes, but just had one or two extra features or small changes. This led to the development of inheritance. In inheritance, you can define a class to be similar to another class, but with small modifications. Therefore, rather than having to reinvent the wheel when developing very similar classes, you can just inherit the main functionality from a previously-defined class and make the small changes that you need. This is known as inheritance.
Most object-oriented programming languages implement each of these concepts in some way, though each language has its own features and drawbacks. Nonetheless, the overall paradigm of object-oriented programming matches what good programmers intuitively do. By making these things features of the language itself, the programming language can both simplify these practices and make them more powerful, both because they can use the power of the language’s type system and because having a unified system used by all programmers of the language makes code written by different programmers work together better.
Object-Oriented Programming is Here to Stay
Every once in a while, I come upon someone arguing that object-oriented programming is going out of style, or that it was always just a fad. However, every time I reflect on the basic ideas of object-oriented programming, I realize just how great a foundation they are for almost every programming project. Object-oriented programming makes programmers think in more systematic ways about their code and how it will be used both in the present and the future. It forces them to be clear about which functions are tied to which pieces of data. It forces programmers to ask themselves foundational questions about which parts of their code are for public consumption and which parts are for their own private use. It forces them to think about how other similar classes might be used in the code both now and in the future. It forces them to think about the relationships between the different parts of their code and how they might be integrated more cleanly. In short, object-oriented programming helps programmers write better, clearer, more maintainable code. And that’s why these principles are not going anywhere anytime soon.