Actors and Boxes

Most people think computer programming is about making computers do something. And it is that, but that’s not the important part. The important part, the real challenge in writing software, is task management.

Suppose you want to build a new room on your house. That is your task — “Add a room”. But to actually accomplish it, you need to break that down into smaller tasks. Maybe things like “take some measurements”, “get some lumber”, “drive some nails”, “paint the walls”, and so on. Then each of those will get broken into smaller tasks, like “cut a 2×4 to 94 inches”, “Align 2×4 with mark on east wall”, “Drive nails into 2×4”, and so on.

And so it goes. You have a high level task that is the goal. You break that down into sub-tasks with smaller goals, and break those down further until you get to small, simple tasks that you can actually accomplish. This is the core of software development.

Objects as Black Boxes

There are a few different ways to approach this paradigm, but the two most common software development methods are “procedural” and “object oriented”. A procedural approach will take the sub-tasks, put them in order, and do them. For example:

VB

Public Sub AddARoom()
    Dim width = MeasureWidth()  ' Dimensions are in inches
    Dim height = MeasureHeight()
    Dim length = MeasureLength()
    Dim num2x4s = (((width / 16) + 1) * 2) + (((length / 16) + 1) * 2)
    Dim haveLumber = BuyLumber(num2x4s)
    Dim haveNails = BuyNails(8 * num2x4s)
    If haveLumber And haveNails Then
        BuildWalls()
        ' and so on ....
    End If
End Sub

Java

public void AddARoom() {
    int width = measureWidth();
    int height = measureHeight();
    int length = measureLength();
    int num2x4s = (((width / 16) + 1) * 2) + (((length / 16) + 1) * 2);
    boolean haveLumber = buyLumber(num2x4s);
    boolean haveNails = buyNails(8 * num2x4s);
    if haveLumber && haveNails{
        buildWalls();
        // and so on
    }
}

The procedural approach is “top down” — you start at the top of the program and work your way down to the bottom. It’s a laundry list of instructions that you check off one by one until you’ve finished the task.

Object oriented programming takes a different approach. Instead of thinking in terms of tasks, it thinks in terms of objects. It will still have tasks, of course, but those tasks are part of the objects. That’s what an object is — a representation of some “thing”, which contains data and methods characteristic of that “thing”. An object oriented approach to the Add A Room task might be more like this:

VB

Class Room
    Public Integer Width
    Public Integer Height
    Public Integer Length

    Public Sub new(wide as Integer, high as Integer, howlong as Integer)
        Width = wide
        Height = high
        Length = howlong
    End Sub

    Public Function calculate2x4s() as Integer
       return (((Width / 16) + 1) * 2) + (((Length / 16) + 1) * 2)
    End Function

    Public function calculateNails() as Integer
        return calculate2x4s() * 8
    End Function
End Class

Class SupplyStore
    Public Function buyLumber(num2x4s as integer) as boolean
       ...
    End Function
    Public Function buyNails(numNails as integer) as boolean
       ...
    End Function
End Cass

Class Builder
    Public Function buildWalls(theroom as Room) as boolean
        ...
    End Function
End Class

' ... elsewhere...

Public Sub AddARoom()
    Dim newRoom as Room = new Room(MeasureWidth(), MeasureHeight(), MeasureLength())
    Dim store as SupplyStore = new SupplyStore()
    Dim haveLumber = store.buyLumber(newRoom.calculate2x4s())
    Dim haveNails = store.buyNails(newRoom.calculateNails())
    If haveLumber And haveNails:
        Dim mybuilder as Builder = new Builder()
        mybuilder.BuildWalls()
        ' and so on ....
    End If
End Sub

Java

class Room {
    int width;
    int height;
    int length;

    public Room(int wide, int high as Integer, int howlong) {
        width = wide;
        height = high;
        length = long;
    }

    public int calculate2x4s() {
       return (((width / 16) + 1) * 2) + (((length / 16) + 1) * 2);
    }

    public int calculateNails() {
        return calculate2x4s() * 8;
    }
}

class SupplyStore {
    public boolean buyLumber(num2x4s as integer) {
       ...
    }
    public boolean buyNails(numNails as integer) {
       ...
    }
}

class Builder {
    public boolean buildRoom(Room theroom) {
        ...
    }
}

