Domain-driven design (DDD) is an approach to software development for
complex needs by connecting the implementation to an evolving model. The
premise of domain-driven design is the following:
placing the project’s primary focus on the core domain and domain logic
basing complex designs on a model of the domain
initiating a creative collaboration between technical and domain experts to iteratively refine a conceptual model that addresses particular domain problems
Ubiquitous Language is the term Eric Evans uses in Domain Driven Design for
the practice of building up a common, rigorous language between developers
and users. This language should be based on the Domain Model used in the
software - hence the need for it to be rigorous, since software doesn’t cope
well with ambiguity.
So in short it is all about language between domain experts and developers.
Usually it means that in our code we have to abstract our code from infrastructure details and describe everything in terms of a business model.
Main pattern in OOP languages is to use Interface as abstraction and encapsulate infrastructure details in a class which implements an interface. All other classes depends only from other interfaces, so we can easy swap implementation. Main patterns for composition are Dependency injection and Inversion of control.
There are a lot of tutorials how to do DDD in c#, but what about f#?
Actually we can use the same way in fsharp, but this way has some drawbacks and we probably could find better way to do that.
I found several excellent articles about how to do ddd in fsharp.
For example A different look at service design and Domain Driven Design with the F# type System. They describe how to model domain in terms of functions and types. But we will try to go deeper.
DSL
So ddd is about languages. In programming languages we could build our domain language as external or internal dsl. Internal dsls are better because we could use all existing tools of a host language. Internal dsls could be shallow or deep. Shallow embedding means that we describe our domain in types and have fixed interpretation of the data. It is not very useful, because for testing we want to use a fake interpreter and in production use an optimized interpreter.
So we want to build deep internal dsl. How? We could represent all operations as abstract types and compose abstract computations with a computation builder. But we also want to abstract not only from implementation but also from function effects. DDD with interfaces has this problem. For example dependency without effect.
Now we want to add numbers asynchronously. And we have to change everything.
Lets start. We need to represent our abstract operations as data types. But how? In short our command will look like a request to a server(interpreter).
Request is a type for representation of a command tag and input params
But we also need a way to compose commands together. So interpreter needs a way to execute a command and invoke some function which will take command result and return a new command to proceed or some kind of stop signal like return something command. Lets do that.
And for composition we need some bind function which will compose some request with a function which will take output of a command and produce a new one. Internally it will use bind function of continuation type ‘k.
This is the main building block of our dsl language. Now we could start to define our domain.
Database operations is a good way to represent dsls composition. So we have for example one language for our problem domain and one as an sub dsl for database ops(Something like DDD’s repository pattern). Usually it is not the best way to do things but good enough for our example.
As you could see databse dsl in bind uses bindRequest function and additional bind function of parent dsl. Lets finally describe our main dsl.
So as you can see it is just a boilerplate code which probably could be generated by a type provider for you. Now we need to lift our type constructors into our dsl monad.
Now we can write programs by using lifted funcs in dsl builder. But before that lest create a fake interpreter.
As you can see our abstract download function has no effects because all effects exist only in interpreter. In our case download has async effect.
So everything is ready for programming.
lets run our abstract program with fake interpreter.
We used an fake ad hoc interpreter in this post, but we could describe interpreter as a data structure(comonad). It allows us to abstract and compose interpreters. More advanced version with comonadic interpreter here
Result
Now we can write abstract programs which are easy to test with fake interpreters and we abstract our code from execution effects. No more code rewrite when we decided to use async/lazy version of some method.