Computer programming/Object oriented programming
Preface
Objected Oriented programming has been one of the most influential developments in computer programming, gaining widespread use in the mid 1980s. Originally heralded for its facility for managing complexity in ever-growing software systems, OOP quickly developed its own set of difficulties. Fortunately, the ever evolving programming landscape gave us "interface" programming, design patterns, generic programming, and other improvements paving the way for more contemporary Multi-Paradigm programming. While some people will debate endlessly about whether or not a certain language implements "Pure" OOP—and bless or denounce a language accordingly—this book is not intended as an academic treatise on object oriented programming or its theory.
Instead, we aim for something more pragmatic: we start with basic OO theory and then delve into a handful of real-world languages to examine how they support OO programming. Since we obviously cannot teach each language, the point is to illustrate the trade-offs inherent in different approaches to OOP.
Introduction
For an overview and history of Object Oriented programming OOP, please reference the Wikipedia article.
The reader is expected to have a basic familiarity with programming in general, as we will give examples in a variety of languages. We will explain any non-obvious syntax in the discussion, although this is besides the point. The point is to give some indication of the flavor of the languages and some insight into the real-world application of OO ideas. It is not expected that the reader know anything about object oriented programming, although we suspect most readers will at least have some preconceived notions.
Overview
We will divide up OOP into two phases— classic and modern. While this distinction is somewhat arbitrary, we believe it is instructive to consider OOP as it was practiced in the 1980s and early 1990s to demonstrate the motivation for more current practices.
Classic OOP can be traced back to a language called Simula, and in particular Simula 67, which was popular during the 1960s. It was Simula that first instituted "classes" and "objects," leading to the term "object oriented" programming. By the early 1990s, enough experience had been gained with large OOP projects to discover some limitations. Languages such as Self, ideas like interface programming (also known as component or component-oriented programming), and methodologies such as generic programming were being developed in response to these difficulties. Although often derided by OOP purists, it was the standardization of C++ in 1998 —including generic programming facilities — that really ushered in the modern era of OOP. This is largely due to the popularity of C++ and the genius of the STL demonstrating the utility of the new methodology to such a large audience.
Classic OOP
By 1980, Xerox had made Smalltalk available to outsiders, appropriately named Smalltalk-80. Unlike other early programming languages, Smalltalk was a complete environment rather than just a language, a characteristic it had in common with Lisp at the time. While Lisp machines were forshadowing IDEs to come, Smalltalk was pioneering the GUI, ultimately influencing the development of the Macintosh computer. Meanwhile, while Xerox was developing Smalltalk during the 70s, the C language was becoming popular thanks to UNIX being largely written in C. Therefore, it was C —an otherwise unlikely candidate —that Bjarne Stroustrup fused with ideas from Simula to create "C with Classes" which was renamed to C++ in 1983. In 1985, unsatisfied with Smalltalk, C++, and various object systems being tacked onto Lisp dialects, Bertrand Meyer created Eiffel. Even though much more recent, Java essentially cloned vintage C++, and so we consider Smalltalk, vintage C++, Eiffel, and Java to be Classic OOP. The object systems tacked onto Lisp (eventually standardized as CLOS in 1994) produced a very different methodology. While we don't consider CLOS to be Classic OOP, it did influence modern OOP.
Modern OOP
David Ungar and Randall Smith completed their first working Self compiler in 1987, and by 1990 Sun Microsystems had picked up the project. While Self did not survive to really become a modern OOP langauge, it was a second generation OOP language. Then, in the early 1990s, Alexander Stepanov and Meng Lee pioneered generic programming and wrote early drafts of C++'s STL. This began a (still ongoing) trend of incorporating functional programming ideas into more traditional OOP environments, in a kind of reverse CLOS. Additionally, the early 1990s saw the development of CORBA and Microsoft's COM, which was a natural extension of the ideas that had lead to the original Windows API. This interface or component programming was a natural extension of encapsulation —a basic tenent of OOP, as we will see. All of these developments were aimed at further managing or reducing complexity. Since this was the original goal of OOP and these techniques are used in conjunction with classic OOP, we find it appropriate to consider them in a treatment on "OOP."
Contempory object oriented programming therefore tends to be rather distinct from classic object oriented programming. Particularly, getting used to which abstractions are the most useful for which problem types is more challenging now that there are more to choose from! In a now classic book, Gamma et. al. introduced Design Patterns which helped to synthesize a variety of OOP techniques as applied to very common problems.
Future of OOP
The future contains more standardization of functional programming techniques in OOP environments, particularly lambda expressions and closures, and more robust meta-programming constructs. Applying design patterns automatically through generic or meta programming techniques is an interesting area.
What is an "Object"?
Loosely, the term "object" is used to conjure up connections with real world objects like a chair or a guitar. Except that for software, only some simplified abstraction is used, designed specifically for the task at hand. While a real chair is composed of atoms and molecules and reacts to its environment based on the laws of physics and its atomic composition, a "chair object" will vary wildly depending on whether you're writing a game or a Point-of-Sale system for a furniture store. Approaching your problem from the perspective of the chair, besides the difficulty in thinking while relaxing in a LazyBoy, will not be as productive as approaching the chair from the perspective of your problem!
In most of the languages used in this book, you will find that technically speaking (something akin to whistling 9600 baud, I understand), an object is "an instance of a class." Great, so what does that mean? Well, we can trace this idea all the back to Plato and his Platonic Ideal. If you spent more time watching Bill and Ted's Excellent Adventure than reading Plato, the idea is that the concept of a chair is a separate entity from any particular chair. In other words, you can concieve of a chair in the abstract without thinking of any particular chair.
In most OOP languages, this abstracted idea of a chair is called a class (from classification) and is a prototype or blueprint for actually making chairs. The act of making something from the blueprint is often called "instantiating," and the made thing is both an object and an instance of the class that served as a blueprint. As humans, we normally tend to do this in reverse —we categorize objects we encounter. We can easily identify chair-like things we run into as being chairs; this classification is where we got the term "class" in the first place.
It is easy to drift off into abstruse philosophical debates over objectness; in some areas like knowledge representation and computational ontology they are very relevant. However, for a computer programmer, it is only necessary to figure out what your application needs to know about and do with chairs. This can be a sufficiently difficult problem, it's usually not necessary to make it harder!
If you're still fuzzy on the whole idea, consider a more technical explanation. Structs (structures), records, tables, and other ways of organizing related information predated objected oriented programming. You may be familiar with something like the following Pascal code:
chair = RECORD
model : integer
weight : integer
height : integer
color : COLOR
end
This doesn't actually create a chair variable, but defines what a chair variable will look like when you create one. You could proceed to create arrays of chairs and so forth, and as we hope you've discovered for yourself, this kind of thing is quite indispensable for keeping your programs understandable. Object oriented programming wants to push this advantage and milk it for every ounce of understandability, correctness, and simplicity it can.
A fundamental problem solving technique is divide and conquer —you just have to figure out how to partition your problem into sub-problems. OOP innovators realized that we already had figured out ways to partition the problem, and it was reflected in the way we organized our data, like above. If you looked at an application that had that chair RECORD in it, you would surely find lots of code for doing things with chairs (why else would you bother to define it?). So, if you were to extract all of that code from all over the application and put it together with the chair definition, the reasoning goes, you should have an easier time ensuring that:
- all chair code is correct
- all chair code is consistent with each other
- there's no duplicated chair code
- overall, you have less spaghetti because chair code is no longer tangled up with sofa code etc
So you take that chair definition and that code extracted from all over the application, and call that a class. Take a chair variable and call it an object, and you're off to the races.
Since this is supposed to be a pragmatic book, let's look at our first example code, this time in Python:
class Chair:
model
height
weight
color
def has_arms(self):
return self.model % 2 # even-numbered models have armrests
This doesn't look too terribly different from the Pascal example. The only difference is that class Chair now contains a has_arms method. Hopefully the purpose is fairly clear, so let me point out the important part: has arms is a calculated or inferred property--this information is not stored directly. You would use this class like so:
c = Chair()
c.model = 15
c.height = 40
c.weight = 10
c.color = 7
if c.has_arms
do_something
else
do_other_thing
Here, the "c" variable is an object. We initialize the properties like you would in Pascal (this will change later!) and do one thing if the chair has arms and something else if it doesn't. But we never initialized a "has_arms" property or anything of the like.
Since part of the goal is for you to become syntax agnostic, we present this same example again in C++:
class Chair
{
public:
int model;
int weight;
int height;
int color;
bool has_arms()
{
return model % 2 ? true : false;
}
};
Chair c;
c.model = 15;
c.height = 40;
c.weight = 10;
c.color = 7;
if (c.has_arms())
do_something;
else
do_other_thing;
Now, we would just like to mention that this is just the tip of the iceberg and this example is not representative of good style (in either language). There may seem like little purpose to making objects with so little functionality, and you could easily generate equivalent code that requires no new class:
struct Chair
{
int model;
int weight;
int height;
int color;
} c;
Chair c = {15, 40, 10, 7 };
if (c.model % 2)
do_something;
else
do_other_thing;
The purpose of this section is to help you understand the terms; we will delve more into the benefits in the sections on encapsulation, polymorphism, inheritance, and on and on. However, let us say that while the "low-level" way might seem shorter and simpler, the object oriented benefits accrue with program size and complexity. This is not surprising, as that's what OOP was designed for, but it does make simple examples hard to come by. So bear with us.
State Machines
In object oriented programming, "objects" are sometimes implementations of state machines. In the next chapter, we will talk about the difference between simple objects and behavioral entities. The previous section, discussing "What is an Object?" was really about simple objects. State machines are a better conceptual underpinning for behavioral entities —servers, regular expression processors, graphics engine, and virtual machines are all good examples of behavioral entities.
A basic distinction between simple objects and behavioral entities is that simple objects do not change their behavior based on their value. In fact, we tend to refer to their value rather than state. Simple objects tend to be kept in collections and searched through, passed to and returned from functions, and in general act like values. String and Rectangle are classic exmaples. Later on, we'll discuss message passing as an abstraction of method invocation, and it may seem a little strange in the context of simple objects.
Why would simple objects pass messages? Well, generally, they wouldn't —message passing makes more sense when you think of behavioral entities. Of course, visualizing a regular expression processor as an object isn't nearly as intuitive as visualizing a chair as an object. So the terms seem a little bit funny —I didn't make 'em, so you'll have to live with them!
If you are unfamiliar with state machines, we won't delve too deep into them, but we highly recommend you look into them more. It's good for your clarity of thought process. Pedantically speaking, state machines are somewhat complicated mathematical constructs, and there are many interesting results than can be derived using the mathematical notation. But that's not our interest. Our interest is two-fold:
- For one, state machines provide a clear graphical way to illustrate potentially complicated application logic. First, you identify what your possible states are. For example, a server might be IDLE, CONNECTED, BUSY, ERROR, etc. Then, you define all the possible inputs, ie messages that the client might send, and for each state diagram the server's response to each input. This helps promote thoroughness as you think about reactions to unanticipated messages in various states.
- Secondly, state machines are a standard tool with a relatively standard notation. There are tools and libraries that will automatically make code given some canonical state machine format as input. This is effectively what regular expression processers do.
State machines get their "objectness" more from their encapsulation than from any similarity to physical objects.
- Ed Note: Need some examples of state machines here, with diagrams.
- Ed Note: Lex/Yacc/Spirit cited as examples?
- Ed Note: there are some very interesting FSM libraries being worked on by the Boost people
- Ed Note: research other FSM libraries/tools
Classes, Types, and Classic OOP
In most books, this chapter would be the heart of the book. But this isn't "How to Program Language-of-the-week Without Even Trying," and we're not syntax obsessed, assuming you can look in the manual or one of those other books. We want to blaze through here focusing on the concepts and the why's and what-fors so we can get on to the programming! Like we said before, this isn't an academic treatise.
There are some books on OOP that will tell you that the cornerstones of object oriented programming are encapsulation, inheritance, and polymorphism, or something along those lines. This is fairly typical of the classic OOP view, and there's nothing wrong with it per-se, it's just that we've generalised some ideas and decoupled things here and there since then. But they are still important concepts, so let's dive in!
Simple Objects vs Behavioral Entities
There are two fundamentally different types of objects, created and designed with different purposes in mind, using different techniques. We label these two types "Simple Objects" and "Behavioral Entities," although these labels are our own. Simple objects tend to represent values, like colors, coordinates, vectors, strings, etc, while behavioral entities tend to represent system components like services, message queues, engines, application logic, etc.
Simple objects, if they own anything, are just wrapping what they own (like a system level handle). Usually, however, simple objects are untied to anything else, have no complicated startup or shutdown, and have little overhead. Simple objects tend to demand high performance, guaranteed correctness, discourage incorrect usage, are put in collections, searched, passed around, and generally manipulated by the rest of the program. Writing good simple objects, combined with good collection libraries provided by your language of choice, can easily reduce the complexity of basic operations by as much as 80%. Simple objects tend to focus on the encapsulation side of OOP rather than fancy message passing, as it's usually not a helpful abstraction to think of simple objects sending or receiving messages.
Behavioral entities tend to maintain their state in various collections of simple objects, like a server keeping a list of logged in users. Behavioral entities tend to focus on behavior (to state the obvious), and configuring or tailoring that behavior in various circumstances. Message passing mechanisms, including polymorphism, tend to be central to the construction of behavorial entities. As a result, you tend to get whole families of similar entities, for example, an ftp server, an ssh server, an http server, etc. that all have common functionality (like accepting connections).
One of the primary forces behind OOP is seperating out all of the common elements from these families and writing it once. This has obvious advantages in debugging and maintanence: you only have to fix bugs in common code once. Obviously, this is just abstraction at work, but OOP gives you a well understood set of tools to do it with. Some languages features are tailored for one or the other. For example, operator overloading is truly important for simple objects, but mostly irrelevant for behavorial entities.
Encapsulation
Encapsulation is one of the core, core, core, fundamental, absolutely central guiding principles of OOP —although this is probably true of all kinds of programming. In a nutshell, encapsulation is about risk management, reducing your maintenance burden, and limiting your exposure to vulnerabilities —especially those caused by bypassed/forgotten sanity checks or initialization procedures, or various issues that may arise due to the simple fact of the code changing in different ways over time.
That might seem like a strange way of putting it, but think about it in the context of an OS kernel, like the Linux kernel. In general, you don't want a common user level application modifying any internal kernel data structures directly —you want applications to work through the application protocol interface or "API". Hence encapsulation is the general term we use for giving varied levels of separation between any core system elements and any common application elements. Otherwise, "unencapsulated code" would be bad for a number of obvious reasons:
- Applications could easily set invalid or nonsensical values, causing the whole system to crash. Forcing the application to use the API ensures that sanity checks get run on all parameters and all data structures maintain a consistent state.
- Internal data structures could be updated and change (even drastically so) between seemingly minor kernel updates. Sticking to the API insulates application developers from having to rewrite their code all the time.
- Applications could be used to "snoop" on each other, elevate their privileges, hog system resources, and violate any number of security protocols if they could directly manipulate kernel data structures.
There are more, I'm sure you can think of some. You may be thinking, "This is supposed to be about object oriented programming. I don't want to be a kernel developer!" Yes, we know, but the software kernel is the perfect example for discussing encapsulation. All the kernel's internal data —its message queues, its process lists, its filehandles, etc. —are all encapsulated inside the kernel, and cannot be seen outside the kernel. To work with the kernel, you must use its public interface, the API. For all the same reasons, kernel-like design is a good way to organize your code, even when not writing kernels!
Compile Time Enforced Access Restrictions
Runtime Enforced Access Restrictions
Accessors and Virtual (or Calculated) Properties
In a nutshell, accessors are just functions that set or return properties of an object. All too often, you'll see this kind of code in Java:
class Dummy
{
private int val;
public int GetVal() { return val; }
public void SetVal(int val) { this.val = val; }
};
In this case, GetVal and SetVal are accessors, or "accessor functions." Java promotes lots of gratuitous accessors, and it often looks like unnecessary bloat. Sometimes it is. But often, people miss the point of encapsulation. If Dummy is well thought out, then Val is conceptually a property of Dummy. The fact that in this case that property is kept as a couple of bytes in memory is besides the point for anybody using a "Dummy". Even more importantly, users of Dummies are protected from any changes to how Val is calculated. All too often, the writer of a Dummy class is one himself, and Val has no logical or conceptual significance, and the accessors just get in the way.
"State" is Evil!
Something that we don't see reiterated enough: State (as opposed to change) is evil! Or, (perhaps better said) maintaining unnecessary state is the root of all (er... many) bugs. Let's look at an example, now in en:Ruby:
# We're displaying something over time, so we're dealing
# with both pixels and seconds. Based on the zoom level,
# there is a correlation between them: pixels per second,
# or PPS.
class Timeline
{
PPS;
current_position_in_seconds;
current_position_in_pixels;
public:
GetPosInSeconds() {current_position_in_seconds;}
SetPosInSeconds(s) {current_position_in_seconds = s;} # oops, we're out of sync
GetPosInPixels() {current_position_in_pixels;}
SetPosInPixels(p) {current_position_in_pixels = p;} #oops, we're out of sync
}
In this example, we're maintaining gratuitous state — which is to say, we're holding the position in both seconds AND pixels. This is convenient, and important for users of this class, but we've messed up the encapsulation here. Whenever you set the value in one unit, you've destroyed the correctness for the other unit. Okay, you say, here's a simple fix:
# We're displaying something over time, so we're dealing
# with both pixels and seconds. Based on the zoom level,
# there is a correlation between them: pixels per second,
# or PPS.
class Timeline
{
PPS;
current_position_in_seconds;
current_position_in_pixels;
public:
GetPosInSeconds() {current_position_in_seconds;}
SetPosInSeconds(s)
{
current_position_in_seconds = s;
current_position_in_pixels = s*PPS;
}
GetPosInPixels() {current_position_in_pixels;}
SetPosInPixels(p)
{
current_position_in_pixels = p;
current_position_in_seconds = p/PPS;
}
}
This fixes the obvious error with the previous example, but you've created a lot of work for yourself. Now you have to keep every calculation in every method in the Timeline class duplicated to maintain consistency. There could be dozens more where this came from (trust me). And what about when you add in other units, say inches? You have to duplicate all of that again. Presumably, you see where this is heading: calculated properties, logical properties, virtual properties, whatever you like to call them. Let's see this example again:
class Timeline
{
PPS; # Pixels Per Second
IPS; # Inches Per Second
current_position_in_seconds;
public:
GetPosInSeconds() {current_position_in_seconds;}
SetPosInSeconds(s) {current_position_in_seconds = s;}
GetPosInPixels() {current_position_in_seconds*PPS;}
SetPosInPixels(p) {current_position_in_seconds = p/PPS; }
GetPosInInches() {current_position_in_seconds*IPS;}
SetPosInInches(i) {current_position_in_seconds = i/IPS; }
}
Now, we're presuming that the conversion factors of PPS and IPS are set correctly for simplicity, but otherwise we've vastly simplified our problem. In twenty other functions we now write, we only have to worry about seconds, there's no consistency problems to worry about. Additionally, if we need to change our base units from seconds to pixels, the Timeline users need never know.
Inheritance
Multiple Inheritance
Multiple inheritance is where an object inherits its properties from many different classes. For example, a house is both a "building" and a "place to sleep in."
In multiple inheritance, an object inherits its properties from many objects (its parents or base classes). Therefore, we need to set up some classes, which I will use C++ for:
public class Building
{
int size;
int purpose;
int price;
}
public class PlaceToSleepIn
{
int type; // tent, house, dorm, etc.
bool pleasant_to_live_in; // true or false
}
In C++, to inherit all the types, one uses the : operator, like so:
public class House : Building, PlaceToSleepIn
{
// all of the Building and PlaceToSleepIn variables are here!
int rooms; // the number of rooms
}
Looking at other popular languages :
Multiple Inheritance is not a feature of Java, where as it is very much
used in C++.
Problems with Inheritance
This is my point of view: one problem of Inheritance is that users of predefined classes may not know the location of the abstracted data. Therefore, it may not be easy for them to obtain it.
Object/Relational Mappings
Separation of Static and Dynamic
Procedural Programming
Flow Charts
Systems Architecture
(Logical) Networks
The Benefits of Static Analysis
Provable Correctness
Dealing With Hierarchies
Deep Single-Rooted Hierarchy
Shallow, Multi-Rooted Hierarchies
Traditional Neglect of Dynamic Analysis
Data Flow Diagrams
Interfaces and Components
Messages, Protocols and Dispatch
RPC
Synchronous vs Asynchronous
Delegation
Virtualization and Polymorphism
Interfaces
The introduction of Components
Delegation
Inheritance vs Delegation
Dynamic Inheritance
Lifetime Management
Manual Lifetime Management
Factories
In the real world, factories are used to make stuff (e.g. automobiles). In OO programming, factories also make stuff; specifically, OO factories "make" (instantiate) objects.
Reference Counting
Bold text== Garbage Collection == Garbage Collection is done when the object is dereferenced or not in use for a long time. So Java Garbage Collection will remove the dereferenced objects from the heap or memory, so that this resource can be allocated to some other reference in the system
Persistance
Serialization
Generic Programming
Proxies
Abstracting Classes
Abstracting Algorithms
The Significance of Uniformity
Design Patterns
Abstracting Systems
The Introduction of Functional Programming Ideas
Functions as "True Citizens"
Closures
|