Object Oriented Paradigm
Object orientation is a way of programming that bears a remarkable resemblence to the way electric devices have evolved. This page will explain the problems of the past, and how these were solved by components (in electric devices) and objects in programs.
- 1 In the beginning...
- 2 Organization
- 3 Back to object orientation
- 4 Back to Organization
- 5 Object Oriented Principles
In the beginning...
The first electric devices were one web of components. If you looked into the interior of an ancient radio, everything was connected. Only a few things could be disconnected: the power plug and maybe external speakers. Sometimes these devices came with an electrical scheme, which had everything in a detailed manner. You could look for hours at such a scheme and discover all sorts of functions in that scheme, like radio reception, amplification, tone control, etc.
The first computer programs had the same structure. A program written in assembler or in Basic could easily span hundreds and thousands of code lines. It had to, because all the details had to be provided in one code file.
Off course, a developer was really proud those days if he managed to make a working program with a decent set of features, but maintenance was hard. And if you wanted another program, you had to write it from scratch. Reusing code from previous programs was not really impossible (you could copy some subroutines), but it was not exactly easy.
So, both in electric devices and in programs, the huge structures had to be organized. If you open a present-day radio, the interior radically differs from the first generation devices. What you will find nowadays are components. These components often look like small boards with one basic function. So you may have a pre-amplifier component (or two in a stereo device, or even more in a surround sound system), tone control components, electrical power source components, etc. These components can be easily replaced, because the are connected with plugs.
Often, a factory does not make one complicated product for a number of years, but a whole range of products is made, all made up of the same or similar components. In fact, the factories often does not make all the components itself, but they are made by other factories. All because these components have one defined function and adhere to standards. Personal computers even go a step further: they are built in a way that enables the owner to upgrade it at any time.
For the components to be interchangable, a few things are necessary. All electrical communication with the component has to go through a plug. Such a plug defines a broad function, such as "sound out", leaving the possibility to connect headphones, speakers, recording systems, or any other "sound processor" regardless of its internals. In fact, the internals are only important to the component itself and should not bother the designer who builds a device with these components.
Back to object orientation
Computer programs have evolved the same way as the electric devices. The reusable components are called objects, and the plug through which all the communication should go is called an interface.
Interfaces and plugs
The word "interface" can have several meanings. Many computer languages allow you to define an interface. Such a definition is then a minimal interface, or the bare minimum that should be supported for anything that connects to it. Any object can have more than one such interface, just like any electrical component can have more than one plug. Lots of plugs are well-known and documented (sound plugs, USB plugs, wall socket power plugs and so on) and likewise interfaces are often well documented, especially if they come with external libraries. One of the nice things in programming is that an interface is defined by named functions and the parameters you can pass them. Just giving the functions a descriptive name can already explain the possibilities of an interface, even if it does not come with any further documentation.
The word "interface" is sometimes also used for the entire exposed set of functions, or the complete set of plugs.
Performing one elementary function also brings the notion of responsibility to mind. A wall socket, for example is responsible for delivering a certain voltage. If the voltage is extremely high, you might damage anything that you connect to it. But the type of socket shows what the power should be. A 220 Volt socket looks entirely different from a 380 Volt socket. Some devices check the voltage, for example with a fuse, but they should not have to.
Likewise, your object can be thought to have a contract with the outside world: if you provide it with valid input, it will perform its function properly and provide the right output. Some programming languages allow you to specify what is required by a specific function and what is ensured by it if the requirements are met. The theory of these guarantees is called Design by Contract.
This contract is especially useful in object-oriented languages, where any complex structure is handled by an object. For example, when you pass a path to a file, you can state the requirement that the file must exist. Or when a database connection is passed, a method can require that the connection is open and that a transaction is started. Likewise, a method can ensure that a file exists (after is has written data to it) or that a database transaction is closed. This contract therefore helps to separate the responsibilities between calling objects and called objects.
But an object can still run into problems it cannot solve itself (like a network connection that has suddenly disconnected, for example). In that case, an object can escalate the problem to the surrounding application. This is usually done with exceptions, which will be explained later.
Because a object performs one function, it can also be tested separately. You can build a little test bench that feeds all kinds of valid and invalid input to the object and tests if it produces the right output and if it escalates when it should. These test benches are called unit tests.
Chains of Responsibility
If any object can be trusted to do its job well, such an object can also trust other objects to do finer-grained tasks well. This means that any object that has a responsibility can delegate some of this responsibility to another object. For example, a SettingsStorage object may write settings to a file, but delegate all the file handling to a TextFile object. Or, in another example, a Backend object may instruct a Database object to execute a query and send the results to the ObjectRelationalMapper object so it can build a nice object structure. This can make the code quite clear, because the ObjectRelationalMapper object does not have to "dig" for its data itself.
Objects and classes
In an electric device, all components usually have to be there when you switch the device on. But computer programs generate the objects as they are running. To be able to do so, lots of programming languages allow you to define the type of object you create and what it can do. This "design" of an object is called a class. So, simply put: a class is the blueprint of an object. A programmer writes a class and a program uses it to construct objects.
If you can precisely define the responsibility of an object, you can also test it. Unit tests are special objects that test the responsibility of other objects. Unit tests usually are not active during normal operation of a program, but can be activated from a development environment or by using some kind of "selftest" option.
It is extremely hard to test all the details of a class by using a finished application. Some test are hard to organize (a dropped connection, mismatched permissions, syntax errors in queries, etc), but you would want to know that the internals of your program work reliably with al these conditions. This is where the unit tests come in. The unit tests test that the examined object can perform its task, but also tests that it escalates when it cannot. Unit tests allow the programmer to test the most basic, low-level objects in the system. But unit tests also allow the programmer to have faith in those objects. Off course, there is no such thing as perfect code, so there is always the chance that you have missed a test. Unit tests therfore evolve along with the code under test.
One area where unit tests are extremely important is when a programmer must repair some code. If this code is covered by unit tests, the programmer simply runs the tests to see if the repair went well and had no unintended side effects (or "bugs").
Exceptions are usually objects as well, but they are used to communicate problems. If an object cannot fulfill its responsibility, it escalates by constructing a new object with the problem description and "throwing" it at the surrounding code. Surrounding code can "catch" these exceptions and decide what to do with it.
Polymorphism gives you the opportunity to define your objects in different terms of abstraction. For example, you can define a WebWidget class that can output a piece of a web page and process some submitted input. From this fairly abstract class you can derive a TextBoxWebWidget that specifically draws a textbox on a web page and retrieves a piece of text. The more abstract classes are called the superclasses and the more specific are called the subclasses: In this case the WebWidget class is the superclass of the TextBoxWebWidget, and the TextBoxWebWidget class is a subclass of the WebWidget class. It is also said that the TextBoxWebWidget class inherits from the WebWidget class.
There is one special consideration to this polymorphism: you can define variables as either the more abstract form or either the more specific form. If you define your variable or parameter as the more abstract form, you can always pass a more specific form. So you can pass all sorts of widgets to the code that generates the complete web page and each widget performs the actions that are necessary for that specific widget, without bothering the web page building code with all the differences.
A superclass can be have many subclasses, but in most object oriented languages a subclass can be derived from only one superclass. Note that there is nothing in object oriented theory that forbids this "multiple inheritance", it is only there because language designers found it more convenient for either themselves or for the language users.
Back to Organization
When you read about responsibility and inheritance, you might have the feeling that object-oriented code is organized like people. It is. Better said, code is organized like a company or other structured organization.
Inheritance and Specialization
Lots of organizations have general functions and specialized functions. For example, any policeman could make an arrest, but there are special policemen for solving murder cases, for solving drug cases, for stopping drunk people in large crowds to become a problem or for making sure nobody drowns at the beach. Similar structures exist for doctors and other professions. In case of policemen and doctors, these people are also easily recognized as such. So you may compare an interface or class to a kind of uniform: each uniform communicates what the wearer can do, and with the uniform comes responsibility.
Responsibility, Skill and Escalation
When someone in a company calls the warehouse with the request "please send a new notebook", the warehouse employee is trusted to have enough skills to do that. That does not mean that the warehouse employee has to complete the task himself. He may assign it to a colleague or fetch the notebook himself but send somebody else to bring it to the requester. That does not matter: the job is done and the requester does not have to know how. Only in case of problems, the situation is escalated (for example, when the notebooks are out of stock), probably with the option of getting one from outside the company or bringing one later. In any case, no one expects a warehouse employee to start cutting trees to make paper to make a notebook from. That is certainly not his responsibility. Likewise, an object that displays a value from a database is usually not responsible for opening connections, building queries, and so on. It outsources these tasks to other objects, or other objects outsource the task of displaying to our object.
Object Oriented Principles
In object oriented theory, a few principles have arisen to try to make maintainance possible without introducing errors. In practise, following the principles to perfection is really hard, but they help in avoiding new bugs in existing software.
The Open Closed Principle
"Software should be open to extension but closed to modification."
If you can only extend software, you cannot touch existing parts and the only place where you are able to introduce bugs is in the extensions. In practise this is done with Polymorphism: you can write a new class that extends an existing class, uses all the existing functions where possible and overrides only the new functions.
Not really a "principle", but more of a practice. Refactoring sounds as something completely useless: refactoring is changing software without changing its behaviour. In other words, after refactoring a program you have a different program that does exactly the same as the original program. This may sound completely useless, but it is not.
If you apply the Open Closed Principle, you may rest assured that you introduced no new bugs in the existing software, but you still added something to it. Lots of these added repairs may look ugly and be a burden in maintenance. So at some point you will have to reorganize the structure to make it logical and sound again. Or the other way around: you may not even be able to add a repair to the software without opening up the structure for such a repair.
But how do you know that the behaviour has been preserved? This is where the unit tests come in. The unit tests check the behaviour of the code, so any change in behaviour should be visible in the unit tests.
Note that the Open Closed Principle and the Refactoring practise complement each other. By first leaving the structure and the existing code intact, but only adding functionality, and then leaving functionality intact but only modifying structure, you have the best chance of preventing bugs in the code you write.