CRUD apps start simple, yet often get messy and nasty really fast. They are a great test bed for Extreme Isolation.
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:
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.
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:
ValidAudiencechange if the new
Audienceobject is valid
InvalidAudiencechange 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:
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
AudienceEdited messages, as well as
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
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
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.
What do you think of the approach? Can you see yourself using it on your next project?