Small and Simple Web Applications - the Friki Way (Part 3)
Abstract
This article is the third of a series which laments the bloated and
unmaintainable state of so many J2EE web applications and looks at ways to keep
web applications small, simple and flexible. The series uses the author's Friki software as a case study, and
discusses ways to design and build a powerful, flexible and usable web
application in less space than a typical gif image.
This article continues the coding process begun last time, and finally starts
working on real user requirements. On the way it helps take the effort out of
rebuilding and re-running tests whenever anything changes.
Introduction
If you've read the first
and second
articles, you should be aware that the aim of this project is to develop a
small, simple and understandable "Wiki" (editable web site) application. We've
considered and decided to defer the relatively heavy decision of how to store
and retrieve pages by introducing a Java interface, decided on and implemented a
simple "template" system to display pages, and are building a supporting test
suite as we go along.
Before we continue, I'd like to remind everyone of what our "customer" wants.
For the purposes of this article, our customer says we will be "done" when we
have a Java web application in which:
- each page may be viewed using it's own unique URL
- page content may contain links to other pages by name
- links to nonexistent pages will be marked with a "?"
- page content may be created and edited using just a browser
- at least 100 different pages can be stored
What do we write next?
By now, you should know that we start each new feature by writing a test for
it, so the questions should perhaps rather be "what do we test next?".
Whichever way you look at it, this is still a tough choice. If we want to make
real progress toward our customer's goals, we need to start delivering software
which meets those goals. So far, all we have is a repository interface and a
template engine, neither of which is mentioned in the list.
There are lots of ways of deciding what to do next. Perhaps simply do them in
the order they are written in. Maybe do the hardest first so we learn the most.
Maybe ask the customer which is most important. I'm sure you can think of many
more. In this case I'm going to recommend that we start by choosing the easiest
to test. The idea behind starting with the easiest is that the customer
goals are all tangled together. The more we can remove from the tangle, the
simpler the rest will become. With any luck, by the time we get to the hard
tasks, so much will already have been removed that they will be easier, too. The
idea behind starting with the easiest to test is that until we have
written a test, we can't start writing code, and I want to start writing real
code as soon as possible.
Using this approach (often known informally as "pick the low-hanging fruit")
can be suitable at the start of a new project or large single change, where
there are several inter-related requirements which all need to be met for
anything to work at all. Beware, though! This approach can become dangerous as
soon as something has been delivered which the customers/users can try
themselves. At that point it is usually better to pick whichever the customer
wants the most, and see how that affects the remaining ones. It is also
important to resist the temptation to keep adding more "vital" features which
delay this first delivery. The sooner we can get real priority decisions from
the customer, the better.
We have decided to go for the easiest to test. Which one is it?
Goals one and four look like we would need to set up a web server of some
sort. To make it work would in turn mean building our software into something
which can be deployed to the web server. The tests would maybe try to fetch
pages from different URLs and see what they get. Sounds a lot of work before we
see any results. Goal five looks like it would need us to build that repository
we keep putting off. Goals two and three look as if they can be tested in a
similar way to the way we tested the template engine, which seemed pretty easy.
I think I'll choose to do goal two first, as completing it might help make goal
three easier.
First task: "page content may contain links to other pages by name"
Let's start with a nice simple, "empty" test to make sure we have everything
working. Even with this, we still need to think about how we would use the code
we are going to write. One of the simpler ways is to think of it in terms of a
formatting process. Some "raw" text from a stored Page will be "formatted" into
"processed" text containing links where neccesary.
AllTests.java
package tests;
import junit.framework.*;
public class AllTests extends TestCase
{
public static TestSuite suite()
{
TestSuite ret = new TestSuite();
ret.addTest(new TestSuite(EmptyTest.class));
ret.addTest(new TestSuite(TemplateTest.class));
ret.addTest(new TestSuite(LinkTest.class));
return ret;
}
}
LinkTest.java
package tests;
import junit.framework.*;
import friki.PageFormatter;
public class LinkTest extends TestCase
{
public void testEmpty()
{
PageFormatter formatter = new PageFormatter();
assertEquals("", formatter.format(""));
}
}
PageFormatter.java
package friki;
public class PageFormatter
{
public String format(String input)
{
return input;
}
}
Just as with the TemplateEngine, the first test and the first implementation
can feel a bit like cheating. But remember, all of this is part of the design
process -- we have already designed how to use the formatting code, now we have
to make it work for more cases.
Next, though, a major design turning point. We know that "processed" page
names will need to appear as HTML links, but how should we represent the names
of pages in the "raw" text? This is an important decision, users of the system
will have to type one of these names in to the browser every time they want to
link to another page. The answer is not obvious, either. There are Wikis which
use patterns of upper-case and lower-case letters, and automatically recognize
things like FrankCarver and FrikiServlet. There are Wikis which use any group of
characters wrapped in end characters like [[Frank Carver]] or {:Friki}. There
are Wikis which use any alphabetic or alphanumeric word which starts with a
special symbol, like @FrankCarver or ~Friki. There are even Wikis which check
every word in the text, in case it might be the name of a page, and need no
special delimiters at all.
This decision is so important, it needs the involvement of the customer. The
job of the developers is to explain the options and answer questions to help the
customer choose the right compromise between desired features and cost. In this
case, our customer grumbles a bit, and settles for the word-starting-with-symbol
approach, using '@' as the symbol. He says he may change his mind later, but we
knew that already!
With this knowledge we can add some more tests to our LinkTest class:
public void testNonLink()
{
PageFormatter formatter = new PageFormatter();
assertEquals("hello", formatter.format("hello"));
}
public void testEmbeddedLinkSymbol()
{
PageFormatter formatter = new PageFormatter();
assertEquals("friki@javaranch.com", formatter.format("friki@javaranch.com"));
}
Both these tests pass with no extra code. Excellent. However, just as with
the TemplateEngine test case, we now have the same setup code appearing in three
places. It's time to remove the duplication and create a setUp fixture.
LinkTest.java
package tests;
import junit.framework.*;
import friki.PageFormatter;
public class LinkTest extends TestCase
{
PageFormatter formatter;
public void setUp()
{
formatter = new PageFormatter();
}
public void testEmpty()
{
assertEquals("", formatter.format(""));
}
public void testNonLink()
{
assertEquals("hello", formatter.format("hello"));
}
public void testEmbeddedLinkSymbol()
{
assertEquals("friki@javaranch.com", formatter.format("friki@javaranch.com"));
}
}
Now we get to add a test which fails:
public void testLink()
{
assertEquals("<a href='view?hello'>hello</a>", formatter.format("@hello"));
}
As usual, the next step is to write the simplest solution. Maybe something
like:
PageFormatter.java
package friki;
public class PageFormatter
{
public String format(String input)
{
if (input.startsWith("@"))
{
return "<a href='view?" + input.substring(1) + "'>" + input.substring(1) + "</a>";
}
return input;
}
}
Good, That works. It's a bit messy, and I can see some duplicated code in
there, so lets refactor it to tidy it up. Don't forget to re-run all the tests
after every change to make sure nothing has broken.
PageFormatter.java
package friki;
public class PageFormatter
{
private String symbol = "@";
private String makeLink(String name)
{
return "<a href='view?" + name + "'>" + name + "</a>";
}
public String format(String input)
{
if (input.startsWith(symbol))
{
return makeLink(input.substring(symbol.length()));
}
return input;
}
}
You can probably guess where we go next. We've done just enough testing to
test that we can generate a link, but it's pretty obvious that a Wiki page with
just one word is not very useful, even if it is a link to another page. So we
need to test that the PageFormatter can find and convert page references
wherever they appear in a page. So, we add another test:
public void testLinkInPage()
{
assertEquals("You say <a href='view?hello'>hello</a>, I say goodbye.",
formatter.format("You say @hello, I say goodbye."));
}
After the last test passed, we refactored the code to be simpler and neater.
Now is a chance for a second sort of refactoring, the kind you can do
before a change, to make the change itself simpler. I'm pretty sure we'll
still need to be able to convert a word to a link if it starts with the special
symbol, so let's extract that code as a method, which we can call from the
"smarter" code we are about to write.
PageFormatter.java
package friki;
public class PageFormatter
{
private String symbol = "@";
private String makeLink(String name)
{
return "<a href='view?" + name + "'>" + name + "</a>";
}
public String formatWord(String word)
{
if (word.startsWith(symbol))
{
return makeLink(word.substring(symbol.length()));
}
return word;
}
public String format(String input)
{
return formatWord(input);
}
}
Our new test still fails, of course, but now we have a clean place to put the
new code. Of course there will be other changes to this class, but it's often a
good idea to "clear the decks" before adding anything major.
PageFormatter.java
package friki;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
public class PageFormatter
{
private char symbol = '@';
private String makeLink(String name)
{
return "<a href='view?" + name + "'>" + name + "</a>";
}
private boolean startsWith(String word, char symbol)
{
return word.length() > 0 && word.charAt(0) == symbol;
}
public String formatWord(String word)
{
if (startsWith(word, symbol))
{
return makeLink(word.substring(1));
}
return word;
}
public String format(String input)
{
StringBuffer word = new StringBuffer();
StringBuffer ret = new StringBuffer();
CharacterIterator it = new StringCharacterIterator(input);
for(char c = it.first(); c != CharacterIterator.DONE; c = it.next())
{
if (c == symbol || Character.isLetter(c))
{
word.append(c);
}
else
{
ret.append(formatWord(word.toString()));
word.setLength(0);
ret.append(c);
}
}
ret.append(formatWord(word.toString()));
return ret.toString();
}
}
It looks like we've finished one of the user's goals. Hooray!. There are, of
course, a few things to note about this code:
First, although that "formatWord" method is still there, it has changed a
little. The variable "symbol" is now a char rather than a String to make things
easier in the "format" method. This restricts future expansion a tiny bit, but
that doesn't matter. If anyone really ever wants to use a multi-character word
prefix, there isn't much to change. The thing that irritates me most is that I
can't use the built-in and nicely descriptive String.startsWith method
any more, as there is no version for asking if a String starts with a char.
Sigh.
Second, the structure of the "format" method looks strangely similar to the
TemplateEngine.expand method we developed last time. Bear this in mind, and if
we see an opportunity, we should consider refactoring the whole system to remove
this duplication.
Making Rebuilding Simpler
Are you getting fed up with all that typing "javac ..." and "java
..." yet? I know I am. Even if you use an IDE or a command-line history
mechanism which hides it, there's still a lot of repetition. We owe it to
ourselves and to our customers to stamp this out. The solution I suggest for
this is to use a "build tool".
A build tool is a program which reads a pre-defined set of instructions and
relationships, and uses them to build software for you. A build tool is a kind
of specialized scripting language, indeed you can get a long way using just the
scripting abilities built in to your system (DOS .bat files, Unix shell-scripts
etc.). In this case I recommend the Ant
build tool from the Apache project. If you are not already familiar with Ant,
you may want to look at Thomas Paul's March
2002 newsletter article, and install Ant as directed.
Ant is flexible and powerful, and we will be using some of its more
interesting features in later articles. For now, though, we just want to get it
to compile and test our code. Before we leap into scripting, we need to
understand what we want Ant to do, and we also need to be able to test (even if
only by inspection) that it has done it right.
The first step is to set up a sensible directory structure for our project,
and copy our source files into it. The structure I recommend follows the common
Ant pattern of separate directories for separate operations. In particular, note
that source code and classes for the delivered application are kept completely
separate from the test code. We will likely have more test code than application
code by the time we have finished, and we want to keep the delivered application
as lean as possible. So we keep test code separate to avoid the risk of
delivering any of it.
friki
|
+-----------+----------+
| |
src build
| |
+-----+------+ +---+---+
| | | |
delivery tests delivery tests
| | | |
+-+---+ +-+--+ | |
| | | | | |
java files java files classes classes
- Copy PageFormatter.java and TemplateEngine.java into
friki/src/delivery/java/friki
- Copy AllTests.java, EmptyTest.java, LinkTest.java and TemplateTest.java
into friki/src/tests/java/tests
- Create a new file friki/build.xml and put the following into it:
build.xml
<project name="friki" default="build" basedir=".">
<path id="classpath">
<pathelement location="build/delivery/classes"/>
<pathelement path="${java.class.path}"/>
</path>
<path id="testclasspath">
<pathelement location="build/delivery/classes"/>
<pathelement location="build/tests/classes"/>
<pathelement path="${java.class.path}"/>
</path>
<target name="clean">
<mkdir dir="build"/><delete dir="build"/>
</target>
<target name="compile-delivery">
<mkdir dir="build/delivery/classes/"/>
<javac srcdir="src/delivery" destdir="build/delivery/classes" debug="on">
<classpath refid="classpath"/>
</javac>
</target>
<target name="compile-local-test">
<mkdir dir="build/tests/classes"/>
<javac srcdir="src/tests/java" destdir="build/tests/classes" debug="on">
<classpath refid="testclasspath"/>
</javac>
</target>
<target name="compile" depends="compile-delivery,compile-local-test"/>
<target name="test">
<junit fork="yes" haltonfailure="yes" printsummary="yes" dir="build/tests">
<jvmarg value="-Djava.compiler=NONE"/>
<test name="tests.AllTests"/>
<classpath refid="testclasspath"/>
<formatter type="plain"/>
</junit>
</target>
<target name="build" depends="compile,test"/>
</project>
This may look complicated, but mostly it's just a way of configuring Ant to
understand the directory structure we are using. Each target is equivalent to a
public method or function definition, and the final "build" target brings them
all together
Now, open a command-line, change to the top friki directory, and type
ant. As specified on the first line of the build file, if no target name
is specified it runs the default "build" target. You should now see a series of
messages finishing with something like:
test:
[junit] Running tests.AllTests
[junit] Tests run: 9, Failures: 0, Errors: 0, Time elapsed: 0.156 sec
build:
BUILD SUCCESSFUL
If you get any complaints from Ant, double-check the directory structure and
the build.xml file. If you see any failures or errors reported by junit, look in
the file friki/TEST-tests.AllTests.txt for more details. If it all works,
celebrate!
How are We Doing?
We still haven't made a Wiki yet! But we have met one of the five user goals.
We have continued expanding our test suite to cover everything we have done so
far, and have created a directory structure and build script to enable the whole
project to be built and tested using a single, simple command. We are poised on
the edge of producing a real web application.
Next session we'll add more customer features to our Wiki and extend our
build script to automatically create a deployable "web application archive"
(war) file. Why not try and guess which of the remaining customer goals will be
next on the list...
Return to Top