Introduction to Object-Oriented Programming by Bruce Newburger, Zack Urlocker Copyright(c) 1989 The Whitewater Group, Inc. Overview -------- Object-oriented programming (OOP) represents a new, more productive approach to solving problems. Rather than working with traditional procedures and separate data structures, we create objects which encompass both the data and the operations. Object-oriented programming languages reduce the amount of code that needs to be written and maintained by enabling the programmer to create classes of reusable objects which encapsulate behavior and ensure data abstraction. Object-oriented programming is easy to learn since it is based on a few simple yet powerful concepts. Many programmers find object-oriented programming to be a natural extension to how they think. We will define objects, classes, messages and inheritance using understandable non-programming examples. We will often refer to the Actor(R) language for programming examples of object-oriented concepts. Actor is an interactive object-oriented language for the development of Microsoft Windows(tm) applications. History of OOP -------------- Object-oriented programming was first introduced in the Simula language in 1967. Smalltalk, developed at Xerox Palo Alto Research Center (PARC), later refined the concepts and became the first pure object-oriented language. Since then, many other object-oriented languages have emerged including Actor, C preprocessors such as C++ and Objective-C, and extensions to Lisp such as Flavors and Scheme. In order for a language to be considered object-oriented it must meet three criteria: 1] encapsulation of data and instructions into units of functionality called objects 2] inheritance of functionality through a class hierarchy 3] dynamic runtime binding of messages sent to objects Languages such as Ada and Modula-2, which are sometimes called object-oriented, meet the first criteria but fail the second and third. Pure object-oriented languages such as Actor meet all three criteria and have the advantage of a consistent world in which everything is an object and all operations take place by sending messages to objects. This makes learning the language and debugging programs easier. Hybrid languages such as C++ add objects to standard C, providing the mixed blessing of being able to freely mix object-oriented C with traditional C. This compromises some of the power of object-oriented programming. For example, classes are not objects and dynamic binding is limited due to the constraints of C. When selecting an object-oriented language you should consider factors such as: 1] ease of learning 2] predefined classes 3] programming environment and tools 4] ease of debugging 5] ability to rapidly prototype and test code 6] execution speed 7] memory management 8] access to existing libraries 9] ability to create a standalone, distributable application One of the goals of developing Actor was to provide a rich class library and interactive environment that improved programmer productivity without compromising efficiency. Object-oriented Programming --------------------------- The basis of object-oriented programming is the creation and management of objects. An object is a programming entity that is designed to closely resemble real-life objects. An object has attributes and responds to instructions. For example, in a check-writing program, a check object has these attributes: amount, check number, date, to whom, and comment. It responds to these instructions: write, cash, sign, record. The advantage of breaking down a problem in terms of objects is the way in which we represent data. In the check-writing problem above, the data is the amount, the check number and so on. The rest of the program can access the data only through the instructions like write and cash. In effect, we have encapsulated access to the data since a check object is both the data and the instructions that act upon it. In Actor, many entities that you would normally think of as data structures are actually objects. For example, integers, characters, arrays, strings and files are all objects. There are also objects corresponding to more sophisticated data structures such as stacks, queues, look up tables, windows, dialog boxes and so on. A non-programming example of an object as a functional entity is a car. A car object has attributes such as the transmission, year and color. In addition, the car object responds to certain instructions, such as go (step on the gas), stop (step on the brake), left (turn the wheel left) and right (turn the wheel right). When you step on the gas, you need not be concerned with the internal workings of the engine. All cars are driven the same way, regardless of the particular make or model. The ability to manipulate an object's data without knowledge of the data's internal format is called data abstraction. Objects can be made up of primitive data, indexed data or named data. Primitive data is embedded directly in the structure of objects such as numbers, characters and symbols so that access is as efficient as possible. Indexed data is used in objects such as arrays or strings where there are several items that make up an object. We can refer to indexed data in an object x via the familiar array notation x[i]. Objects can also be made up of named instance variables, which refer to other objects. For example a car object myCar will have instance variables paint, year and transmission. We can refer to the instance variables via dot notation such as myCar.year, although generally we will want to encapsulate access to private data. To review, an object is the basic building block of an object- oriented programming system. Objects provide a simple means to represent the entities of a real world problem in terms of a programming entity. An object has instance variables that describe its data, and instructions that define operations to be performed on the object. In general, objects may have named instance variables, indexed variables, both or neither, depending on what is required. The instructions are part of the object and are tailored to the object's internal representation. Objects are Grouped in Classes ------------------------------ In object-oriented programming, we call a type a class. Each object belongs to one and only one class. We say that an object is an instance of the class it belongs to. There can be many different classes; Actor includes over a hundred predefined classes that you can use in your programs. Think of a class as an object template, or an object factory. Every object of a given class has the same data format and responds to the same instructions. In the car example, we might have a class called Ford. Ford is a class of car, and all Ford cars belong to the class. There is no limit to the number of objects you can create of a given class. The instructions that an object can respond to are managed by the class. The data associated with a particular object is managed by the object itself. For example, you might have a red Ford Cortina and a blue Ford Mustang. Thus, the objects you use in your programs come from classes. You can use any of the predefined classes that are loaded in the system. You can also create your own classes. Objects Respond to Messages --------------------------- Objects perform operations in response to messages. For example, when we press on the brake we send a stop message to a car object. The car object's brake system is authorized to handle the stop message because it consists of specialized parts like brake pads and hydraulics. By following a set of conventions, or protocol, the Actor programmer is protected from unauthorized data manipulation. Too often, procedural programmers put their hands right to the wheel and get burned. A message is different than a subroutine call in that different objects can respond to the same message in different ways. For example, cars, motorcycles and bicycles will all respond to a stop message, but the operations they perform are object- specific. Examples of messages in Actor are shown below. printLine(x); /* whatever x is, tell it to print */ draw(item, hDC); /* tell the item to draw itself */ close(device); /* send a close message to device */ y := 5*7 /* tell 7 to multiply by 5 */ In the examples above the receiver of the message is the first parameter in parenthesese; arguments may follow the receiver. Note that the message makes no assumptions about the class of the receiver or of the arguments; they are simply objects. It is the receiver's responsibility to respond to a message in an appropriate manner. This gives us a great deal of flexibility since different objects can respond to the same message differently. This is known as polymorphism, literally meaning "many behaviors." Polymorphism allows us to write generic reusable code more easily, since we can specify general instructions and delegate the implementation details to the objects that are involved. Since there are no assumptions about the classes of objects involved, there are fewer dependencies in the code and maintenance is easier. Objects respond to messages according to methods that have been defined in their class. For example if a Cube object receives a draw message, then the class Cube (or one of its ancestors) must define a draw method. A method definition corresponds to a procedure definition in other languages. A sample method definition in Actor is shown below. /* This method defines how a Cube object will respond to a draw message. The argument hDC is a handle to a Windows display context where the drawing takes place. A cube object is made up of two rectangles called front and rear. A cube is drawn by drawing the front and rear faces and then connecting them. self refers to the cube that received the draw message. */ Def draw(self, hDC) { draw(front, hDC);/* tell the front to draw */ draw(rear, hDC); /* tell the rear to draw */ drawConnections(self, hDC); /* connect the faces */ } Class Hierarchy --------------- An object-oriented system has the responsibility of organizing and managing the one-hundred or more classes that your programs will use. It does this by organizing classes into a hierarchy. At the top of the hierarchy is the most general classes and at the bottom are the most specific classes. For example, in the car example, Ford is a class that defines what Ford car objects are. But there are classes of Ford car objects that are more specialized Fords, such as Taurus, Escort, and Thunderbird. These classes define Fords in a much more specialized manner than does the Ford class itself. So we say that the classes Taurus, Escort and Thunderbird descend from class Ford and the Ford class is their ancestor class. In some object-oriented languages the terms superclass and subclass or base class and derived class are used instead of ancestor and descendant. But there can be classes more general than Ford. For example, Ford might descend from class Car, which would, in turn, descend from class Vehicle, and so on. Class Car defines how a car behaves, class Ford defines how Ford cars behave in addition to cars in general, and class Escort defines how Escorts behave in addition to Ford cars in general. Of course, if all we wanted was a Ford Escort object, we would write only one class, called Escort. This class would define exactly how a Ford Escort car operates. This methodology is limiting because if we decide later to create a Ford Taurus object we will have to duplicate most of the code which describes first how a vehicle behaves, then how a car behaves, and then how a Ford behaves. This is what is done in a procedural language. An object-oriented language eliminates duplicated effort by allowing classes to share behaviors. You might find it strange to define a Car class. After all, what is an instance of the Car class. We have no such thing as a generic car; all cars must be of some make and model. In the same way, we have no instances of the Ford class. All Fords must belong to one of the subclasses Escort, Taurus or Thunderbird. We call classes like Car and Ford formal classes. We never create instances of formal classes. Formal classes are used to share common behavior among classes. A real Actor example of formal classes is the part of the Actor class tree that defines various collections including arrays, sets, strings, dictionaries and so on. The formal class Collection defines the common behavior among the classes and has descendant classes Set, Bag, IndexedCollection and KeyedCollection. IndexedCollection is a formal class that defines the common behavior among collections that use integer indices. It has descendant classes such as Array, String, Struct and OrderedCollection. KeyedCollection is a formal class that allows arbitrary keyed access and has descendant classes such as Dictionary, Frame and Slot. Inheritance ----------- Inheritance is a mechanism for sharing behaviors between classes. The behavior of a class's instances is defined in that classes methods. But a class also inherits the behavior of all of its ancestors. For example, the class Car defines how cars in general behave. The class Ford inherits the general car behavior from class Car, and adds behavior that is specific to Ford cars. But the Ford class does not have to redefine the car behavior. Next, the Escort class inherits behavior of cars from class Car, and behavior of Fords from class Ford. It adds the behavior specific only to Escorts. For example, assume that all Fords use the same braking system. Then the method brake would be defined in class Ford. When we step on the brake pedal of an Escort, a stop message is sent. However, Escort does not define a stop method, and the search continues in class Escort's direct ancestor, the class Ford. The stop method of class Ford is then invoked. In a similar way, Escort can inherit behaviors from the Car and Vehicle classes. The behaviors of any given class is really an amalgamation of the behaviors of all of its ancestors. This straightforward process of inheritance prevents you from having to re-invent the wheel, or the brakes, for that matter. Let's say that most Ford cars use the same braking system, but the Thunderbird has its own, anti-lock braking system. In this case, the Thunderbird class would redefine the stop method. Thus the brake method of the Ford class would never be invoked by Thunderbirds. However, its existence higher up in the class hierarchy does not cause a conflict. Other Ford cars would continue to use the standard braking system. Single Inheritance vs. Multiple Inheritance ------------------------------------------- Actor, like most object-oriented languages, operates according to single inheritance. That means that the system searches for methods directly up the class hierarchy, first in the class of the instance, then in that class's direct ancestor, and so on up the line. Each class directly descends from only one ancestor class. In a scheme called multiple inheritance, used in some object- oriented versions of Lisp, a class can descend from many unrelated classes. Multiple inheritance can make it easy to combine behaviors of several classes at the expense of having to specify how conflicts are resolved. Although there are some limitations to single inheritance, you can often achieve the benefits of combined behavior by adding instance variables of other classes. This is not as general a solution as multiple inheritance, but it avoids some of the complexity of conflict resolution. Encapsulation ------------- Encapsulation is a design goal of object-oriented programming. It is achieved by allowing access to data only through its own methods. This ensures that instructions are operating on the proper data. No other parts of the program can operate directly on another object's data. An object's internal format is insulated from other objects. An important factor in achieving encapsulation is designing different classes of objects that operate using a common protocol. That means many objects will respond to the same messages but will implement their own methods. That way, your program can send a generic message and leave the implementation up to the receiving object. This reduces interdependencies, and increases interchangability and reusability. If we bring to mind the car example, we can think of a car's engine as being encapsulated. Although engines may differ in implementation, they all interface with the driver through a common protocol: step on the gas for more power, and let up on the gas for less power. Since all drivers know this protocol, all drivers know how to drive all cars. It doesn't matter what engine is in the car, that detail is insulated from the rest of the car and the driver. This makes cars more easily interchangeable and maintainable. Data abstraction is the result of good object-oriented code that takes advantage of encapsulation and polymorphism. Data is abstracted when it is shielded by a full set of methods. Only a class's methods utilize the details of the class's data format or contents. Summary ------- Object-oriented programming is a significant departure from procedural programming. Rather than treat data and procedures separately, they are closely linked into what are known as objects. The main advantage of object-oriented programming is the ability to reuse code and develop more maintainable systems in a shorter amount of time. This is achieved by enabling the programmer to create reusable classes that inherit the behavior of ancestor classes. Good object-oriented programming makes use of encapsulation and polymorphism to provide abstract data types. Actor is a registered trademark and The Whitewater Group is a registered service mark of The Whitewater Group, Inc. Other product names may be trademarks of their respective owners.