A fresh take on DCI with C++ (with example)
I’ve been reading quite a lot about DCI recently, both from the point of view of the original paper published in 2009 and various other sources on the Internet. There’s been a discussion around beginning to use it with Ruby on Rails, but at the moment I’m more interested in how to apply the principles to Sol Trader in C++.
What is DCI?
DCI stands for Data, Context and Interaction. It places behaviour at the forefront of your design by setting the stage for a particular use-case through Context objects, and having all the behaviour exist in Role objects seperate to your basic persisted Data objects. For example, you might have an Account
“Data” class, which just contains the data representation and basic methods such as IncrementBalance
and DecrementBalance
. You’d then have a use-case TransferMoney
with two roles: SourceAccount
and DestinationAccount
. These roles would be played by Account
objects, but the behaviour of the objects would depend on the roles they play in the use case. The behaviour of the system is therefore captured in one place: it’s in the interaction between the roles within a particular use case context, rather than being spread all over different data classes.
This design paradigm is very interesting. We’ve known for a while that if you mix persistence and behaviour you’ll get into somewhat of a mess with your code design after a while. What’s new is that whilst we avoid mixing persistence and behaviour, we still often mix data and behaviour: that is, we put code describing what the object does in the same class as the code which describes what it is. This is a subtle violation of the Single Responsibility Principle; I hadn’t noticed this violation before reading about DCI.
The proponents of DCI advocate injecting methods describing the Role in any given use case directly into the data object when setting the use case up. This is easy in a language like Smalltalk or Ruby, but is considerably harder in C++. What approach should we take in C++ with Roles? Is this the right approach at all?
The templating method for injection of role behaviour
One way around this it to use templates: subclass your Data objects with a templated class which includes the roles you want the object to play. For example, to take our account example earlier, we could have:
We would also have a similar set up for SourceAccount
. This way we can pass a pointer to the DestinationAccount
interface to our Context to set up the use case:
The DataClass would then instantiate itself from the account id and the role contains the description of the behaviour.
In practice however, I found this extremely unwieldy. My data classes all had slightly different interfaces, especially as many of them served as API endpoints. The templates ended up being ‘clever’ code - they saved very little space at the expense of a good amount of readability. The whole point of DCI is to try and capture behaviour in the Role classes to improve readability and create a cleaner design, and this approach wasn’t serving that purpose. There might be better ways of doing it, and I’d be grateful if you’d let me know if you know of a better approach.
The composition method for roles
The DCI literature teaches us to inject behaviour into the Data objects, to prevent self schizophrenia. For complex use cases, I can see that it would be useful for roles to have access to the methods defined on data objects: but perhaps it would be better to have simpler use cases and have roles only be defined in terms of other roles? In that instance, the roles can simply compose the data objects and expose whichever methods seem appropriate to the other roles in the use case.
A worked example
As an example, consider this use case that came up recently in Sol Trader: I have a MarketListing
object which contains a particlar Good
(such as Grain, or Water) available at a certain price. The GUI displays a list of these MarketListing
objects in a table format. Whenever a change was detected to any of the listings I would clear the table and reconstruct the gui elements, rather than updating the original elements.
This worked fine, until I realised that the GUI library I’m using did not expect GUI elements to be deleted and recreated under the mouse cursor, and wouldn’t fire “click” events correctly at the newly created elements.
Therefore I needed a way to synchronise the GUI table with the MarketListings
somehow, add new listings that have appeared, update the text of any existing listings, and remove old elements that refer to listings that no longer exist in the set. I decided to try to implement this using DCI, using the composition approach to roles I’ve discussed.
The use case is quite simple:
- For each source data structure:
- Does it already exist? If so, update the elements
- If not, create new elements
- Remove any elements that weren’t checked this run
After writing some tests, I started with the following context object:
Note the two role objects, TableSource
and TableRepresentation
. I’ll come back to those later.
execute()
and checkRow()
were defined like this:
(In C++ you can’t easily enumerate, so I used boost::bind to call checkRow()
on each row in the _source
object.)
This code is beautifully simple, and very close to the pseudo code I wrote earlier.
Implementing TableRepresentation
What does this context require of the roles? Here are the methods needed for TableRepresentation
, taken directly from the context above:
This object is created with the data it needs to manipulate: in this case a Rocket::Core::Element
object. When it needs to update elements, it is passed a TableSource
role to give it the relevent data. Here’s some of the code for the addRowFor()
method:
There’s a lot of noise here, but note the use of source
. The code creates a new li
element, gets the column list from the source and then asks the source for the string data for a particular row using rowFor()
. The roles are interacting to provide the behaviour of the use case.
Implementing TableSource
TableSource
needed to be an interface in the end to manage both viewing a series of MarketListings
and also a player Inventory
. Here are the methods:
For a MarketListing
, here’s the implementation of the key methods that the TableRepresentation
needs:
The player Inventory
table source code is very similar.
Tying it together
How do I use this thing? Inside my controller for updating the GUI window, I do the following:
In each case the TableRepresentation
is the same, with a different target element, and the source is different depending on what I want to show for that table.
In conclusion
I could have simply used a list of MarketListing
objects instead of my TableSource
, and manipulated the Element
objects in the GUI directly. That’s what I did at first, but this approach gives me a number of advantages:
- The code for enumerating rows, and exposing certain data to to the GUI is kept out of
MarketListing
, which is great: it only makes sense in this Context which is exactly what a role is for. - The actual guts of the synchronisation code is kept in the Context. I’m not sure this is the best place, but it’s great to have it in one place.
- It was trivial to add a
role::InventoryDataSource
object: in another 30 minutes or so I had TDDed out the display of inventories of goods using the same Context and slightly different roles. - I could potentially replace the
TableRepresentation
with anything which we need to sync lists of tabular data with.
I tested this using some real CommodityMarket
objects, which contain MarketListing
objects: I poked new goods into them and checked the elements were being created and removed successfully.
Here’s a screenshot of the market at work:
In summary, I’m very pleased with how this turned out. There is a bit more code than just hard wiring it, but all my behaviour is in one place, and I’ve not loaded my market and goods classes with yet more functionality. I’m now looking for other use cases to implement using a similar method, as I move on to building a realistic (as opposed to random) economy.
How do you like my approach to DCI? Have I missed something profound, or how could I improve my approach?
Share this article
Read more
How to Build a Robust LLM Application
Last month at Cherrypick we launched a brand new meal generator that uses LLMs to create personalized meal plans.
It has been a great success and we are pleased with the results. Customers are changing their plans 30% less and using their plans in their baskets 14% more.
However, getting to this point was not straightforward, and we learned many things that can go wrong when building these types of systems.
Here is what we learned about building an LLM-based product that actually works, and ends up in production rather than languishing in an investor deck as a cool tech demo.
Read moreYour Code Is A Liability
Every chunk of code you commit is more for someone else to read, digest and understand.
Every complex “clever” expression requires another few minutes of effort for each of your team. They must now interpret what you wrote and why you wrote it.
Every line you add limits your project’s responsiveness to change.
Your code is a liability. Never forget this.
Read moreThe Sol Trader Christmas Eve update: moddable missions
The relative radio silence from Sol Trader Towers is for a reason: I’ve been working hard on a flexible and moddable mission structure, that allows players to take a variety of interesting quests in-game.
This build is now available on the forums should you have access (there’s still time if you don’t.)
I’ve built a few missions to start with, including delivering parcels for business or personal reasons, taking characters on business trips and making other characters disappear. It’s great fun to have a variety of things to do for characters now and adds yet more colour to the game. Because it’s completely moddable, I’m also excited to see what storylines other people come up with!
Under the hood
The full details of how to create your own missions are available as a lengthy forum post, which will be kept up to date with changes and clarifications. Here’s an overview:
The missions are organised into packs, which exists under the data/missions
subfolder. If you have access to the beta builds, you’ll see there’s one pack there already: these are the missions that are built in to the game.
There are several csv files in each mission folder:
requirements.csv
: This file details the cases in which this mission might be triggered. Each character in the game has a chance of picking this mission (and becoming the ‘giver’ of the mission), based on the conditions imposed by this file.conversation_player.csv
: The extra conversation options available to the player because of this mission.conversation_ai_response.csv
: The extra options the AI can choose from as conversation responses.opinions.csv
: The extra opinion triggers, used for reactions to the generation and completion of these missions.strings.csv
: The new strings needed for the previous CSV files.
The possibilities for you to build your own missions are expanding all the time, as I add new missions triggers and possible goals for the AI.
What’s next?
At the moment it’s possible to take on any mission from any person, which isn’t very realistic. I need to allow players to gain other character’s trust, so that they will only give you sensitive missions in certain cases. Additionally it will soon be possible to start a career with an organisation, which will give you a rank, a certain amount of built in trust, and access to more senior characters.
I’m also going to be working on the in-space AI very soon. At the moment only freelance traders fly around between planets: it’s time we had passenger ships, military guards and pirates thrown into the mix.
Have a fantastic Christmas and I’ll see you all in the new year with some more updates.
Read moreNew Sol Trader beta: the science of blame and unforgiveness
Previously I wrote about how I’m modelling opinions and prejudice in Sol Trader. It’s time to put some of that information to use.
The opinions a character has of other people, based on the partial events that they know about them, will now directly affect the things that happen in the history generation. This creates new events, which will in turn feed more character opinions.
There’s a new beta available on the forums if you have insider access.
Dudley and Meredith
In the example on the left, we can see that an acrimonious divorce of Meredith’s parents has left an indelible mark on her childhood. She now has a very low opinion of her father, Dudley.
When characters are adults, they can then generate a series of ‘favours’ (or ‘missions’) that they want completed. This is a source of work for the players, although completing certain missions does have real consequences on your relationships with the target of the mission. If they find out you’ve taken a mission against them, then they won’t be happy with you.
To continue our example, Meredith, whom we are now married to, wants us to find out some potentially incriminating information about our own father-in-law, Dudley. It’s up to us whether we take it or not. If he finds out, we’ll make an enemy of him.
As the game goes on, the player will get embroiled in these relationships between the various characters and be able to directly affect their stories. Choosing what to take on and who to ally yourself with forms a major part of Sol Trader’s gameplay.
Sarina’s spiral of doom
Another example: the sad tale of Sarina, our older half sister. I picked Dagny and Warren in history generation to be my character’s parents, knowing that Dagny was cheating on her husband Hayden, mostly to see what happened. Little did I know how much it would affect Sarina, Dagny and Hayden’s eight year old daughter. When she found out about my birth, she got very upset.
She didn’t blame me, thankfully, although she never thought much of me. However, she never really spoke to our mother again, especially since her beloved father Hayden died soon after we were born.
She left home at a young age, and became a political assistant, but she didn’t make too many friends. She was doing ok for a time, only to find out that the love of her life, Richard Ruhr, had been having an affair behind her back all along.
She divorced him, got depressed, quit her job and by the time I grew to adulthood at the start of the game, she was living in a hippie commune somewhere on Mercury, trying desperately to get some gossip on her ex-husband.
New beta out now
This new beta is now available from the forum if you have purchased insider access (if you haven’t there’s still time.) Let me know if you find any other interesting stories such as these!
Read moreModelling opinions and prejudices in Sol Trader
I’ve been working hard on the Sol Trader core gameplay mechanics in the last two weeks. High up on my list was a way of generating more interesting missions for the characters to complete.
In order to have a reason to gather dirt, find locations or desire an early end for an enemy, our characters need to feel strongly about other people they know. This is where their opinions and prejudices come in.
Characters already keep track of the events they know about for each other character in the game. Now they can form an opinion of a character based on the partial set of info they know about someone else’s past.
The plan is to use these thoughts about each other to make decisions about who they’re friends with, deal with relationship breakdown, blame and prejudice.
Here’s an example of how we configure this under the hood for an occasion where a character is caught and reported for taking bribes:
Anyone knowing about this event will think the character is less deserving of sympathy and assume the character is less moral. If we’re the one catching them take the bribes, then the briber becomes much less influential over us. If we’re the one being caught, then the one catching us is definitely no longer our friend. Depending on our profession, we will brief against them or possibly try to take them out.
Now characters have opinions about others, we can use these to guide their conversation choices, who they’re likely to target, give us gossip on, etc. It’s all game design fuel for other behaviours in the game, and will combine to form interesting unexpected effects and tell original stories each time.
Next time I’ll discuss about the new events that get created in the history generation because of these new opinions. Our stylised formulaic view of history is about to become, well, a lot more messed up. Rather like real history…
Read more