Return to lecture notes index

15-100 Lecture 12 (Friday, September 29, 2006)

Software Design and the Object-Oriented Approach

There are many different approachs to the design of software systems. Over the years, we have developed and taught many different design methodologies. In the past, we began programming courses discussing flow charts and/or top-down diagrams. And sometimes we even discussed the merits of bottom-up approaches. But, these approaches proved too limited for complex problems and a new methodology came to light, object-oriented programming (OOP).

At the heart of the object-oriented approach are objects. Don't expect a complex mathematical description or a long technical definition. You won't find one, at least here. Instead let me just suggest that objects are all of the individual identifiable components of a system. If you want to point at it, name it, use it, or talk about it, it is an object.

One way of discovering the objects in a system is to describe the system to someone else. Then consider all of the nouns that you used in your description. These are probably objects.

A very important piece of object-oriented design is, as you might imagine, describing the objects within the system. Again, a conversational model might be useful here. Often times when we describe things to each other, we will talk about the objects first in terms of their behavior. For example, if a little child asks you, "What is a car?" You would first tell the child that it can move forward and backward and turn. And then you would tell the child that it can carry people. Eventually you would tell the child that cars come in all different colors, and that some have two doors and others four doors, &c.

This is the same process that we'll use to describe the objects in software systems. After we identify each object, we'll ask ourselves, "What does this object do?" We call the things that an object can do its behaviors. We also ask ourselves, "What do we need to tell it to get it to do these things? What does it need to know?" For example, if we want a car to move forward, we need to tell it, "how fast". If we want it to turn left, we have to tell it, "how much". These are the parameters of its behaviors.

After considering , "What can the object do?" We'll ask ourselves, "What are the other properties of the object?" What color is it? How big is it? How many doors does it have? These aspects of an object we call its attributes.

Many of these attributes are visible attributes, we can observe them from outside of the object. But, some of the attributes we can only discover by inference. We'll call these the hidden attributes.

For example, if we consider a typical calculator, the only number we can see on the display is the most recent input. But, by observing how a calculator adds and subtracts, we can infer that it maintains an accumulator containing the most recent result. We can't see the accumulator, but we know that it must exist -- without it, we cannot explain the behavior of the calculator.

Classes: Different Types of Objects

If I would ask you to describe the contents of a room with only one person inside, you would probably describe that one person, possibly by name. But, if I would ask you to describe a room with 100 strangers inside, you would probably approach the problem slightly differently.

You would describe a generic person first. You would tell me what all people have in common, and how they differ. Then, you would tell me about each object in the room by first identifying it as a person, and then telling me about how it is special -- by describing its collection of attributes.

In object-oriented languages, we can do exactly the same thing. We can describe types of objects, a.k.a., classes of objects. We do this by writing a class specification that describes the behaviors and attributes of a class of objects. Then, when we create a new object, we do it by building them according to thier class specification and "filling in the blanks" for each attribute. So, in object oriented languages, new objects are "instances of a class" meaning that they are built according to some class specification.

That's a whole bunch of computer lingo, so lets apply it to the car example. We can define a class for the object "car." We observe that cars can have differing numbers of doors, differing sizes of engines, and differing colors, but are all capable of moving forward, backward, and turning left and right. Thus the class specification would include the car's attributes (number of doors, size of engines, and color) and its behaviors (moving forward and backward, and turing). Then, we can create an instance of the car class, filling in the specifications. If the instance is a ferrari, the new object should be be red and have two doors and a V8 engine.

Food for thought: Sometimes in using the car we might be concerned with how it looks -- and on other occasions we just might want to go for a spin without worrying about its color.

The Object-Oriented Approach to Controling a Room's Temerature

To help you guys with possible questions, a small example follows. Let's model something in the real world, controling the temperature in one of the 5419 clusters:


You would then model the room by defining it's size and openings and then populating it with heat sources (the objects). People and Computers are two different objects and thus two different class types with different specifications. If you weren't using an OO (object oriented) approach you might model the room and each person/computer in it individually (as opposed to grouping them as types of objects). However, that approach has several disadvantages:

  1. It would take alot more time (since you have to rewrite similar code for each Person or Computer).
  2. It would be harder to read/ interpret.
  3. Additions or changes to People's attributes would have to be individually recoded. (For example: if their clothing changed from summer weight to winter coats it's eaiser to only change the specification of the class "People" and not the coding describing each individual)
Another advantage to OOP: The classes created to solve one problem can be used to populate another room/ solve another problem (like how to heat a conferance room).

