Hi and welcome to a SOLID example of implementing inversion of control (IOC) in TypeScript when working in distributed teams.
Photo by rawpixel on Unsplash
We performed this tennis refactoring Kata by Emily Bache yesterday at work.
Your task is to write a “TennisGame” class containing the logic which outputs the correct score as a string for display on the scoreboard. When a player scores a point, it triggers a method to be called on your class letting you know who scored the point. Later, you will get a call “score()” from the scoreboard asking what it should display. This method should return a string with the current score. [1]
Six developers trying to untangle code, wonderful!
Having the game’s nitty-gritty details inside the Kata, I’m just going to highlight its gameplay.
Player one starts strong and gets to “Thirty-Fifteen” in his favor, but player two summons his inner strength and wins!
We refactored the TennisGame class which exposed the wonPoint and getScore methods to the caller, keeping its tests green. During the refactoring, I felt concepts like player and score trying to emerge.
Later that evening I was drinking a beer, maybe more, with a friend, discussing Robert C. Martin’s SOLID principle, dependency inversion.
Two things struck me:
The Dependency Inversion Principle (DIP) tells us that the most flexible systems are those in which source code dependencies refer only to abstractions, not to concretions [2]
Building on Robert C. Martin’s explanation quoted above let’s highlight this design principle through a hopefully exciting journey.
Let’s implement the same tennis game described in the Kata, but with a distributed team working on it.
Working on the 25th floor in a New York City office building, Frank is in charge of the overall game and its integration in the company’s systems. He knows the importance of separating concerns in distributed projects.
Frank takes a cup of coffee, thinks about the tennis game and writes its interface.
The state of the game will consist of two player objects. Each player will have a name and a total number of points. With this information, Frank writes the Player interface.
Find out more about the concept of state and how it applies to modern single page applications in my article.
Next, Mr. Gamelord reads the requirements and realizes that he will need a way to manage players, ergo the ManagePlayers interface.
Looking out the window, sipping more coffee, the last piece of the puzzle unveils, the score!
Frank puts down his cup of coffee and rushes to the printer. He comes back with five contracts, each representing one of the interfaces he just modeled.
Frank’s job is to fulfill the TennisGame contract. Due to his busy schedule, he’ll have to delegate the rest.
Frank creates the project’s structure in a way that allows everybody to work independently. He will work in the gameplay folder, Bill in the player folder and Andy in the score one.
Notice how the gameplay folder holds all the interfaces while the player and score folders contain only implementations?
Mr. Gamelord creates the curried createGameFactory factory function. It expects a ManagePlayers object and a calculateScore function as arguments and returns another function. The later takes the names of the players as parameters, returning the game.
The createGameFactory does not import any concrete implementations. Its source code dependencies are the CalculateScore and MangePlayers interfaces.
Not depending on actual implementations, allows Frank to finish his work even before Bill and Any start theirs.
It’s a chilly British morning when Mr. Playwell starts working. He implements the five methods required by the ManagePlayers interface with pure functions and exports them as a whole.
Bill’s module has two abstract source code dependencies, the Player and ManagePlayers interfaces, exporting the laters implementation.
It’s a beautiful Icelandic morning when Mr. Scoreson thinks of a descriptive algorithm composed of pure functions. It allows him to achieve temporal decoupling. The order in which functions inside the calculateScore closure get called doesn’t matter.
Andy’s module has a single abstract dependency, the CalculateScore interface and it exports its implementation.
All the concrete implementations createGameFactory, PlayerManager, and calculateScore depend only on abstract interfaces, the mighty contracts.
Frank thinks proudly about his team and puts everything together in the createGame higher-order function.
The source code dependency graph looks like this:
Notice how the blue source code dependencies arrows of the createGameFactory point in the opposite direction of the red dependencies of the calculateScore and PlayerManager implementations?
IOC allows developers to work together without stepping on each other’s toes.
It enables temporal decoupling for the engineers. Frank finished his work before Bill and Andy. Andy could have started long after Bill without problems and source control conflicts.
It enables modules similar to PlayerManager and calculateScore to become plugins of the application.
If Andy thinks he can improve the scoring algorithm he can change it without affecting the rest of the app.
If you feel like sharing your thoughts with me, please do! Learning together is the reason I write!
Feel like wanting more? Watch Mattias Petter Johansson’s video on inversion of control.
For an academic presentation, you can purchase Robert C. Martin’s video (I am not an affiliate).
You can find the repo with the presented code here.
[1] https://github.com/emilybache/Tennis-Refactoring-Kata/blob/master/javascript/TennisTest.js
[2] Robert C. Martin, 2017, Clean Architecture: A Craftsman’s Guide to Software Structure and Design