// elsewhere...
public void addARoom()
    Room newRoom = new Room(measureWidth(), measureHeight(), measureLength());
    SupplyStore store = new SupplyStore();
    boolean haveLumber = store.buyLumber(newRoom.calculate2x4s());
    boolean haveNails = store.buyNails(newRoom.calculateNails());
    if haveLumber && haveNails{
        Builder myBuilder = new Builder();
        myBuilder.BuildWalls();
        // and so on ....
    }
}

While the difference in these approaches may look mostly semantic at first glance, there are actually very important design differences.

In the procedural approach, the AddARoom method is doing task management. It is keeping track of dimensions, it knows how to figure out the number of boards and nails needed, and other related housekeeping. It has to micro-manage. In the object-oriented approach, the AddARoom method only manages a few objects and doesn’t need to micro-manage them. It has a room object, and that room knows how to figure out how many boards and nails are needed. It has a store object that handles purchasing supplies. And it has a builder object that knows how to build the room.

One of the advantages of the object oriented approach is that each of the objects is a self-contained unit. The application doesn’t need to concern itself with the details that go on inside the object. When you drive a car, you don’t need to understand compression ratios, air-to-fuel mixtures, ignition timings, or valve dwell. You just turn the key, knowing there’s a motor under the hood that takes care of all those mechanical things for you. It’s the same with objects. You don’t care how a room knows the number of boards it needs, only that it does. You don’t need too worry about what goes on when you tell a SupplyStore object that you need some nails, only whether or not it delivered the nails. This simplifies your application. It also makes maintenance simpler. Suppose for example that your city planning department decrees that new rooms should have boards on 12 inch centers rather than 16. A change to the calculation function in the Room object is all that is needed. Every application that uses the room object will automatically pick up the change, without needing to change a single line of the main application code.

The fact that the objects are self-contained also gives you a big advantage in that it makes those objects testable all by themselves. If you want to check that a room calculates the correct numbers for boards and nails, for example, you can create a unit test that checks against a known value. If a certain room dimension actually needs 100 boards, you can feed those dimensions to an object and check whether it returns 100 for the number of boards. This is valuable because it proves your assumptions about how the object operates. If the object gets changed at some point and returns 103 instead of 100, your unit test will fail and alert you that something is wrong — either a bug in the object itself, or an error in your expectations of what it will return.

Objects as Actors

It is typical for a procedural program to behave as a single sequence of actions. It is like a one-person company. If that person is you, you go and measure the area you want to put a room. Then you figure out the supplies you need. Then you go to the store and get the materials. Then you start sawing boards and swinging hammers. In this scenario, nothing gets done unless you do it. You are the sole actor.

In the object oriented approach, each object may be an actor. It is like you hired a bunch of employees. When you want to build the room, you tell Fred to go measure the area. You then give those dimensions to Joe, who calculates the dimensions and materials needed. You then send Jane to the store to pick up supplies. And finally you tell Bob (the builder) to begin construction. Each object is an actor that performs a part of the overall task. All you did was manage who did what when.

In the simple scenario we’ve described so far, there may not seem to be much advantage. But when you break out of thinking in a single sequence into the possibility of multiple things happening at once, the advantages become significant. Suppose you want the room to have windows and carpeting. In the procedural example, you’re probably going to do all the steps we’ve listed, then after the walls are framed, measure for the windows, go buy them, come and install them. Then you’ll calculate how much carpet you need, go buy it, and come put it in. Then you’ll need to calculate the roof area, go buy more lumber, and so on. If you’ve ever seen a house being built, you know this isn’t how it works. (Well, sometimes it is, but most often it isn’t).

In the real world, things happen in parallel. You can have one guy hammering nails while another guy is measuring for carpet. You can have one guy laying roofing while another guy is installing windows. And you can have someone installing carpet while someone else is painting the exterior. An object oriented approach to software design makes it easier to replicate this scenario.

For example, with a few changes to your classes, your code might look something like this (class definitions omitted, you should get the idea. Explanation follows):

Java

class RoomManager {
    private SupplyStore store;
    private Builder builder;
    private Roofer roofer;
    private WindowInstaller windowGuy;
    private CarpetInstaller carpetGuy;
    private Room room;
    private Map<String, boolean> steps;
    private int problems;
    
