Objects in Problem Solutions
(IB Objective 5.4 as treated by John Lees-Miller)


Objectives

5.4.1 Outline the features of an object.
5.4.2 Explain the basic features and advantages of encapsulation.
5.4.3 Explain the basic features and advantages of information and data hiding.
- By Example: OOP Vector Drawing.
5.4.4 Explain the basic features and advantages of polymorphism.
5.4.5 Explain the basic features and advantages of inheritance.
5.4.6 Trace an algorithm that includes objects.
5.4.7 Discuss the advantages and disadvantages of using objects in problem solutions.

5.4.1: Outline the features of an object.

What is an object, and why would I use one?

Figure 1.1: An object contains both data and operations.

Figure 1.2: An object representing a fraction.

Figure 1.3: An instance of the fraction object, representing 1/2.
In computer science, an 'object' can be thought of as a blueprint. This blueprint defines a 'layout' of data and operations that are intrinsic to that particular object (Figure 1.1). So, objects define abstract representations of real things and concepts that can be used in computer programs. The process of defining an object is explored in more depth, in subsequent sections.

The primary role of objects in programming is the decomposition of solutions - that is, the goal is to break a problem and its solution into small, manageable units. The result is increased modularity, maintainability and robustness, in the program's code.

A Simple Example

For an example, let us consider a fraction, like one half. We can represent a fraction in terms of its numerator and its denominator. Therefore, we have two distinct data members. We might also consider a few operations, like Add and Subtract. These operations are termed member functions (or member procedures, or, in general, member methods). Figure 1.2 illustrates an object that defines a fraction, in terms of data and operations.

Objects and Instances

Once you have defined an object, you can create it. Just as you cannot live in a blueprint of a house, you must "build" your object in order to use it. In this case, the building process is that of allocating memory for an instance of an object, according to the "blueprint" of the object. This process of creating an object is called instantiation (that is, to create an instance).

Constructors and Destructors

There are two special types of member method that give the object control over how it is created and destroyed: its constructor and destructor, respectively. A constructor is simply a method that is automatically called when the object is instantiated. A constructor can take parameters, just like a normal method, but cannot specify a return type. When an instance goes out of scope, its destructor is automatically called. Destructors neither accept parameters nor have a return type.

Terminology

In C++, Java, C# and other popular object-oriented languages, the term "class" is used to denote what IB calls an "object." Be careful when speaking of "objects," because the term often means different things in each language. This text will use IB's definitions, and will give code in PURE.

Back to Top


5.4.2: Explain the basic features and advantages of encapsulation.

What is encapsulation?

The term "encapsulate," in object oriented programming, refers to the process of grouping of related data and operations in named objects.

In procedural programming languages, the programmer's ability to integrate data and operations was very limited. For example, to implement the Add member method of the Fraction object (described above in Figure 1.2), one could define a method such as:

// Postcondition: The values of NUMERATOR and DENOMINATOR are modified
// such that they reflect the result of adding their old values with the
// numerator and denominator passed in parameters 3 and 4.
procedure AddFractions( ref NUMERATOR integer, ref DENOMINATOR integer
	, val NUMERATOR_TO_ADD integer, val DENOMINATOR_TO_ADD integer )
	// ... (Code) ...
endprocedure AddFractions
While functional, this definition is cumbersome. Using a record (struct), a more succinct definition could be achieved:
newtype FRACTION record
	declare NUMERATOR	integer
	declare DENOMINATOR	integer
endrecord

// Postcondition: The fields of FRACT are updated such that they reflect the
// result of adding FRACT_TO_ADD to their original values.
procedure AddFractions( ref FRACT is FRACTION, val FRACT_TO_ADD is FRACTION )
	// ... (Code) ...
