Attend QCon Plus online conference (May 10-20) and find practical inspiration from software leaders. Register
Facilitating the Spread of Knowledge and Innovation in Professional Software Development
Avdi Grimm describes the future of development, which is already here. Get a tour of a devcontainer, and contrast it with a deployment container.
The panelists reflect on various microservices topics.
Monte Zweben proposes a whole new approach to MLOps that allows to scale models without increasing latency by merging a database, a feature store, and machine learning.
In this article we will be sharing our experience learned from 12 months of adopting certain management and organisational insights from the book Team Topologies. It explores how we identified areas of responsibility and assigned those into mostly customer facing domains which could be given to our teams. It shows how an inverse Conway manoeuvre can be used to improve the architecture.
The panelists discuss the security for the software supply chain and software security risk measurement.
Uncover emerging trends and practices from software leaders. Attend online on May 10-20, 2022.
Learn how cloud architectures achieve cost savings, improve reliability & deliver value. Register Now.
Understand the emerging software trends you should pay attention to. Attend in-person on Oct 24-28, 2022.
InfoQ Homepage Articles Exploring Architectural Concepts Building a Card Game
One of the things I missed during the pandemic were my friends, the possibility to meet them, discuss with them and, why not, play some cards with them.
Zoom could partially work as a substitute for physical presence, but what about cards? What about our games of Scopone*?
So I decided to implement an app to play Scopone with my friends and, at the same time, test “in the code” some architectural concepts which had been intriguing me for some time.
D2iQ: The Leading Independent Kubernetes Platform. Learn more.
All the source code of the app can be found in this repo.
An interactive card game app involving more than one player has to have a client part and a server part. The server part has to reside somewhere in the Cloud. But where in the Cloud? As a component running on a dedicated server? As a Docker image on a managed Kubernetes? As a serverless function?
I did not know which was the best option but I wanted to check whether it was possible to maintain the core of the logic of the game independent from the deployment model I would have eventually chosen.
“Angular is best”. “No, React is much superior and faster”. I read too much of this. But is it really relevant? Shouldn’t we have most of the Front End logic as pure Javascript or Typescript code completely independent from the UI framework or library which we will eventually end up using? I had the feeling this was possible, but wanted to try it for real.
A card game, as other interactive applications nowadays, has multiple users interacting with each other in real time via a central server. For instance, when one plays a card, all others need to see in real time the played card. At the beginning it was not clear to me how to test this type of application. Would it have been possible to test it automatically with simple Javascript testing libraries like Mocha and standard testing practices?
The Scopone app represented a good ground to try to answer in a concrete way the questions I had. So I decided to try to implement it and see which lessons I could draw from it.
Scopone is a traditional italian card game which is played by 4 players, split in 2 teams of 2, with a 40 cards deck.
At the start of the game all players are given 10 cards each and the first player plays the first card which is put on the table face up. The second player then plays its card. If this card has the same rank as the card on the table, the second player “takes” the card from the table. If no cards are left on the table, the player taking the cards scores a “scopa”. Then the third player plays its card and so on and so forth until all cards have been played.
Enough of rules. The key point to remember here is that when a player plays a card, they change the state of the game, for instance in terms of “which cards are face up on the table” or “which player can play the next card”.
The Scopone app requires one server instance and four client instances that are launched by the four players from their devices.
If we look at the interactions among the various elements of the game, we note that
This means that the clients and the server need a two-way communication protocol, since the clients have to send commands and the server needs to push the updated state. WebSockets is a popular protocol suitable to this aim and available in various languages.
The server is implemented in Go since it has good support for WebSockets and fits well with the different deployment models at hand, in other words it can be deployed as a dedicated server, as Docker image, or as a Lambda.
The client is a browser-based application implemented in two different flavors: one with Angular and one with React. Both versions use Typescript and leverage a reactive design implemented via RxJs.
The following diagram represents the general architecture of our game app.
In a nutshell, the app works like this:
This cycle is repeated until the game is over.
The server receives messages representing commands sent by clients. Based on these commands, it updates the state of the game and sends messages to clients with the new updated state.
A command is a message sent by a client via a websocket channel which is transformed into the invocation of a specific API of the server.
The response produced by the invocation of the API is a picture of the new state which is transformed into a set of messages that have to be sent to each client over the websocket channel.
So, in the server implementation there are two distinct layers with distinct responsibilities: the game logic layer and the websockets mechanics layer.
This layer is responsible for implementing the game logic, that is to update the state of the game based on the command received and return a picture of the new state to be sent to each client.
This layer therefore can be implemented with an internal state and a set of APIs implementing the command logic. The APIs return the new state that has to be communicated to the clients.
This layer is responsible for transforming a message received over the websocket channel into the invocation of the corresponding API with the expected parameters. Additionally, it transforms the updated state, received as a response from the API invocation, into the set of messages that have to be pushed to the respective client.
Based on the previous discussione, the game logic layer is independent from the concept of websocket. It is just a set of APIs returning a state.
The websockets mechanics layer, on the other hand, is where the websockets specificities are implemented. This layer will depend on the specific deployment model chosen.
For instance, if we decide to deploy as a dedicated server, we will have to deal with the specific package chosen to implement the websocket protocol (in our case the Gorilla package), while if we decide to deploy as an AWS Lambda function, we have to rely on the Lambda implementation of the websocket protocol.
If we keep the game logic layer strictly separated from the websockets mechanics layer, with the latter importing the former (and not vice versa) we are sure that we can use the game logic layer regardless of the specific deployment model chosen.
Applying this strategy, it was possible to develop a single version of the game logic with the freedom to deploy the server where most convenient.
This brought several advantages. For instance, during the development of the client it is very convenient to run against a local Gorilla websocket implementation, maybe even launched in debug mode from within VSCode. This makes it possible to place breakpoints in the server code and step through the logic triggered by the various commands sent by the clients while playing a real game.
When the time came to deploy the server for production, which was more convenient for real play with my friends, it was possible to deploy the same game logic to the Cloud, for instance to Google Application Engine (GAE).
Furthermore, when I discovered that Google was charging a minimum fee regardless of whether we played or not (GAE always keeps at least one server on), I decided to move the server to AWS Lambda for a full “on demand” model with no change in the game logic code.
And then came the big question: Angular or React?
But then I also asked myself another question: is it possible to code most of the client logic as pure Typescript, independent of which framework or library will be used to manage the view part of the Front End?
It turned out that it is possible, at least in this case, with some interesting benefits as side effects.
At the base of the design of the Front End part of the app there are three simple ideas:
A player can play a card by clicking on it (suits in the picture are traditional north-eastern italian)
To make it more concrete, let’s consider what it means to play a card.
Let’s assume Player_X is the player that is to play the next card. Player_X clicks on the card “Ace of Hearts” and this UI event triggers the action “Player_X has played Ace of Hearts”.
These are the steps the application goes through:
View layer and service layer interactions
Following these rules we end up building “light components”, which manage only the UI concerns (presentation and UI event handling) and “heavy services” where all of the logic is kept.
The most important consequence though is that the “heavy services”, which contain most of the logic, are completely independent from the UI framework or library used. There is no dependency on either Angular or React.
More details on the way the UI layer works can be found at the end of the article.
Which are the benefits of such an approach?
Certainly not the portability between different frameworks and libraries. Once Angular is chosen it is unlikely that someone wants to switch to React and vice versa. But there are still advantages.
A first advantage of such an approach is that, if implemented thoroughly, it standardizes the way we develop the Front End and makes it easier to reason about it. At the end of the day, it is just another way to design a unidirectional flow of information using a bespoke store (the service layer is just a bespoke store). Being bespoke has the advantage of a lower level of abstraction and greater simplicity at, maybe, the cost of a bit of “reinventing the wheel” feeling.
The biggest benefit though lies in better and easier testability of the application.
Testing UI is complex, no matter which framework or library you use.
But if we move most of the code to a pure Typescript implementation, testing becomes easier. We can test the core logic of the application using standard testing frameworks (in our example we use Mocha) and we can also approach complex testing scenarios in a relatively simple way, as we discuss in the next and last section.
We have seen that Scopone is a game played by four players.
Four clients have to be connected simultaneously to a central server via WebSockets. Actions performed by one client, for instance “play a card”, trigger updates (side effects) on all clients.
This is an interactive real time multi-user scenario. This means that we need to have multiple clients and a server running at the same time if we want to test the behavior of the app in its entirety.
How can we test such scenarios automatically? Can we test them with standard testing Javascript libraries? Can we test them on a standalone developer workstation? These were the questions to answer next. It turns out that all these things are possible, at least up to a large extent.
Let’s imagine, with a simple example, that we want to test the correct distribution of the cards among all players at the beginning of a game. As soon as a new game is started, all clients receive ten cards each from the server (the Scopone deck is composed of 40 cards of which each player gets ten).
If we want to test this behavior automatically from a single standalone machine (say, the developer’s machine) we need a local server. This is possible since the server can run locally as a Container or a WebSockets server. So we assume to have a local server up and running on our machine.
But, for the test to run, we need also to find a way to create the right context assumed by the test and launch the action that triggers the side effects we want to test (the distribution of the cards to the players is a side effect of the fact that one player has started the game). In other words we need to find a way to simulate the following:
Only then can we check whether the server sends the expected cards to all players.
A test in a multi user scenario
Each client is composed of a view layer and a service layer.
The APIs of the service layer (methods and Observable streams) are defined in a class (called ScoponeServerService in the implementation code).
Each client creates an instance of this service class and connects it to the server. The view layer interacts with its instance of the service class.
Therefore, if we want to simulate four clients, we have to first create four different instances of the service class and connect all of them to our local server.
Create 4 service class instances representing 4 clients
Now that we have four clients available and connected, we need to build the right context for the test. We need four players and we need that each of them joins the same game.
Setting the context for the test
After we have created the four clients and built the right context, we can run the core of the test. We can have one player sending the command to start the game and then we can check that each player receives the expected amount of cards.
The test of an interactive multi-user scenario is a function that:
We can see this approach as a form of behavior-driven development (BDD) performed against the APIs offered by the service layer.
The behavioral specifications are provided, in line with the BDD approach, such as:
The function representing the test is written with a sort of DSL which is composed of ad-hoc helper functions whose combination sets up the context (an example of helper function is playersJoinTheGame).
This is not a full end-to-end test. We are not testing the view layer.
But it can still be a very powerful tool, especially if we stick to the rule “Light components and heavy services”.
If the view layer is made up of light components and most of the logic is concentrated in the service layer, then this approach allows us to cover the core of application behavior, both on the client and on the server side, with a relatively simple setup, pretty standard tools (we use Mocha as testing library, definitely not the latest shiniest thing) and on a standalone developer’s machine.
The net benefit is that developers can create test suites that are fast to run and therefore can be executed often. At the same time, such test suites are really testing the entire application logic, from client to server, giving a high level of confidence even with a multi-user real time application.
Building a card game app has been an interesting experience.
Apart from bringing some relief during the darkest days of the pandemic, it gave me the opportunity to explore some architectural concepts with some code.
We often use architectural concepts as abstractions to express our point of views. I find that looking at these concepts in action, even in simple proof-of-concept scenarios, increases our understanding of them and our level of confidence when we eventually use them in a real project.
The components of the View layer do two things:
To be more concrete about what the last point means, we can look at one example of logic: how to determine who is the player that can play the next card.
As we said, one rule of the game is that players can play cards one after the other. So, for instance, if Player_X is the first player and Player_Y is the second, after Player_X plays a card only Player_Y can play the next card. All other players can not play any cards. This information is part of the state which is kept by the server.
Any time a card is played the server sends a message to all clients specifying which is the next player.
The service layer turns this message into a notification over an Observable stream called enablePlay$. If the message says that the player can play the next card, the service layer will notify true over enablePlay$ otherwise false.
The component that enables for a player the possibility to play a card has to subscribe to the enablePlay$ stream and react accordingly to the data notified.
In our React implementation this is the functional component Hand. This component defines a state variable, enablePlay, that governs the possibility of playing a card. The Hand component subscribes to the enablePlay$ Observable in an effect hook and, any time it receives a notification from enablePlay$, it sets the value of enablePlay triggering the redraw of the UI.
The relevant code for this particular functionality implemented by the Hand component using React is the following.
The Angular counterpart to this example is logically identical and is implemented in HandComponent. The only difference is that the subscription to the enablePlay$ Observable is made directly in the template via the async pipe.
Becoming an editor for InfoQ was one of the best decisions of my career. It has challenged me and helped me grow in so many ways. We'd love to have more people join our team.
A round-up of last week’s content on InfoQ sent out every Tuesday. Join a community of over 250,000 senior developers. View an example
You need to Register an InfoQ account or Login or login to post comments. But there's so much more behind being registered.
Get the most out of the InfoQ experience.
Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p
Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p
Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p
A round-up of last week’s content on InfoQ sent out every Tuesday. Join a community of over 250,000 senior developers. View an example
Real-world technical talks. No product pitches. Practical ideas to inspire you and your team. QCon Plus - May 10-20, Online. QCon Plus brings together the world's most innovative senior software engineers across multiple domains to share their real-world implementation of emerging trends and practices. Find practical inspiration (not product pitches) from software leaders deep in the trenches creating software, scaling architectures and fine-tuning their technical leadership to help you make the right decisions.
InfoQ.com and all content copyright © 2006-2022 C4Media Inc. InfoQ.com hosted at Contegix, the best ISP we've ever worked with. Privacy Notice, Terms And Conditions, Cookie Policy