    public RoomManager(int width, int height, int length) {
        store = new SupplyStore();
        builder = new Builder();
        roofer = new Roofer();
        windowGuy = new WindowInstaller();
        carpetGuy = new CarpetInstaller();
        room = new Room(width, height, length);
        steps = new Hashmap<String, boolean>();
        steps.put("walls", false);
        steps.put("roofing", false);
        steps.put("windows", false);
        steps.put("carpet", false);
    }

    public boolean isFinished() {
        boolean allDone = true;
        for(Map.Entry<String, boolean> entry : steps.entrySet())
            if(entry.getValue() == false) {
                allDone = false;
                break;
            }
        return allDone;
    }

    public void AddThisRoom() {
        roomIsFinished = false;
        problems = 0;
        Map<String, int> shoppingList = new HashMap<String, int>();
        shoppingList.put("lumber", room.calculate2x4s());
        shoppingList.put("nails", room.calculateNails());
        shoppingList.put("roofing", room.calculateRoofing());
        store.buy(shoppingList, this::lumberDelivered);
        while(!roomIsFinished) {
            goHaveCoffeeAndDonuts();
            if (problems > 0)
                handleProblems();
        }
    }

    private void lumberDelivered() {
        Map<String, int> shoppingList = new HashMap<String, int>();
        shoppingList.put("windows", room.calculateWindows());
        store.buy(shoppingList, this::windowsDelivered);
        builder.frameWalls(this::finishedWithWalls);
    }

    private void windowsDelivered() {
        windowGuy.installWindows(this:finishedWithWindows);
    }

    private void carpetDelivered() {
        carpetGuy.installCarpeting(this::finishedWithCarpet);
    }
   
    private void finishedWithWalls() {
        steps.put("walls", true);
        roofer.buildRoof(this::finishedWithRoof);
    }

    private void finishedWithWindows() {
        steps.put("windows", true);
    }

    private void finishedWithRoof() {
        steps.put("roofing", true);
        Map<String, int> shoppingList = new HashMap<String, int>();
        shoppingList.put("carpet", room.calculateCarpeting());
        store.buy(shoppingList, this::carpetDelivered);
    }

    private void finishedWithCarpet() {
        steps.put("carpet", true);
    }
}

For the moment, assume that each of the actors involved (roofer, builder, etc) will put themselves on a background thread and run in the background. They are each given a method to call when they get done. This is equivalent to you telling the store guy to deliver a load of lumber and call you when he gets there and gets it off the truck. You’re just directing objects. And they are all free to operate on their own schedules while you go have coffee and donuts. The control flow goes like this:

  1. You make a shopping list with details from the room object, and hand it to the store object.
  2. When the store delivers the materials, they call and interrupt your donuts. You order the windows and tell the builder to frame the walls.
  3. While the walls are built, the windows are delivered. When they get there, you get another call, and you then tell the window guy to go install the windows. (Note that you probably need a check to make sure the walls are in before you put in the windows, but that’s omitted for the sake of brevity here.)
  4. When the framer gets done, he calls you and you tell the roofer to get busy while you go back for more coffee.
  5. When the roofer finishes, he buzzes you, and you order the carpet, still munching on donuts.
  6. When the carpet is delivered, you tell the carpet guy to go to work.
  7. After every few donuts, you check to see if everything is done yet. If not, you check for any problems and handle them, then go back to waiting.

This is obviously simplified but you should get the idea. You (the application) don’t need to know or care how each of the actors (objects) involved does their job, only that they do it, and let you know when they’re done. You don’t care if the windows go in before the carpet, or if the roof is done before the windows are in. And for those things where order does matter (for example, you don’t want carpet installed before the roof is done), you handle those in the methods that handle the various tasks getting completed. All you really have to do is make sure all the steps actually get done, and you can come back and lay on the new carpeted floor and snooze from the sugar coma you put yourself into with all those donuts.

An added advantage of this approach is it unhooks the steps from the process. If you want to add a patio, or some hedges, or whatever, all you do is add calls to some additional objects, add them to the list of steps, and make sure they get checked off when they’re done. No need to worry about the details of each task. Your application stays nice and tidy while you let the actors do their jobs.

This entry was posted in Fundamentals, Programming. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *


*