endprocedure AddFractions
This is clearer and easier to work with, but we can do better. Using an object, we can encapsulate the add operation within the fraction object. The following is a partial pseudo-code representation of the object:
newtype FRACTION object
	declare NUMERATOR	integer
	declare DENOMINATOR	integer

	// Postcondition: The value of FRACT_TO_ADD is added to the instance
	// on which this operation is performed.
	procedure Add( val FRACT_TO_ADD is FRACTION )
	// ... (Code) ...
	endprocedure Add
endobject FRACTION
In this example, all of the relevant data and operations are encapsulated by the object. This makes working with fractions more intuitive. In general, encapsulation makes code easier to work with by making it more modular. The other advantage is that encapsulation allows for data hiding, which is described in the next section.

Back to Top


5.4.3: Explain the basic features and advantages of information and data hiding.

What is data / information hiding?

Data hiding is another tool provided by most object-oriented languages. It is, in general, the process of separating the implementation of an object from its clients. This allows the client to use the object in an abstract way. A more thorough discussion of this practice is given in the following section. At its most basic level, data / information hiding means designating some members accessible to calling code and others as hidden from calling code. In PURE, the keywords accessible and hidden are appended to data members and member methods, as in the following example, updating the FRACTION class.

newtype FRACTION object
	// Note: by convention, hidden members of an object should be
	// prefixed with an underscore (_).
	declare _NUMERATOR	integer hidden
	declare _DENOMINATOR	integer hidden

	// Postcondition: The value of FRACT_TO_ADD is added to the instance
	// on which this operation is performed.
	procedure Add( val FRACT_TO_ADD is FRACTION ) accessible
	// ... (Code here can access _NUMERATOR and _DENOMINATOR
	// of itself and of FRACT_TO_ADD) ...
	endprocedure Add
endobject FRACTION

If client code attempts to read or write from a hidden data member or call a hidden member method, the compiler will error. In this way, objects can define strict contracts between themselves and client code. This increase in control can result in robust code.

Back to Top


Example: OOP Vector Drawing.

By Example: How can I apply objects to a problem?

Consider a vector graphics program (e.g. Microsoft PowerPoint) that will, initially, define one shape: a rectangle. Let's consider some ways we might abstract a rectangle, in Figure E.1.

Figure E.1: Three ways of Defining a Rectangle
(a) Use four points. (b) Use two non-adjacent points. (c) Use an origin point and define x and y extents.

Evaluating the options is difficult without a clear picture of what the rectangles will be used for. If rectangles will be read more frequently than written, E.1.a might represent the best choice, because all four points are known, in memory. However, E.1.a defines two superfluous points (B and D) that could be easily calculated from A and C, so we might opt for E.1.b or E.1.c, because they make better use of memory. While adequate planning might allow us to predict the immediate demands on the rectangle, and define it accordingly, future changes to the system could invalidate the assumptions we are making. In OOP, the impact of these sorts of changes (that is, those to the specific implementation of objects) can be minimized using a technique known as black boxing or information hiding.

A 'black boxed' rectangle object does not expose information about how it is internally implemented, but instead defines a "contract" of accessible member methods that client code can rely on. If the contract is well-defined, major changes to the internal structure of the object can be made without having to change client code. For a rectangle, our contract might include methods that:

  1. Create a rectangle instance with points A and C.
  2. Create a rectangle instance with point A, a Δx and a Δy.
  3. Return the four corner points (A, B, C and D) independently.
  4. Set the four corner points (A, B, C and D) independently.
  5. Return the width and height of the rectangle. These will always be positive.
  6. Return the Δx and Δy of the rectangle. These may be negative.
  7. Resize the rectangle to a certain Δx or Δy, relative to point A.
  8. Translate the rectangle to a certain position, relative to point A.
  9. Return a string representation of the object, for display purposes.
Note that, to maintain backwards compatibility, no "clause" must ever be altered or removed from an object's contract. Adding methods is permissible, as is changing the internal workings of methods, but the results of calling methods under any old contract cannot be changed. Once such a contract is defined and documented, construction of the object can begin.