When you are done modeling the room note that it has been populated with People and Computers in other words its composition is of People and Computers.

Object-Oriented Programming and Message Passing

How do we ask objects (or students acting as objects) to exhibit these behaviors? -- we simply ask, e.g., "Please, stand up" and the student stood up. In other words we send a message to the object and ask it to do something. In return the object does what we ask and we observe when it is done.

Sometimes the requests were simple and required no other information, such as, "Stand", whereas others were more complicated and required more information, such as "Jump 2 feet." We call such additional pieces of information paramters or arguments because they describe the specifics of a particular action.

In the language of object-oriented programming, we send a message or request and recieve a reply. The message communicates what we want done and the reply indicates that the action has been taken, and if appropriate, includes a result.

Types of Behaviors

Sometimes we ask objects to tell us things about themselves, but to change nothing. For example, I can ask you for your name -- that gives me a piece of information without changing anything about you. This type of method is often called an accessor.

Other methods actually cause an object to change something about itself. For example, if I ask you to "Stand up", you rise, changing your position. This type of method is known often called a "mutator".

Sending a message to one object can cause it, in turn, to communicate with other objects by sending them messages. These more complicated behaviors can have more complex or subtle effects on many different parts of the system.

As one example, I asked someone in the class to tell me the average age of the three people nearest to her/him. This required asking three people for their age, averaging, and returing the value to me.

As another example, I launched a virus. I asked someone in the class to introduce himself to two other people, and in the process, ask each to do exaclty the same thing. Before long, each person was asking other people to introduce themselves to two other people, and being asked to do the same! Obviously this request had a huge effect on our sytem, the classroom.

Capabilities and Protocols

For two objects to interact, there must be agreement about two things: The capabilities of each object and the proper way to make a request and receive a reply from an object.

In class, I ordered one person to fly and hover in the corner of the room. Another person was ordered to burrow down to the center of the Earth. Each request was met with a confused look, some laughter, and a complete lack of compliance by the student who was asked to act. The basic problem is that a Students can't fly or borrow. These, quite simply, are not behaviors of humans. We are such limited creatures.

As another demonstration, I asked students to volunteer themselves as native speakers of foreign languages. From the volunteers, I selected a student who spoke an obscure language. That student was asked to make a request, in the foreign language, of another student. The request was to be for a G-rated, non-embarassing, general-audience-accessible behavior that the student could, in fact, readily exhibit.

In each case, even after several attempts, the target student didn't perform the requested action -- she or he didn't understand the request. The requested actions were things such as stand up, and introduce yourself to a nearby person. Clearly the target student would have satisfied the request -- if only it was understood.

These demonstrations were designed to highlight two important aspects of the interaction among objects:

  1. Objects can't exhibit behaviors other than those that are characteristic of their type of object. People can't fly.

  2. There must be agreement between the requesting objected and the target object about the format of the messages. What is the behavior called? What other information needs to be sent? What will be sent back, if anything, once the request is complete?

In short, we need to define, in advance, the protocol by which objects will communicate.

Interfaces as a Protocol Specification

In Java, one way to specify the requests that can be made of certain types of objects and how to make them is known as am interface specification. The interface specification lists all of the behaviors that implementing classes of objects can exhibit, how to ask for each behavior, and what will be returned when each has been completed.

By specifying the messages that each type of object can receive and the type of message that it will return after completing the requested action, we are, in effect, specifying the protocol by which objects can interact.

By using the interface specifications that we provide, Java ensures that our programs only send legal, well-formed messages between objects.

Interface Specifications vs. Class Specifications: Messages vs. Actions

Tied to each interface specification is at least one class specification. An interface specification defines "what" an instance can do and "how to ask". The class specification defines "how it is actually done".

So, for each message that we can send an object, the class specification defines the method used to perform the requested behavior.

It is possible for different classes of objects to implement the same interface -- they can do the same things and interact with other objects the same way -- even if, behind the scenes, they might contain different machinery which requires different internal actions. So, as Java programmers, we'll implement both interface specifications and class specifications.

As an aside, if only one class of objects implements an interface, Java doesn't require that the interface be defined separately, instead, it can infer the interface from the class specification. But, for now, we will write interface specifications, even when they aren't strictly necessary.

Our First Real Java: Beginning An Interface Specification

Let's begin by describing a calculator's interface. The framework for an interface specification is below:

  interface CalculatorInterface {
  }
  

