Clean Architecture: Add observer pattern and conclusion

This commit is contained in:
rubenwardy 2018-07-15 16:51:37 +01:00
parent 5ee799b76b
commit bc58dce630
No known key found for this signature in database
GPG Key ID: A1E29D52FF81513C

View File

@ -19,7 +19,10 @@ There is no one good way of designing a mod, and good mod design is very subject
* [Cohesion, Coupling, and Separation of Concerns](#cohesion-coupling-and-separation-of-concerns)
* [Model-View-Controller](#model-view-controller)
* [API-View](#api-view)
* [API-View](#api-view)
* [Observer](#observer)
* [Conclusion](#conclusion)
## Cohesion, Coupling, and Separation of Concerns
@ -172,7 +175,7 @@ you tend to see a lot more of a less formal and strict kind of design -
varients of the API-View.
## API-View
### API-View
In an ideal world, you'd have the above 3 areas perfectly separated with all
events going into the controller before going back to the normal view. But
@ -192,3 +195,61 @@ are views for each type of thing.
Separating the mod like this means that you can very easily test the API part,
as it doesn't use any Minetest APIs - as shown in the
[next chapter](unit_testing.html) and seen in the crafting mod.
## Observer
Reducing coupling may seem hard to do to begin with, but you'll make a lot of
progress by splitting your code up well using a design like the one given above.
It's not always possible to remove the need for one area to communicate with
another, but there are ways to decouple anyway - one such way being the Observer
pattern.
Let's take the example of unlocking an achievement when a player first kills a
rare animal. The naive approach would be to have achievement code in the mob
kill function, checking the mob name and unlocking the award if it matches.
This is a bad idea however, as it makes the mobs mod coupled to the achievements
code. If you kept on doing this - for example, adding XP to the mob death code -
you could end up with a lot of messy dependencies.
Enter the Observer pattern. Instead of the mobs mod caring about awards, mobs
exposes a way for other areas of code to register their interest in an event
and receive data about the event.
{% highlight lua %}
mobs.registered_on_death = {}
function mobs.register_on_death(func)
table.insert(mobs.registered_on_death, func)
end
-- mob death code
for i=1, #mobs.registered_on_death do
mobs.registered_on_death[i](entity, reason)
end
{% endhighlight %}
Then the other code registers its interest:
{% highlight lua %}
-- awards
mobs.register_on_death(function(mob, reason)
if reason.type == "punch" and reason.object and reason.object:is_player() then
awards.notify_mob_kill(reason.object, mob.name)
end
end)
{% endhighlight %}
You may be thinking - wait a second, this looks awfully familiar. And you're right!
The Minetest API is heavily Observer based to stop the engine having to care about
what is listening to something.
## Conclusion
Good code design is subjective, and depends on the project you're making. As a
general rule, try to keep cohesion high and coupling low. Phrased differently,
keep related code together and unrelated code apart, and keep dependencies simple.
I highly recommend reading the [Game Programming Patterns](http://gameprogrammingpatterns.com/)
book. It's freely available to [read online](http://gameprogrammingpatterns.com/contents.html)
and goes into much more detail on common programming patterns relevant to games.