From Figure E.1, it seems that one common feature for rectangles is a point. Therefore, it is worth considering a point, before considering a rectangle. So, let's create an object that represents a point. First, we should ask exactly what functionality we need from a point - that is, develop a contract. In the case of a vector drawing program, it should define a position in two dimensions. It would also be useful to have a way of representing the point as a string, for display and debugging purposes. So, we might build a point that looks something like:

newtype POINT object

	// Declare two type real data members to define a point in X and Y.
	declare _X	real hidden
	declare _Y	real hidden

	// Define a constructor for the class. We take two parameters
	// (one for X and one for Y.) By PURE conventions, CONSTRUCTORs are
	// declared hidden (ie. they cannot be called directly by client code).
	procedure CONSTRUCTOR( val X real, val Y real ) hidden
		// Initialize data members.
		_X <-- X
		_Y <-- Y
	endprocedure CONSTRUCTOR

	// Define methods that return and manipulate _X and _Y.
	function X() result real accessible
		return _X
	endfunction X
	function Y() result real accessible
		return _Y
	endfunction Y

	procedure SetX( val VALUE real ) accessible
		_X <-- VALUE
	endprocedure SetX
	procedure SetY( val VALUE real ) accessible
		_Y <-- VALUE
	endprocedure SetY

	// TO_STRING: Returns a string representation of the instance
	// in the format "(X, Y)".
	function TO_STRING( ) accessible
		// (assume CONVERT_TO_STRING is defined elsewhere).
		declare S is string
		S <-- "("
		S <-- concat(S, CONVERT_TO_STRING(_X))
		S <-- concat(S, ", ")
		S <-- concat(S, CONVERT_TO_STRING(_Y))
		S <-- concat(S, ")")
		return S
	endfunction TO_STRING

endobject POINT
Note that the POINT object does not define a destructor. In most languages, you can rely on the compiler to automatically generate a destructor for such a simple object. Generally speaking, you need only define your own destructor if you are using dynamic memory allocation to allocate member variables. The same rule applies to a copy constructor (responsible for creating a duplicate of an instance of the same type), which can also be generated by the compiler, for this object.

Note that the methods X and Y are often referred to as accessor methods, and that SetX and SetY are often referred to as mutator methods.

With our POINT object defined, we can define an object that represents a rectangle. We will use the representation shown in E.1.b, in this example.