Notice the first word, interface. This word is a reserved word, a.k.a, a keyword. This means that it has a special meaning to Java. It identifies the construct as an interface specification. Anytime Java sees the word "interface", it expects to find a properly formatted interface specification. Java won't use "interface" for any other purpose. Thus you, as a programmer, can't use "interface" and expect Java to treat it as anything but a signal for an interface specification.

Notice that, immediately after the keyword "interface", is the word "CalculatorInterface". "CalculatorInterface" isn't a reserved word. Instead, it is just an identifier that we are assigning to this particular class specification. An identifier is nothing more than a name or label that we, as the programmer, assign.

In fact, the identifier that we assign doesn't have to be a word at all. It can be anything we choose to assign, such as a word, an abbreviation, or a collection of words. There are however, some rules: It can contain numerals, but shouldn't begin with a numeral and it can't contain spaces.

It can be capitalized however we'd like, but Java is case sensitive. So, although we can use whatever capitalization we'd like when we define the interface, we need to use exactly the same capitalization any time we refer to the interface using this name. ie: Java would recognize "Widget" and "wiDGet" as two different names, even though widget is spelled the same, because the capitalization is different.

Having said that, there are stylistic conventions that are often followed by Java programmers. These are rules that aren't enforced by the Java enviornment -- but make it easier for humans to read programs. One of these guidlines is that the names of interfaces should capitalize only the first letter of each word. They should also be descriptive. This is an interface for a calculator so we call it a CalculatorInterface. As an example, "WidgetController" is a "good" name (assuming we're dealing with widgets...not to be confused with gidgets or gadgets), whereas most would consider names such as "widgetcontroller" or "WIDGETCONTROLLER", although perfectly functional, to be "bad" names. Following convention also makes the code easier to understand. "TOGETHER" could be either "Together" or "ToGetHer" how is a reader supposed to decide between the two?

The next thing you'll notice are two "squiggly brackets": "{" and "}". When you see "{" think "Begin". And, when you see "}", think "End." We often call the part of a program that lies in between the {-begin and }-end the body.

In the case of an interface specification, such as the one we are constructing, the body will contain the list of messages that instances of implementing classes can accept.

Developing the Body

Two of the most basic behaviors of a calculator are addition and subtraction. Let's define messages called "add" and "subtract" to request these behaviors. To begin, let's list these messages within the body, as shown below.

  interface CalculatorInterface {
    add; 
    subtract;
  }
  

Notice that, in the above example, we listed each message name on a different line. This is considered good style. Notice also that each of these definitions ends with a ";". This is required. We end sentences with periods "." and Java ends lines with ";". As a rule, Java constructs have two general forms:

But, What to Add?

We're getting there, but what we have doesn't quite define enough to make things work. "Add!" "Subtract!" These statements beg the question, "Add what to what?" And, "Subtract what from what?" Clearly we need to tell Java that these methods require two paramters, a.k.a, arguments -- the two things that should be added:

  interface CalculatorInterface {
    add(leftNumber, rightNumber); 
    subtract(leftNumber, rightNumber);
  }
  

If we look at the example above, we see another part of the definition of each of these messages -- the formal argument list. The formal argument list is a comma-separated list of each parameter contained within ()s. It comes after the name of each method, and before the ;.

Even messages which don't contain any arguments must contain a formal argument list. For these methods, the ()s must exist, but nothing will come in between. For example, if our calculator also happens to have a clear button, that wouldn't require any paramters. Of course, for this to make sense, we'd probably also need to keep the get the value of the last operation -- or else, we wouldn't have anything to clear. So, let's add these two new messages to our interface specification. Notice that the formal argument list, as required by Java, is present -- but empty:

  interface CalculatorInterface {
    clear();
    getLastResult();
    add(leftNumber, rightNumber); 
    subtract(leftNumber, rightNumber);
  }
  

Note the stlyistic convention used with "getLastResult", "leftNumber", and "rightNumber" in these cases they are each an identifier and the convention used with identifiers is to capitalize the first letter of each word except for the first word. (Once again this is just convention...not enforced by Java. But it does make your code easier to read and understand and we will point out when you break this convention)

A note on the naming of the "add" and "subtract" parameters (the formal argument list) "leftNumber" and "rightNumber" are also identifiers. They follow the Java capitalizing convention, and as before, they are just names we give to each of the paramters. Notice that I used the same namesfor each of the two definitions. This is just a coincidence. I could have used the same names or different names for each.

