Build a Clean “Game of Life” App in Flutter Using Hexagonal Architecture and TDD: Part 2

In this article, I code the business logic of the “Game of Life” using a TDD approach.

Romain Straet
The Startup
10 min readAug 1, 2020

--

You can find Part I of the series here.

What is Test-Driven Development (TDD)?

Once again, I’m only going to talk very briefly about TDD since it is already well explained here, here or here.

Basically, TDD consists of writing tests first, writing just enough code to make the tests pass then refactoring it.

I follow here an “inside-in” approach where I start at the lowest level possible. First, I test and code the smallest part of the logic. Then I gradually increase the complexity of the test scenarios.

TDD has several benefits, mainly :

  • It helps you design your code ;
  • It makes you way more confident about your code ;
  • It helps you make change with confidence ;
  • Etc.

Coding the “hexagon”

As explained in Part 1, the business logic of the game of life should be isolated in the « hexagon ». It should be totally independent from the outside and therefore framework-agnostic (it should only rely on pure Dart) and implementation-agnostic (it should only rely on the interfaces).

Therefore, what better way than a pure Dart package to ensure the isolation of the game logic ?

Folder Structure : Project level

On the highest level, I have the above folder structure where :

  • “game_of_life_core” is the package containing the business logic (i.e. the “hexagon” that I implement in this article) ;
  • “game_of_life_mobile_app” will be the flutter app (i.e. the “primary actor” of the Hexagon Architecture);
  • “game_of_life_repository” will be a package containing the implementation of the repository interface that will be provided by the “game_of_life_core” package.

Folder Structure : Core package level

This is the folder structure of the Core package where :

  • “lib” contains all the source code organized according to the hexagon structure. Note that the game_of_life_core.dart file will be the file responsible for showing what you want to show to the outside world (mainly the use cases and the interfaces - see the end of this article) ;
  • “test” contains obviously all the tests which are organized according to the same structure as the lib folder.

Initializing the package

To create a pure Dart package, you just have :

  • to create a “pubspec.yaml” file ;
  • add the following :
  • then run “pub get” in your terminal.

And that’s it. You will now be able to call this package as every other Dart packages. If you want more information about this topic, you can find some here.

Quick refresh

Remember, in Part 1 we defined the use cases, i.e. the users need the “hexagon” to :

  • Get a random game ;
  • Get a list of available game ;
  • Generate the next state of a game based on the rules of the game of life.

Since the third one is the one that will actually contain the rules of the game of life. Let’s start with that one.

Use case 1 : Get the next state of a game

Let’s pause and think before writing anything. What do I need to implement the rules of the game of life ?

  • First, I need a way to express the state of a cell. It could be 0 and 1 but I want to be expressive so I’ll use an enum for this. Let’s code it right away since there is nothing to test here :
  • Obviously, considering the rules, I need a method that returns the next state of a cell depending on its initial state and the number of alive neighbours;
  • Therefore, I also need a method that counts the number of alive neighbours of a given cell;
  • Finally, of course, I need to iterate through the cells of the provided game, apply the new state of each cells, and returns a new refreshed game.

Let’s start with the smaller piece and work on the Cell level.

Get the next state of a Cell

According to the TDD approach, let’s start by writing a test.

Here is the very first test I wrote for this method. In fact, I’m just following the rules of the game of life.

You will notice I already took some decisions here :

  • I will code a Cell class that requires a CellState as argument ;
  • The Cell class will contain a getNextState method that will require the number of alive neighbours as argument.

For the fun of strictly following the TDD approach, let’s run the test in a terminal with the following command :

The result :

This is expected since we have not even created that Cell class yet.

So let’s write the smallest amount of code required to make the test passes :

Let’s verify :

Good! Let’s write a second test and run the tests.

It failed once again. Let’s adjust the code to make it pass :

Run the tests…

Great ! Third test (then I’ll take some shortcuts for this article ;-) :

Run the tests…

Oh wait… It passed without any changes. Great!

Finally, I wrote the following tests (one by one) and tried to make them pass (one by one*).

And here is the final result.

Count the number of alive neighbours

As stated above, in order to get the next state of a cell, I need to count and provide the number of alive neighbours of the cell.

In this respect, I took the following decisions :

  • I will code a Game class that requires a state as argument. The state is the game grid that will be represented as a List of List of CellState* ;
  • The Game class will contain a countAliveNeighboursOfCell method that will require the row and the col index as arguments.

* By the way here is an illustration of a game grid in code. Each list inside the root list is a row. Inside the rows are the cells. First cell is in the first column, second cell is in the second column, etc.

