27 A Complete Program
The earlier chapters introduced individual ideas one at a time. This chapter puts them together in one small but complete program.
The goal is not to build something large. It is to show the full arc of development:
- state the problem clearly
- choose the data model
- write small routines with contracts
- test the result
The example will be a small task manager that stores tasks, marks them complete, and reports progress. By the end of the chapter, every major idea from the book will have appeared in one place: classes, queries, commands, contracts, and tests.
27.1 The Problem
We want a program that can:
- create tasks with a title
- mark a task complete
- report how many tasks are done
- report whether all tasks are complete
Even in a small program like this, good design matters. We do not begin by typing methods at random. We begin by identifying the concepts in the problem itself.
The obvious concepts are:
- a
Todo_Item - a
Task_List
27.2 The First Class: Todo_Item
A task needs:
- a title
- a completion flag
Here is the class:
nex> class Todo_Item
create
make(t: String) do
title := t
done := false
end
feature
title: String
done: Boolean
mark_done()
do
done := true
ensure
now_done: done
end
is_done(): Boolean do
result := done
end
invariant
title_not_empty: title.length > 0
end
The invariant says a task must have a non-empty title. The method mark_done has a simple postcondition. Already the class has a clear meaning.
27.3 The Second Class: Task_List
Now we need a collection of tasks and a few operations over it.
nex> class Task_List
create
make() do
tasks := []
end
feature
tasks: Array[Todo_Item]
add_task(title: String)
require
title_not_empty: title.length > 0
do
tasks.add(create Todo_Item.make(title))
ensure
added_title_visible: tasks.get(tasks.length - 1).title = title
end
task_at(index: Integer): Todo_Item
require
index_in_range: index >= 0 and index < tasks.length
do
result := tasks.get(index)
end
size(): Integer do
result := tasks.length
end
invariant
storage_exists: tasks /= nil
end
This is enough to create and access tasks, but not yet enough to report progress.
27.4 Queries That Summarize State
Add routines that count completed tasks and decide whether all tasks are complete:
nex> class Task_List
create
make() do
tasks := []
end
feature
tasks: Array[Todo_Item]
add_task(title: String)
require
title_not_empty: title.length > 0
do
tasks.add(create Todo_Item.make(title))
ensure
added_title_visible: tasks.get(tasks.length - 1).title = title
end
task_at(index: Integer): Todo_Item
require
index_in_range: index >= 0 and index < tasks.length
do
result := tasks.get(index)
end
completed_count(): Integer
do
result := 0
across tasks as task do
if task.is_done() then
result := result + 1
end
end
ensure
count_in_range: result >= 0 and result <= tasks.length
end
all_done(): Boolean do
result := completed_count = tasks.length
end
size(): Integer do
result := tasks.length
end
invariant
storage_exists: tasks /= nil
end
Notice the style:
- commands change state
- queries summarize state
- contracts explain routine boundaries
27.5 A First Manual Run
nex> let todo := create Task_List.make
nex> todo.add_task("write chapter draft")
nex> todo.add_task("check examples")
nex> todo.add_task("revise wording")
nex> todo.size
3
nex> todo.completed_count
0
nex> todo.task_at(0).mark_done()
nex> todo.task_at(1).mark_done()
nex> todo.completed_count
2
nex> todo.all_done
false
The program already works. But we can still improve the interface.
27.6 Raising the Level of the Interface
The call task_at(i).mark_done() now works correctly. Still, a list class can offer a better public routine. Marking a task done is an operation in the vocabulary of the problem, not just a low-level storage update. A dedicated command also gives one clear place for contracts and future changes:
nex> mark_task_done(index: Integer)
require
index_in_range: index >= 0 and index < tasks.length
do
task_at(index).mark_done()
ensure
selected_done: tasks.get(index).done
end
Add it to Task_List:
nex> class Task_List
create
make() do
tasks := []
end
feature
tasks: Array[Todo_Item]
add_task(title: String)
require
title_not_empty: title.length > 0
do
tasks.add(create Todo_Item.make(title))
ensure
added_title_visible: tasks.get(tasks.length - 1).title = title
end
task_at(index: Integer): Todo_Item
require
index_in_range: index >= 0 and index < tasks.length
do
result := tasks.get(index)
end
mark_task_done(index: Integer)
require
index_in_range: index >= 0 and index < tasks.length
do
task_at(index).mark_done()
ensure
selected_done: tasks.get(index).done
end
completed_count(): Integer
do
result := 0
across tasks as task do
if task.is_done() then
result := result + 1
end
end
ensure
count_in_range: result >= 0 and result <= tasks.length
end
all_done(): Boolean do
result := completed_count = tasks.length
end
size(): Integer do
result := tasks.length
end
invariant
storage_exists: tasks /= nil
end
The class now offers a cleaner interface.
27.7 Tests for the Program
Before considering the program finished, write tests.
nex> function test_task_list()
do
let todo := create Task_List.make
todo.add_task("one")
todo.add_task("two")
todo.add_task("three")
if todo.size /= 3 then
raise "test failed: size after add"
end
if todo.completed_count /= 0 then
raise "test failed: initial completed count"
end
todo.mark_task_done(1)
if todo.completed_count /= 1 then
raise "test failed: completed count after mark"
end
if todo.all_done then
raise "test failed: all_done too early"
end
todo.mark_task_done(0)
todo.mark_task_done(2)
if not todo.all_done then
raise "test failed: all_done should be true"
end
print("task list tests passed")
end
The tests exercise:
- creation
- adding tasks
- marking completion
- summary queries
That is enough to give confidence in a small program.
27.8 What the Example Shows
The finished program is not complicated, but it demonstrates the whole method of development:
Start from concepts.
Todo_Item and Task_List emerged from the problem statement.
Use contracts to shape the interface.
Invalid indices and empty titles became preconditions. Important effects became postconditions.
Use invariants to capture class meaning.
Tasks always have non-empty titles.
Prefer higher-level routines.
mark_task_done is a better public routine than exposing raw storage details to callers.
Test the finished behavior.
The tests operate through the public interface, which is exactly how clients will use the program.
27.9 Summary
- A complete program should be developed from concepts, not from isolated code fragments
- Small classes with clear responsibilities make the rest of the design easier
- Contracts help turn vague intentions into precise interfaces
- Good public routines hide representation details
- A few focused tests can validate the full flow of a small program
- The same process scales upward: problem, model, contract, implementation, test
27.10 Exercises
1. Extend Todo_Item with a description: ?String field and a routine for setting it. Decide whether an invariant is needed.
2. Add a routine remaining_count(): Integer to Task_List and write a postcondition for it.
3. Prevent a task from being marked done twice by strengthening the interface. Decide whether this should be a precondition or simply an idempotent operation.
4. Split the chapter’s program into two or three files using intern.
5.* Replace the task manager with a different complete miniature program of your own, such as a library checkout tracker or a grade book, and follow the same process from problem statement through tests.