Promise: Building the repository pattern on the language IoC
Before I get into the code samples, I should point out one more "construction" caveat and change from my previous writing: Constructors don't have to be part of the Type. What does that mean? If you were to explictly declare the Song
Type and excluded the Song:(name)
signature from the Type, it would still get invoked if someone were to call Song{name: "foo"}
, i.e. given a JSON resolution call, the existence of fields is used to try to resolve to a constructor, resulting in a call to Song:(name)
. Of course that's assuming that instance resolution actually hits construction and isn't using a Use
lambda or returning an existing ContextScoped
instance.
A simple Repository
Let's assume we have some persistence layer session and that it can already fetch DTO entities, a la ActiveRecord. Now we want to add a repository for entities fetched so that unique entities from the DB always resolve to the same instance. A simple solution to this is just a lookup of entities at resolution time:
$#[Session].In(:session).ContextScoped;
$#[Dictionary].In(:session).ContextScoped;
$#[User].Use {
var rep = Dictionary<string,User>();
var name = $_.name;
rep.Get(name) ?? rep.Set(name,Session.GetByName<User>(name));
};
In the above the $_
implicit JSON initializer argument is used to determine the lookup value. I.e. given a JSON object, we can use dot notation to get to its fields, such as $_.name
. This name is then used to do a lookup against a dictionary. Promise adopts the C# ??
operator to mean "if nil, use this value instead", allowing us to call .Set
on the dictionary with the result from the Session. There is no return since the last value of a lambda is returned implicitly and Set
returns the value set into it.
One other thing to note is the registration of Dictionary
as ContextScoped
. Since Dictionary is a generic type, each variation of type arguments will create a new context instance of Dictionary. For our example this means that the lambda executed for User
resolution always gets the same instance of the dictionary back here.
context(:session) {
var from = User{ name: request.from };
var to = User{ name: request.to };
var msg = Message{ from: from, to: to, body: request.body };
msg.Send();
}
The usage of our setup stays nice and declarative. Gettting User
instances has no knowledge how the instance is created and just passes what instance it wants, i.e. one named :name
. Swapping out the resolution behavior for a service layer to get users, a mock layer to test the code, a different DB layer, all can be done without changing the business logic operating on the User
instances.
A better Repository
Of course the above repository is just a dictionary and only supports getting. It assumes that Session<User>.GetByName
will succeed and even then only acts as a session cache. So let's create a simple Respository class that also creates new entities and let's them be saved.
class Repository<TEntity> {
Session _session = Session(); // manual resolve/init
+Dictionary<String,Enumerable> _entities; // automatic resolve/init
Get:(name|TEntity) {
var e = entities[name] ?? _entities.Set(name,_session.GetByName<TEntity>(name) ?? TEntity{name});
e.Save:() { _session.Save(e); };
return e;
}
}
Since the Repository
class has dependencies of its own, this class introduces dependency injection as well. The simplest way is to just initialize the field using the empty resolver. In other languages this would be hardcoding construction, but with Promise this is of course implicit resolution against the IoC. Still, that's the same extraneous noise as C# and Java that I want to stay away from, even if the behavior is nicer. Instead of explicitly calling the resolver, Promise provides the plus (+
) prefix to indicate that a field should be initialized at construction time.
The work of the repository is done in Get
, which takes the name and returns the entity. As before, it does a lookup against the dictionary and otherwise set an instance into the dicitionary. However, now if the session returns nil, we call the entity's resolver with an initializer. But if we set up the resolver to call the repository, doesn't that just result in an infinite loop? To avoid this, Promise will never call the same resolver registration twice for one instance. Instead, resolution bubbles to next higher context and its registration. That means, lacking any other registration, this call will just create a new instance.
Finally, we attach a Save()
method to the entity instance, which captures the session and saves the entity back to the DB. This last bit is really just there to show how entities can be changed at runtime. As repositories goes, it's actually a bad pattern and we'll fix it in the next iteration.
The registration to go along with the Repository
has gotten a lot simpler as well. Since the repository is context scoped and gets a dictionary and session injected, these two Types do not need to be registered as context scoped themselves. And User
resolution now just calls the Repository
getter.
The access to the instance remains unchanged, but now we can change its data and persist it back using the Save()
method.
Now with auto-commit
As I mentioned, the attaching of Save()
was mostly to show off monkey-patching and in itself is a bad pattern. A true repository should just commit for us. So let's change the repository to reflect this:
class Repository<TEntity> {
+Session _session;
+Dictionary<String,Enumerable> _entities;
_rollback = false;
Get:(name|TEntity) {
var e = entities[name] ?? _entities.Set(name,_session.GetByName<TEntity>(name) ?? TEntity{name});
return e;
};
Rollback:() { _rollback = true; };
~ {
_entities.Each( (k,v) { _session.Save(v) } ) unless _rollback;
}
}
By attaching a Disposer to the class, we get the opportunity to save all instances at context exit. But having automatic save at the end of the :session
context, begs for the ability to prevent commiting data. For this the Rollback()
method simply sets a _rollback
flag that governs whether we call save on the entities in the dictionary.
We've iterated over our repository a couple of times, each time changing it quite a bit. The important thing to note, however, is that the repository itself, as well as the session, have stayed invisible from the business logic. Both are an implementation detail, while the business logic itself just cared about retrieving and manipulating users.
I hope that these past posts give a good overview of how language level IoC is a simple, yet powerful way to control instance lifespan and mapping without cluttering up code. Next time, i'll return to what can be done with methods, since fundamentally Promise tries to keep keywords to a minimum and treat everything as a method/lambda call.
More about Promise
This is a post in an ongoing series of posts about designing a language. It may stay theoretical, it may become a prototype in implementation or it might become a full language. You can get a list of all posts about Promise, via the Promise category link at the top.