As per TDD approach, I wrote the below tests one by one and try to make it pass one by one. You’ll notice that the complexity of the tests increase progressively.

  1. Given a 2 by 2 game grid with only dead cells, each cells of the game should have 0 alive neighbours ;
  2. Given a 2 by 2 game grid with only alive cells on the top row, the bottom cells of the game should have 2 alive neighbours ;
  3. Given a 2 by 2 game grid with only alive cells on the bottom row, the top cells of the game should have 2 alive neighbours ;
  4. Given a 2 by 2 game grid with only alive cells, each cells of the game should have 3 alive neighbours ;
  5. Given a 3 by 3 game grid with only alive cells :
    - Corner cells should have 3 alive neighbours ;
    - Top/Right/Bottom/Left-center cells should have 5 alive neighbourg ;
    - Center cell should have 8 alive neighbours ;
  6. Given a 4 by 4 game grid with alive and dead cells, random tested cells should have the correct number of neighbours.

This battery of tests can be found here.

And here below is the result. It might not be perfect but it passes the tests and therefore gives a solid foundation.

Get the next state of a game

I have now to link everything to implement the use case.

Once again, I follow the TDD approach. I therefore wrote the below tests one by one and tried to implement it one by one :

  1. Given a 2 by 2 game grid with only dead cells, the next state of the game should only contains dead cells ;
  2. Given a 2 by 2 game grid with only alive cells, the next state of the game should only contains alive cells ;
  3. Given a 2 by 2 game grid with only 1 alive cell, the next state of the game should only contains dead cells ;
  4. Given a 2 by 2 game grid with 3 alive cells, the next state of the game should only contains alive cells ;
  5. Given a 3 by 3 game grid with only alive cells, the next state of the game should evolve according to the rules of the game of life (here : only corner cells should remain alive) ;
  6. Given a 4 by 4game grid with dead and alive cells, the next state of the game should evolve according to the rules of the game of life.

This battery of tests can be found here.

In practice, to make those tests pass, I create a GetNextGameState class in the usecases folder with a call method.

Here below is the result. In the call method :

  • I create an instance of the Game class with the provided game ;
  • I iterate over each cells and count their alive neighbours ;
  • Given this information, I get the next state of each cells ;
  • I add those cell state in a new game variable ;
  • When iteration is finished, it returns the new game state.

One again, it might not be perfect but this is a solid foundation.

Let’s move on.

Use case 2 : Get a random game

A this stage, you should understand the approach. Let’s move on quickly.

Here are the test I wrote (one by one) :

And here is the implementation :

In short, GetRandomGame().call() returns a 30 by 30 grid game by default (could be customized) with random CellState (but with a bit more dead cells than alive cells).

Use case 3 : Get all games

Basically, this use case is a GetAllGames class (in usecases folder) with a call method.

The call method has itself to call a repository to get all the games available.

Of course, the repository could be a SQL database, a NoSQL database, a “in memory” repository, etc.

However, considering the “hexagon” approach, I do not want the business logic to be dependent of the repository chosen/implementation.

Therefore, I have to code and provide an interface, i.e. an abstract class in Dart, instead of an implementation. Here is the abstract class :

This means that :

  • If a user has to call a use case that requires a game repository (i.e. the GetAllGames use case), he needs to implement this GameRepository abstract class ;
  • In his class, the user will have to implement a getAllGames method that returns a Future with a List of games organized in a key-value pair style as below :

Finally, here is the GetAllGames use case class :

  • This class requires a implemented GameRepository as argument ;
  • The Call method itself calls the getAllGames method of the GameRepository ;

Note that the GameRepository interface has been defined by the “hexagon”. Therefore, the GetAllGames use case already knows that, regardless of the how the user implements the game repository, the latter will always have a “getAllGames” method that will always returns values with the correct types (and if not : it’s not the problem of the “hexagon”).

Hence, the “hexagon” is clearly isolated from the outside world and the implementation choices.

What about the tests ? Well I just have to mock the repository and make sure the appropriate method was called.

Simple, right ?

All Use Cases in a single Class

I like to provide each use cases separately but also in a single class with all the use cases. Here how I simply do :

Wiring everything

Now that I have all the use cases, interfaces and entities, I need to provide them to the outside world.

In practice, it’s just a Dart file, placed in the lib folder, exporting everything you need to export.

Obviously, I need to export the use cases, the repository interface as well as the CellState entity (in other words, the outside world should not need to dig into the other entities).

That’s it.

We have isolated the business logic in a package that relies on absolutely nothing else than the dev dependencies (for testing purposes).

The “primary actors” can use this package the way they want. The repository can be implemented anyhow. It doesn’t matter, it will have no impact on this package.

Note that you can find the package on github here.

Next up

Now I have the Business Logic isolated in a Dart package, the next and final article will focus on building the Flutter App that will actually show the Game of Life in action.

Stay tuned.

--

--