This is becase the scope of each identifier is limited to the message defintion in which it appears. In other words, the "leftNumber" in add() is different than the "leftNumber" in subtract. The names must be different within the same argument list, in order that the paramters can be properly identified. However, among diferent argument lists names can be coincidentally the same among different argument lists without causing confusion.

An identifier's "scope" includes the parts of a program in which it is valid. In other words, if we select a particular identifier's definition within a program, its scope includes the parts of the program where that particular definition is known. An identifier defintion is "in scope" in a part of the program if that particular definition is used by Java within that particular part of the program. It is possible that the same name is used in different identifier definitions, each of which has a different scope. This is, for example, the case with "leftNumber" and "rightNumber".

There are two different definitions of each -- one for each of add and subtract. These are completely independent. What is being added doesn't necessarily have enything to do with what is being added, even if each is adding something called "leftNumber" to something called "rightNumber".

But, What to Add? (Part II)

Okay. Let's try this. Humans can, at least for small numbers, play the part of a calculator, right? We can add. See, watch me add:

Now, I'd like you to try. Please do the following:

Did those requests make sense? What is Kesden plus three? Or "Dog" plus "Cat". Our calculator can't add anything -- just numbers. So, we need to be a little more specific in our definition. We need to let Java know that only numbers can be added. We do this by specifying a type for each identifier, as follows:

  interface CalculatorInterface {
    clear();
    getLastResult();
    add(int leftNumber, int rightNumber); 
    subtract(int leftNumber, int rightNumber);
  }
  

Notice the keyword "int" in the definitions above. It specifies that the type of each of the identifiers will be an integer. Recall from high school math that the set of integers is the set of numbers without fractions, e.g. -5, -3, 0 1, 100, &c. Recall that, in Java, an "int" is a type that can represent this type of "no fractions" number.

Primitives vs. Objects

Until today, we mostly discussed one type of thing in our universe, the primitive. It can in typed flavors, such as int, float, double, char, boolean, and long. But, basically, it was a value that could be read or written. We only briefly considered Objects, by taking a glancing look at the richness of Strings by comparison.

Primitives are basically like pieces of notebook paper, a whiteboard, an audio CD, or DVD. We can write to them and read from them. They are just named places to store things.

Objects are living models. They have properties, defined by primitives, or Objects containing them. But, more importantly, we can ask them to do things, to exhibit behaviors, and they can retain state between actions.

Revising The Formal Argument List

So, as we originally defined it below, our calculator can add "2 + 2", but not "2.5 + 2.5". This is because the messages take "int" paramters, not parameters of the "float" or "double" type:

  interface CalculatorInterface {
    clear();
    getLastResult();
    add(int leftNumber, int rightNumber); 
    subtract(int leftNumber, int rightNumber);
  }
  

Let's fix that, so we can add and subtract with fractions:

  interface CalculatorInterface {
    clear();
    getLastResult();
    add(float leftNumber, float rightNumber); 
    subtract(float leftNumber, float rightNumber);
  }
  

Now, we can send the object two numbers, including fractions (but in decimal form), along with our request to add or subtract.

Return Types

We're getting much closer -- but we're not quite there, yet. We've now defined the message that we send to the object -- but what does it send back? Each of these methods sends back another "float". The clear method sends back nothing at all -- it just returns when it is done. This is specified using the Java keyword "void".

So let's specify these return types. Notice that the return specification comes before the name of each message name:

  interface CalculatorInterface {
    void clear();
    float getLastResult();
    float add(float leftNumber, float rightNumber); 
    float subtract(float leftNumber, float rightNumber);
  }
  

At this point, we have a complete interface sepcification.

A Class Specification

We're going to talk a lot more about class specifications next week. The focus of today's lecture is really intended to be interface specification. But, I do want you to see the correspondence between the two. So, here's one class specification that implements the interface we just described:

  class SimpleCalculator implements CalculatorInterface {

    private float result;

    public SimpleCalculator {
       result = 0;
    }

    public void clear() {
      result = 0; 

      return;
    }

    public float getLastResult() {
      return result;
    } 

    public float add(float leftNumber, float rightNumber) {
      result = leftNumber + rightNumber;
      return result;
    }

    public float subtract(float leftNumber, float rightNumber) {
      result = leftNumber - rightNumber;
      return result;
    }
  }
  

Notice the Java keyword, "class". Much as "interface" indicates that an interface specification follows, the reserved word "class" indictes that a class specification follows. Notice the kyeword "implements" (another reserved word) followed by the identifier "CalculatorInterface". This let's Java know that this SimpleCalculator class follows the message protocol specified in the CalculatorInterface interface we defined earlier.