newtype RECTANGLE object

	// Declare two type real data members to define a point in X and Y.
	declare _A is POINT hidden
	declare _C is POINT hidden

	// Constructor: Create from points A and C.
	procedure CONSTRUCTOR( val A is POINT, val C is POINT ) hidden

		// This constructor is especially easy, for us.
		_A <-- A
		_C <-- C

	endprocedure CONSTRUCTOR

	// Constructor: Create from point A, a Δx and a Δy.
	// Note: Technically, PURE does not allow more than 1 constructor per
	// object, but this limitation is ignored, here. (This is constructor
	// overloading.)
	procedure CONSTRUCTOR( val A is POINT
		, val DELTA_X real, val DELTA_Y real ) hidden

		// We know A.
		_A <-- A

		// C must be calculated. Note how we create the POINT:
		// TYPE( constructor parameters ).
		_C <-- POINT( A.X() + DELTA_X, A.Y() + DELTA_Y )

	endprocedure CONSTRUCTOR

	// Define accessor and mutator for A.
	function A() result is POINT accessible
		return _A
	endfunction A
	procedure SetA( val VALUE is POINT ) accessible
		_A <-- VALUE
	endprocedure SetA

	// Define accessor and mutator for B.
	function B() result is POINT accessible
		// Calculate as B( C.x, A.y ).
		return POINT( C.X(), A.Y() )
	endfunction B
	procedure SetB( val VALUE is POINT ) accessible
		// Affects C.x and A.y.
		_C.SetX(VALUE.X())
		_A.SetY(VALUE.Y())
	endprocedure SetB

	// Define accessor and mutator for C.
	function C() result is POINT accessible
		return _C
	endfunction C
	procedure SetC( val VALUE is POINT ) accessible
		_C <-- VALUE
	endprocedure SetC

	// Define accessor and mutator for D.
	function D() result is POINT accessible
		// Calculate as D( A.x, C.y ).
		return POINT( A.X(), C.Y() )
	endfunction D
	procedure SetD( val VALUE is POINT ) accessible
		// Affects A.x and C.y.
		_A.SetX(VALUE.X())
		_C.SetY(VALUE.Y())
	endprocedure SetD

	// Define accessors and mutators for Δx and Δy.
	function DELTA_X() result real accessible
		// Calculate using A.x and C.x.
		return (C.X() - A.X())
	endfunction DELTA_X
	procedure SetDELTA_X( val VALUE real ) accessible
		_C.SetX(_A.X() + VALUE)
	endprocedure SetDELTA_X

	function DELTA_Y() result real accessible
		// Calculate using A.y and C.y.
		return (C.Y() - A.Y())
	endfunction DELTA_Y
	procedure SetDELTA_Y( val VALUE real ) accessible
		_C.SetY(_A.Y() + VALUE)
	endprocedure SetDELTA_Y

	// Define accessors for width and height.
	function WIDTH() result real accessible
		return abs(DELTA_X())
	endfunction WIDTH
	function HEIGHT() result real accessible
		return abs(DELTA_Y())
	endfunction HEIGHT

	// TRANSLATE: Moves rectangle such that A is at NEW_A.
	procedure TRANSLATE( val NEW_A is POINT ) accessible
		// Preserve Δx and Δy in _C.
		_C.SetX( NEW_A.X() + ( C.X() - A.X() ) )
		_C.SetY( NEW_A.Y() + ( C.Y() - A.Y() ) )
		// Then just set _A.
		_A <-- NEW_A
	endprocedure TRANSLATE

	// TO_STRING: returns a string representation of the instance
	// in the format "(A(X, Y), B(X, Y), C(X, Y), D(X, Y))".
	function TO_STRING( ) accessible
		declare S is string
		S <-- "(A"
		S <-- concat(S, A().TO_STRING())
		S <-- concat(S, ", B")
		S <-- concat(S, B().TO_STRING())
		S <-- concat(S, ", C")
		S <-- concat(S, C().TO_STRING())
		S <-- concat(S, ", D")
		S <-- concat(S, D().TO_STRING())
		S <-- concat(S, ")")
		return S
	endfunction TO_STRING

endobject RECTANGLE
Note how the functions A(), B(), C() and D() all work the same way for the caller (each returns the associated point), even while the returns for B() and D() are actually calculated "on the fly." This makes the TO_STRING method far easier to write, because it can leverage the work done by B() and D().

Also, it is worth noting that WIDTH() and HEIGHT() reference the DELTA_X() and DELTA_Y() methods. When a method call to a member method is made from within another member method, it assumed that it is being invoked with the same instance, as this example demonstrates. (This is the same idea as that of accessing instance data members from member methods.)

Back to Top


5.4.4: Explain the basic features and advantages of polymorphism.

Polymorphism is an inventive way of saying that two different versions of a method with the same name and parameter list may exist as part of two different objects. We have already used polymorphism, in the above example: the TO_STRING methods of POINT and RECTANGLE are polymorphic. Clearly, calling TO_STRING on an instance of POINT will execute different code than calling TO_STRING on an instance of RECTANGLE.

To appreciate one advantage of polymorphism, consider the result of its absence. We would have to define a method POINT.POINT_TO_STRING, in order to avoid a conflict with RECTANGLE, which is redundant - we just need POINT.TO_STRING. So, polymorphism makes function names less cumbersome, and makes objects easier to work with. The other advantage of polymorphism comes into play in the overridden methods of inherited classes (see the next section).

