Extreme isolation part 3: coding a CRUD app (with full example)
CRUD apps start simple, yet often get messy and nasty really fast. They are a great test bed for Extreme Isolation.
I started a few months ago looking at a fresh new way of architecting web applications. I suggest you read parts one and two first.
The app I’ve been mainly working on using this new method is an online version of Sol Trader, which isn’t really a typical web application most people write. I’ve since applied this paradigm to a directory application called “Discover” I’ve been working on for the Trust Thamesmead charity, and I thought I’d share the results.
Discover is a much more traditional “CRUD style” application. The administrators define audiences for a local area (people who go to school, or want to find a job) and add places to a site, grouped into topics for that audience. For example, if you’re into music (the “music” audience) you might want to see places in the “music shops”, “gig venues” and “music video shoot locations” for a particular area.
The source code is fully open source. Trust Thamesmead have a great ethos: they would love other local areas to pick up the application and run with it. This also means that I can use the codebase as a demonstration of extreme isolation.
Let me take you through how it works.
The basic models: Audience, Topic, Place
Let’s have a look at the data representation for models first. Check out audience.rb:
These objects are immutable. They are created from an AudienceRepository
, which handles all the persistence of the objects for you. They know nothing about loading, saving or disk representations, which is exactly as it should be.
Audiences themselves are very simple containers of a description and a list of associated topics. They have a method to generate a slug, and two generator methods to create new audiences based on this topic: that’s how we handle updating audiences.
A web request to retrieve an object
The web logic is wrapped up in two files: a Sinatra application in app/audiences.rb which acts like a controller would in Rails, and a shared module in app/crud.rb which contains logic used by all the other Sinatra apps.
A web request comes in to the application and runs this code in the shared logic:
This find method is defined in the Audience-specific class:
The AudienceRepository
takes care of the persistence end of things (you can see how in persisted/audiences.rb), and returns back a plain ruby Audience
object as shown above. This object is then passed to the edit.haml view file as @object
and we’re done.
A web request to update the object
Updating the object is more interesting. The following action is called first, which then calls a series of other methods:
The first line retrieves a plain immutable Audience
object as before. The update_from_params
method is called next: this returns a new Audience
object with the updated information, using the factory methods we defined on the model earlier.
Validation
The new Audience
object is passed to an AudienceValidator
object (defined here) which takes a list of existing slugs in the database, and returns one of two things:
- A
ValidAudience
change if the newAudience
object is valid - An
InvalidAudience
change if it is not valid
We appear to be reinventing the wheel with the Validator object here: but the great advantage with doing things this way is that the object has no dependency on the database at all. This means it can be tested in isolation, it’s fast, and we can chain them together and reuse them in more situations.
Applying the changes
The queue of changes is then pipelined through various other services in true Extreme Isolation fashion. Firstly we apply the queue to an object we receive from the editor
method call in the Sinatra application:
This processes the ValidAudience
change and returns an AudienceEdited
change, which is tacked on to the end of the queue of changes. (See reactor.rb for exactly how the plumbing works.) An InvalidAudience
change is ignored - we don’t want to edit the audience in this case.
The resulting change queue is then passed to downstream
which is the set of services that process all web requests:
The AudienceRepository
picks up the AudienceEdited
change and does the correct thing to the persisted record. The AudienceHandler
works out how to return the right message to the web interface. It handles InvalidAudience
and AudienceEdited
messages, as well as AudienceCreated
and AudienceDeleted
messages for the other CRUD operations.
Creation and deletion
The other CRUD operations work very similarly. The creation simply constructs a brand new Audience
object, checks validity and passes the resulting set of changes to the AudienceRepository
and the web handler. Deletion is even simpler: it just passes an AudienceDeleted
message to the downstream
method.
Extending the set of services
This way of doing web applications is extremely extendable. Here’s a much more complex downstream
method for Sol Trader Online, which is run for every single player action web request in the game:
Each piece is totally isolated and therefore easily testable. When one service gets too complex, it’s easy to split up what it’s doing into two services: PositionPermissionChecker
is a recent extraction from the code inside the Position
object.
Conclusions
This is still an experiment. It’s more involved that a typical CRUD app, and harder to get going, but the individual pieces (the validators, the Editor
class, the Handler
classes) are all very testable as they only do one thing in isolation.
There are also many ways that I could improve the web logic, but at the moment those classes are fine for my purposes. Likewise, all the javascript is still inline in the views, and has yet to be pulled out and refactored.
What do you think of the approach? Can you see yourself using it on your next project?
Share on BlueSky to comment.