Note the creation of an instance variable in the line


  private int result;
  
Each time an instace of the class "SimpleCalculator" is formed it will have it's own copy of the variable "result".

The next item of interest is probably the lines

    public SimpleCalculator {
	result = 0;
    }
  
Huh? Where did that come from? We didn't metion it in the class specification. This statement is called the constructor and is called each time an instance of the class SimpleCalculator is formed. For this class it merely initalizes or sets "result" equal to zero. So that when getLastResult() is called before performing addition or subtraction 0 will be returned instead of Bill Bob or Joe.

Notice that, instead of ending with a ;, each method specification now contains a body beginning and ending with {}s. And, also notice that in between the { and the } is the body of the method specification that describes the actions the message will take when the message is received. Even though you don't yet know Java, you can probably understand the addition, subtraction, and assignment operations.

"return" is another reserved word. It indicates that everything is now done and the reply should be sent to the requestor. If anything is to be sent back, such as the value of "result", this is indicated by the return statement. Since the return ends the method, nothing should appear after the return and the end of the method.

What's this "public" and "private" stuff?

A "public" method is one that can be called from outside of the class. A "private" method is one that can only be called from within the class. We'll worry more about this later. But, just as a quick example, let's try this.

I can ask you to eat. This is a public behavior. Your will insert food into your mouth, chew, swallow, mix the food with acid and stir, push it into your intensines. Let's stop there.

So. Digest! Excrete insuline! I can't ask you to do these things. These are certainly things you can do -- but you control them. I can ask you to eat -- and you manage the process from there.

"public" methods are those that can be requested from other classes. "private" methods are those than can only be requested from within the same class. As a result, we often call private methods helper methods. They are often used to decompose complex behaviors into simpler, easier to implement ones. This will make more sense to you a little later on -- so don't worry.

Similarly, attributes, such as the "result" variable can be public or private. A variable is just a property that can change. You'll notice that the "result" is decalred as a private variable. This means that it can only be accessed by the methods from within the class -- it can't be grabbed from the outside. A public variable could actually be viewed or changed from other classes.

For the pedantic, you'll notice that I said that private things are private to the "class", not the "object". As strange as this may seem, this is actually what I meant. But, don't worry about this right now. It is a real subtlty -- we've got bigger, more important fish to fry right now. We'll come back to this -- I promise.

For now, when you see public, just think "eat" and when you see private, think, "digest", "excrete insuline", &c.

One Last Detail

Since only public methods can be invoked from the outside, only public methods can be part of the interface. As a result, all methods of an interface are presumed to be public. But, it is considered good style to specifically define this anyway. So, below is the final form of the interface specification:

  interface CalculatorInterface {
    public void clear();
    public float getLastResult();
    public float add(float leftNumber, float rightNumber); 
    public float subtract(float leftNumber, float rightNumber);
  }
  

A Bit of Nomenclature

Notice that the the definition of each message within the interface specification looks just like the definition of the implementing method within the class specification. The only difference is that the class specification contains a body whereas the interface specification does not.

As a result, the description of a message within the interface specification is known as a method signature. Much as my signature identifies me, the method signature identifies the method -- without specifying how it works.

Although different classes can contain methods with the same signature, no two classes can contain two methods with the same signature.

Actually, to be a bit more rigorous, the method signature actually doesn't include the return type. For example, the method signature is actually "add (float leftNumber, float rightNumber);", not "float add (float leftNumber, float rightNumber);" And, for reasons related to the way programs are translated into code, Java doesn't allow two methods within the same class to have the same name and parameter types and order -- even if they differ in return type.

So, again, no two methods within the same class or interface can have the same signature, in other words, the same name and parameterization.

A Quick Review

By defining an interface specification, we are defining the exact forms of messages that can be sent to an object that implements that interface and the values, if any, that will be returned after the requested behaviors are completed. In doing so, we are defining the protocol to be used when communicating with and implementing classes of objects.

The class specification defines exactly what operations should take place to implement behaviors. These behaviors might be defined in a free-standing interface specification or infered from the class definition, itself.

Java defines primitive -- things which can be written to and read, but which don't implement thier own behaviors and don't have attributes other than their own value. These primitives do come in different types, each of which is restricted to a certain valid collection of values.

Access specifiers, such as "public" and "private" can specify the ability of one class of objects to access propeirties and behaviors of other classes of objects.

The method signature defines the format of a message that can be sent to a class of object. No two methods within the same class of object can have the same signature.