InformIT

Becoming a Software Developer, Part 6: Design and Programming

By

Date: May 17, 2002

Article is provided courtesy of Addison Wesley.

Return to the article

The step between having a design idea and the "simple matter" of programming that idea is a massive one. Understanding why programming is not easy is a first step toward mastering the craft of programming.

Introduction

All too often I hear people dismiss the complexity of software development by claiming that something is just a simple matter of programming. My normal response is that if the speaker thinks it's going to be simple, then he can write it himself.

Even a seemingly trivial application can be amazingly complex when you have to address all the weird and wonderful things that can go wrong. Even a simple thing like sending out email event notifications can have hidden complexities, as we saw in Part 5 of this series, "Creating Acceptance Tests from Use Cases." If the requirements for even a simple club membership system can be quite complex, imagine how much complexity is hidden in larger commercial applications.

All Software Is Complex

Once we get beyond the typical "toy" teaching example that's less than 100 lines long, understanding the entire application takes time and effort. Individually, each line of code is nearly always simple and easy to understand. Yes, sometimes you have to Read That Fine Manual (RTFM), as was the case with the collect! idiom in Part 1 of this series, "Ruby for the Nuby."

The complexity in an application lies more in the interactions between the effects of each line of code rather than in the individual lines themselves, which are more or less easy to comprehend. The complexity arises because the data on which one line of code operates may have been changed by any number of lines of code that preceded the one you're looking at.

Object-oriented programming languages such as Ruby reduce this complexity somewhat by encapsulating all data inside objects, so the volume of code that can directly affect any data is reduced. Whenever the data within an object is changed, you know that it had to have been changed by one of the object's methods. This drastically decreases the complexity of the task facing us as developers when we want to understand what some code is doing.

The complexity is further reduced by the fact that with objects, most programmers try to give the methods intention-revealing names, while keeping the methods themselves relatively short. This means that it's relatively safe to infer from the name of a method what is happening. Ruby takes this even further by allowing trailing question marks (?) and exclamation points (!) in method names. Query method names conventionally end in ? (Array.empty?) and methods that can drastically affect the object conventionally end in ! (Array.sort!).

One hidden source of complexity in understanding software is that even with encapsulated data, objects can still be modified by simply invoking one of their methods. To get around this type of complexity, most developers adopt the convention that an object should only send messages to closely related objects, and frown on any object that's globally accessible (and hence could be accessed from anywhere). Ruby supports this convention by explicitly prefixing all globally accessible variables with $, making it immediately obvious when this style rule is being broken.

Controlling Complexity Though Design

When you have the task of designing a new application, you need to consider all of the things that help make existing software more understandable. After all, once you've created the application, sooner or later you have to revisit the code to add new features or change existing ones. At that point, you'll find out whether you were successful in creating understandable, maintainable code.

To create maintainable software, you really need the help that objects provide. You need to make sure that you design the interactions between the objects in a way that makes the resulting code easy to read and understand. After all, you never want your teammates to hear you cry, "Who wrote this piece of garbage?! I can't make sense of it at all! Oh...it was me..."

Classes Represent Concepts

The first thing to remember when designing your application is to use classes to represent important concepts. How do you know what the important concepts are for your application? Simple. Look at your requirements, use cases, and test cases. The concepts that show up in those are in all probability going to become classes in your application.

For the Running Club Membership system discussed in the last article, here are some candidate classes:

Class

Description

Member

Represents a club member

Club

Represents the club to which the Member belongs (this is a very easy concept to miss, believe it or not)

ClubEvent

The special events that the club secretary wants to tell the members about

Race

A special kind of ClubEvent, against which the members report RaceResults

RaceResult

Represents a member's time and placement in a particular Race


Assigning Responsibilities to Classes

Once you have some ideas for the candidate classes, your next design task is to work through one use case and associated acceptance test cases (see Part 5 of this series, "Creating Acceptance Tests from Use Cases") to assign responsibilities for delivering the various subfunction goals to classes within the application. When initially assigning responsibilities, try to group related responsibilities together in the same class. Don't worry if you identify a responsibility that doesn't fit any of your candidate classes; it just means that you have to invent a new class for that particular responsibility.

As you work through your design ideas, you'll inevitably learn about how your design works, and in the process realign the responsibilities. Indeed, you might find it beneficial to come up with three different initial designs to explore different options. If you do this, you might even discover that the first idea you came up with is not actually the best option.

When looking through the use cases to identify the responsibilities, it's easiest to just read through the main success scenario first. Defer looking through the extensions for responsibilities until after you feel you've correctly assigned the main responsibilities.

For the use case Club Secretary : Notify members about special events, here's one possible assignment of responsibilities:

In this design option, the ClubEvent is a data holder object without any really interesting behavioral responsibilities. Another design option would be to have the ClubEvent responsible for emailing itself.

The Craft of Programming

Becoming a good programmer requires that you develop a feel for the aesthetics of code. It's not enough that the code sort of works; we need code that's beautiful as well as functional. After all, as a developer you're going to spend a lot of every working day working with code in one way or another, and it's a drag having to work with ugly things all the time. Much better to make the code pleasing to the eye, because it's amazing how long software lasts. Ten years from now, you don't want to be looking at some of your code and cringing because of how ugly it looks.

Converting from design ideas to source code is not a mechanical process. Yes, initially you can just mechanically translate the design ideas, but once you've done that you have to read the code and adjust it until it feels right. This process of making it feel right is called refactoring, and is a process of identifying and removing code "smells"—as in "That code stinks, we need to clean it up."

Responsibilities Are Implemented as Methods

Whatever an object is responsible for knowing is likely to be an attribute of the class, and as such will need a set of accessor methods. These are represented in Ruby as follows:

class Member
 attr_accessor :name, :phone_number, :email_address
end

Responsibilities that involve the object answering a question are usually implemented as query methods, and the method name will have a trailing ? if the method returns a Boolean true/false answer.

class Member
 def matches? ( search_criteria )
  # to be implemented using test driven development
  return false
 end
end

Normal behavioral responsibilities are usually implemented as methods that return whatever is appropriate. Give a method name a trailing ! only if the method can have drastic consequences.

class Member
 def notified ( club_event )
  # to be implemented using test driven development
 end
 def already_notified? ( club_event )
  # to be implemented using test driven development
  return false
 end
end

For symmetry, a query method (already_notified?) has been added in this example to make it easier to ensure that duplicates are never sent.

Allow Test-Driven Development To Improve Your Design

While it's extremely tempting to jump from the design ideas into writing the methods, that's normally a mistake. It's much better to pause, think of how you're going to unit test the methods, and then write the test first. When you write the test, you'll probably realize that the original name you were going to give the method doesn't really look right when written down. Whenever that happens, pick a better name and use that one instead of your original design.

The next article in this series will show how the email notifications part of the club membership system can be implemented in Ruby using test-driven development. In doing so, it will uncover yet more complexities in what initially looked like a really simple application.

800 East 96th Street, Indianapolis, Indiana 46240