Back to Top


5.4.5: Explain the basic features and advantages of inheritance.

As if OOP wasn't already cool enough, almost all OOP languages allow programmers to use inheritance to organize objects and facilitate code reuse.

If we return to our initial analogy between an object and a blueprint, one could think of the child object as a transparent sheet, laid over the parent blueprint. The programmer then writes on this sheet, so s/he can write over some of the old members and add new ones. Just as it would be easy to overlay another transparent sheet, a child object can be the parent of another object, and so on. Also, because the sheets are transparent, changes to the parent object(s) affect child objects. A full blueprint for each child is obtained by "taking a picture" of the parent object through the transparencies.

For example, if we were to create an object SQUARE, we could say that because a square is a rectangle, we should take the characteristics of a rectangle and impose rules that make it into a square. So, in OOP terms, we would create the SQUARE object as a child of the RECTANGLE object.

newtype SQUARE object inherits RECTANGLE

	// (... make necessary changes to implementation, here ...)

endobject SQUARE

The process of changing member methods between the parent and the child is called overriding. As mentioned above, this overriding is a form of polymorphism because the child object and the parent object (in most cases - there are exceptions, but they are beyond the scope of this document) considered as different objects. To override a method in PURE, you just redefine it in the child class.

Note: So far as IB is concerned, a child object may specify a maximum of one parent objects, though multiple inheritance is supported by languages such as C++. Also, no child object will ever be a parent to another object, in IB's domain.

Back to Top


5.4.6: Trace an algorithm that includes objects.

Consider an algorithm that arranges an array of 4 RECTANGLEs such that they form a chain, with point A's connecting with point C's, for some reason.

procedure ARRANGE_RECTANGLES( ref RECT_ARRAY is RECTANGLE array[0..3] )
	declare I integer

	// Start at 2nd item.
	for I <-- 1 upto 3 do

		// Translate rectangle to C of previous rectangle.
		RECT_ARRAY[I].TRANSLATE( RECT_ARRAY[I - 1].C() )

	endfor

endprocedure ARRANGE_RECTANGLES

Consider the tracing of the call:

ARRANGE_RECTANGLES( {
		(A(1,1), C(2,2))
		(A(3,3), C(5,5))
		(A(6,6), C(9,9))
		(A(10,10), C(12,12))
	} )
IRECT_ARRAY
    [0] [1] [2] [3]
  A() C() ΔX() ΔY() A() C() ΔX() ΔY() A() C() ΔX() ΔY() A() C() ΔX() ΔY()
- 1, 1 2, 2 11 3, 3 5, 5 22 6, 6 9, 9 33 10, 10 12, 12 22
1       2, 2 4, 4               
2             4, 4 7, 7         
3                   7, 7 9, 9   
The above tracing shows the results of the algorithm. Note that the size of the rectangles does not change, but instead only their position.

Back to Top


5.4.7: Discuss the advantages and disadvantages of using objects in problem solutions.

The advantages of object-oriented programming have been detailed in the above sections. Usually, the goal of OOP is to write more modular code while maximizing code re-use, thereby reducing the coding cost of the system. It is a also a valuable tool for problem decomposition. This is made possible using the core OOP techniques, as follows:

  • Encapsulation
  • Data Hiding
  • Polymorphism
  • Inheritance

The disadvantages of OOP are as follows:

  • It takes more time to code objects, in general, at least for small or simple problems.
  • Black boxing is great when it works, but problems in object libraries are very difficult to correct, for clients. Of course, the same is true of any other compiled library.
  • Not all languages support objects (therefore it is not always possible to, for example, integrate objects with an existing code base as part of an upgrade).
  • There is significant variation in the precise support for objects between different languages. This lack of standardization can be prohibitive to designing for portability.

Overall, OOP is a very valuable and popular scheme for solving programming problems.

Back to Top


Revised: April 30, 2003 by John Lees-Miller.