Unity names and Software Craftsmanship
So far my experience of coding in Unity 3D has tended to be a more pragmatic experience. I suspect that is because whilst Unity does make use of Object Orientation (OO), it feels like it is used as a tool to implement some functions rather than a fully modeled OO system. I consider myself to be a pragmatic software developer. Some might call that a dirty excuse but I believe in getting things done. Ignoring some of the more ‘ivory tower’ practices of ‘Software Craftsmanship’ is perfectly fine — providing you understand the trade off you are making.
What’s in a name?
I recently had a discussion about the name of an c# event.
Let me be absolutely clear here, I have written code exactly like this. My thought process would go something like, “hmm, what shall I call this event? Erm, I want to notify the UIManager that an enemy has died so I’ll call it OnEnemyDied and pass in the score value for this enemy”. The question posed was can you re-use this event as is, or should a new event be created? I shall dig a bit deeper into this.
On the face of it there is not anything wrong with the above code. But looking at the event without any context exposes two of the problems.
- The Name of the Event — On*Enemy*Died
- The Action<int> — What is the int?
Here is a class that I have written with almost no OO modeling. The net result is something that is ambiguous. E.g. at some later date I realise that the enemies have a Commander, and it needs to know when one of them dies. So I can see the OnEnemyDied event and can subscribe to it.
Cutting corners with Action<T> ?
Our first problem is that because I have taken a shortcut and used an Action rather than EventArgs I get a very unhelpful looking parameter in the Event handler signature. This is just an example of being pragmatic, it is not that big a problem. However, I do not want the score. So I can rewrite it to show I do not care.
But still, it is a bit ugly.
We have not got to the big problem yet, but here is another side issue, the name.
Ignoring OO Design for Names
If we slow down and read out-loud what we have, we get, “On the Enemy class, subscribe to OnEnemyDied which automatically gives us Enemy_OnEnemyDied”. That is wrong. You do not have a method called OnBehaviourAwake because you know the context of the method. This is part of the OO design of a class. In the classic OO example of implementing Animal Classification you might have Mammal.GiveBirth(), you would NOT have Mammal.MammalGiveBirth(). So we could rewrite this again.
Skipping OO Modeling
The real problem here is that the required functionality is missing. The Commander does want to know which specific team member has died, but it does not care about the score. Whereas somewhere else there is a component that wants to know when the score should be increased. By skipping the OO modeling we are starting to get a bit muddled. Again, I am not saying this is a wrong approach, I am just trying to understand the how/why we can get into these situations. The problem were are facing is, do we continue to re-use the existing OnDied, do we change it, do we use additional events or some combination of these?
Adding more arguments
Adding additional arguments is a tempting idea, we can re-use the same events. Okay it will mean changing the existing clients (oops, a hidden cost creeps in) but it will work.
Now our Commander can ignore the score and process the enemy that has died. Our existing subscribers can continue to process the score but will have to ignore the Enemy argument. It is okay, but it makes my skin crawl — usually a sign that it really is not okay but I have not thought enough about why :)
Adding another Event
In a larger system this could be the correct answer, since you have formed a contract with external systems. To retain backwards compatibility you would resist changing it, but are typically free to add to it. But for a Unity application where all your code is inside one project then you do not usually worry about that. Regardless, I will try and add a new event instead.
Our existing subscribers continue unaffected and the Commander can now subscribe to OnKilled and get the Enemy information they need. All is good. Well…except now our API is looking pretty confusing. When the next potential subscribers comes along they have two very similar looking events to subscribe to. We have got ourselves into a pickle. Imagine if both passed the same argument type, ooooh the bugs and hair pulling that will ensue.
Revisiting OO Design
At this point we could take a step back and think what would have happened if we had spent 6 months modeling the design in UML, Universal Modelling Language, ;) Pragmatically you would still mentally skip the majority of the work but these are the sort of inner-conversations I would have:
I have a service that wants to track the current score, ScoreService. This is a self contained Unity app, so I will live with a bit of tight coupling. ScoreService wants to know when anything occurs that causes the score to increase. I’m choosing not to map entities to scores, so the entity will also have to tell me their score. Therefore I’m declaring a contract for scores, IScoreProvider.
Except, that we are in the world of Unity, and in this world we DO like a good static event. So in reality our IScoreProvider is really more of a notional design idea than an enforced reality (unless we get into Factory Patterns and the like). But what it shows is that the behaviour we are interested in is knowing about the score increasing. Not about enemy death, or treasure collected or, etc. We just want to be told about the score increasing. Following the same design process we would still see a need for the enemy to declare when it was dead. So by modeling and renaming we end up with
I.e. because we have gone through a quick OO design process the question about, “should we re-use OnEnemyDied” is no longer even a thought. We have a system where the separate events simply fell out of the design, we did not have to make any confusing decisions.
Realistically smaller Unity apps are going to encourage us to skip traditional Software Craftsmanship because, rightly so, we are being pragmatic given the software we are writing. I believe that this is the correct and appropriate. However, this pragmatism does come at a cost. The cost is skipping the fundamentals of OO design, or more specifically the modeling. When you find yourself feeling a bit itchy about a design choice, a bit queasy at the thought of ignoring an event argument, then pause for a moment and consider carrying out a quick mental OO design process. Treat the components as separate modeled entities. Think about what they actually need to exist. Sometimes stopping for a moment can save a lot of pain further down the trail.