Lessons Learned: Actor Model Thinking in an Actor Model Architecture

CodeGuru content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

Allow me share an embarrassing secret. When it comes to working in a pure Actor Model system such as Akka.NET, my thinking has been all wrong. I’ve worked with messages before. I am familiar with message queues such as MSMQ and RabbitMQ. I’ve worked in message-driven systems. But, when it comes to the formality of the Actor Model in Akka.NET, I’ve been missing a key point. Here is the point I have been missing. In Akka.NET or any Actor Model for that matter, everything is an actor and actors communicate using messages. Let me repeat this point, because it’s important:

Everyting is an actor and actors communicate using messages.

Everything Is an Actor

Now, it may seem as I am overstating the obvious. But I am not. My mouth has been saying this for a while, but often my mind thinks otherwise. How, you may ask? Consider the following code. It’s an example of an Actor class, MathActor, and a message that gets sent to the actor, MathOperationMessage. Notice please the Console.Write(....) statement toward the end of the MathActor class, in bold.

using System;
using Akka.Actor;

namespace MessageThinkingLib.Actors
{
   /// <summary>
   /// A simple <see cref="Akka.Actor"/>
   /// </summary>
   public enum MathOperation { Add, Multiply,Subtract, None}
   public class MathActor:UntypedActor
   {
      protected override void OnReceive(object message)
      {
         var op = message as MathOperationMessage;
         if (op != null)
         {
            var answer = 0;

            // seed the answer to not mess up the multiplication
            if (op.Operation.Equals(MathOperation.Multiply)) answer = 1;

            foreach (var number in op.Numbers)
            {
               if (op.Operation.Equals(MathOperation.Add)) answer =
                  answer + number;
               if (op.Operation.Equals(MathOperation.Subtract)) answer =
                  answer - number;
               if (op.Operation.Equals(MathOperation.Multiply))
               {
                  answer = answer * number;
               }

            }

            Console.WriteLine("The operation is [{0}] and the
               answer is [{1}]", op.Operation,answer);
         }
      }
   }

   /// <summary>
   /// A message that describes a Math Operation Messsage
   /// </summary>
   public class MathOperationMessage
   {
      public MathOperationMessage(int[] numbers, MathOperation operation)
      {
         Numbers = numbers;
         Operation = operation;
      }
      /// <summary>
      /// An array of numbers to process
      /// </summary>
      public int[] Numbers { get; private set; }
      /// <summary>
      /// The operation to apply to the numbers
      /// </summary>
      public MathOperation Operation { get; private set; }
   }
}

Listing 1: The MathActor does addition, subtraction, or multiplication upon an array of integers

Listing 1 demonstrates that I have failed to really understand that everything is an actor. Take a look at this line of code:

Console.WriteLine("The operation is [{0}] and the answer
   is [{1}]", op.Operation,answer);

Why is this a problem? It’s about the separation of concerns and containment.

Actors Are about the Separation of Concerns and Containment

The concern of the actor, MathActor, is to perform math operations. However, for some reason I am writing to the console in MathActor. Do I have a need to do console writing? Maybe. Is console writing a reasonable activity for an actor that has the purpose of doing math operations? I don’t think so. Remember again, in an Actor model, everything is an actor. Yet, I have a thing in the MathActor that is not an actor. That thing is the Console.

There is a very good case to be made that writing output to the console is the purpose of another actor, not MathActor. Thus, I will do well to create an actor that does console writing and that new actor is the place where Console should live.

Please take a look at the code in Listing 2. It’s a revision of the method, MathActor.OnReceive(object message). Notice please that that Console.WriteLine(...) is commented out and in its place is an actor, ConsoleWriterActor.

      protected override void OnReceive(object message)
      {
         var op = message as MathOperationMessage;
         if (op != null)
         {
            var answer = 0;
            // seed the anwser to not mess up the multiplication
            if (op.Operation.Equals(MathOperation.Multiply))
               answer = 1;
            foreach (var number in op.Numbers)
            {
               if (op.Operation.Equals(MathOperation.Add)) answer =
                  answer + number;
               if (op.Operation.Equals(MathOperation.Subtract)) answer =
                  answer - number;
               if (op.Operation.Equals(MathOperation.Multiply))
               {
                  answer = answer * number;
               }

            }

            var output = String.Format("The operation is [{0}] and
               the answer is [{1}]", op.Operation, answer);

            var writer = Context.ActorOf(Props.Create(() =>
               new ConsoleWriteActor()), "MyWriter");
            var writerMessage = new WriterMessage(CommandType.Custom,
               sConsoleColor.Cyan,output);
            writer.Tell(writerMessage);

            //Console.WriteLine("The operation is [{0}] and
            //the answer is [{1}]", op.Operation,answer);
         }
   }

Listing 2: The ConsoleWriteActor‘s concern is to send output to the console

Listing 3 shows the code for ConsoleWriterActor.

using System;
using Akka.Actor;
using MessageThinkingLib.Messages;

namespace MessageThinkingLib.Actors.Utilities
{
   public class ConsoleWriteActor : ReceiveActor
   {
      public ConsoleWriteActor()
      {
         ReceiveAny(x =>
         {
            CommandType commandType;
            var msg = x as WriterMessage;
            var s = string.Empty;
            if (msg == null)
            {
               Console.ForegroundColor = ConsoleColor.Red;
               s = "Bad Message Format";
               commandType = CommandType.Error;
            }

            else
            {
               s = msg.Output;
               Console.ForegroundColor = msg.OutputColor;
               commandType = CommandType.Ok;
            }
            Console.WriteLine(s);

            Sender.Tell(new WriterMessage(commandType,
               Console.ForegroundColor, s));
            Console.ForegroundColor = DefaultColor;
         });
      }

      public ConsoleColor DefaultColor
      {
         get { return ConsoleColor.White; }
      }
   }
}

Listing 3: The ConsoleWriterActor provides console writing services to other actors

As you can see, I’ve separated the concern of doing math operations from the concern of writing output to the console. Each concern has its own actor. MathActor does math. ConsoleWriterActor does console writing. The way MathActor talks to ConsoleWriterActor is by way of messages, which is a it should be.

The Elephant in the Living Room

But still, the question remains: Why am I writing to the console in the first place? The reason is because I am being evil. I am writing console output to determine that MathActor is working properly. Why is this evil? Because I am, for all intents and purposes, testing my code using visual inspection of console output. This is just fundamentally bad business. I should be writing unit tests.

The good news is that the folks who thought up Akka.NET created a unit testing framework for the platform. So, I have a way to formally test my actors using unit tests under Visual Studio. But, before I can apply the Akka.NET unit testing framework to my code, I need to make my code testable.

Let’s go back to the original MathActor code in Listing 1. Notice, please, that although the actor does computation, it has no capability to report the result of its computation. This is flawed code. What good is a calculator that takes input but provides no output? We need to return the result of a calculation back to the actor that is messaging the MathActor. So, let’s create a message, MathAnswerMessage, that will contain the answer created by the MathActor. The message, MathAnswerMessage, is shown in Listing 4.

namespace MessageThinkingLib.Messages
{
   public class MathAnswerMessage
   {
      public MathAnswerMessage(decimal answer)
      {
         Answer = answer;
      }
      public decimal Answer { get; private set; }

   }
}

Listing 4: The MathAnswerMessage is sent by the MathActor to actors that want the result of calculations

Now that we have a way to notify an actor of a MathActor calculation result, we need to pass the MathAnswerMessage back to the sending actor. Listing 5 shows a revision of MathActor.OnReceive(object message). Notice that the method puts the results of a calculation into a MathAnswerMessage, and the message is returned back to the Sender via Sender.Tell(...).

using Akka.Actor;
using MessageThinkingLib.Messages;

namespace MessageThinkingLib.Actors
{
   /// <summary>
   /// A simple <see cref="Akka.Actor"/>
   /// </summary>
   public enum MathOperation { Add, Multiply,Subtract, None}
   public class MathActor:UntypedActor
   {
      protected override void OnReceive(object message)
      {
         var op = message as MathOperationMessage;
         if (op != null)
         {
            var answer = 0;
            // seed the anwser to not mess up the multiplication
            if (op.Operation.Equals(MathOperation.Multiply)) answer = 1;
            foreach (var number in op.Numbers)
            {
               if (op.Operation.Equals(MathOperation.Add)) answer =
                  answer + number;
               if (op.Operation.Equals(MathOperation.Subtract)) answer =
                  answer - number;
               if (op.Operation.Equals(MathOperation.Multiply))
               {
                   answer = answer * number;
               }

            }

            //Create an answer message and....
            var answerMessage = new MathAnswerMessage(answer);
            //... send it back to the actor that sent the message
            Sender.Tell(answerMessage);
         }
      }
   }
}

Listing 5: The result of a calculation done by MathActoris returned to the sender using a MathAnswerMessage

Now that we have communication going in and out of the MathActor, we need to test the actor. This is where the Akka.NET TestKit comes into play. The Akka.NET TestKit is big and very comprehensive. You can read the details about it in the preceding link.

For now, we are interested in being able create a MathActor and a MathOperationMessage within a unit test. We want to pass the MathOperationMessage to the MathActor using .Tell(). Then, we want to ensure that MathActor sends its MathAnswerMessage and that the answer within MathAnswerMessage is correct.

Here is the test that verifies that the MathOperation.Add works.

      private readonly int[] _numbers = {1, 2, 3, 4};

      [TestMethod]
      public void MathActorAddTest()
      {
         var message = new MathOperationMessage(_numbers,
            MathOperation.Add);

         //Create the ActorSystem
         var system = ActorSystem.Create("MySystem");
         //Create the MathActor
         var actor = system.ActorOf(Props.Create(() => new MathActor()),
            "MyMathActor");
         actor.Tell(message);
         //Let the testing framework get an answer message
         var answer = ExpectMsg<MathAnswerMessage>();
         //Make sure it's correct. (yeah, hardcoding is bad business)
         Assert.AreEqual(answer.Answer, 10);
      }

Listing 6: Testing the MathActor with a MathAnswerMessage using the Akka.NET TestKit

Notice, please, ExpectMsg<T> toward the end of Listing 6. ExpectMsg<T> is provided by the Akka.NET TestKit. The nice thing about the ExpectMsg<T> method in the TestKit is that it will automatically detect when an actor sends a return message and allows you to assign the returned message to a variable. If the desired message is not sent back from the actor, the test fails. When you do get the desired returned message, you can inspect it.

So, What Have We Done?

The first thing we’ve done is to reiterate the fundamental principle of the Actor Model: Everything is an actor and actors communicate using messages. Then, I showed you that I completely ignored this principle by using Console.WriteLine() to do ad hoc visual testing. I showed you how I revised my thinking to be consistent with the Actor Model. I separated concerns. Instead of using Console.WriteLine(...), I created a ConsoleWriterActor and sent messages to it to facilitate sending output to the console.

Lastly, I revealed a bigger problem. I was NOT using unit tests to validate my actor code. To address my shortcoming, instead of visually inspecting console output to ensure proper operation of the actor, I implemented the Akka.NET TestKit to test my code.

What Did I Learn?

  • I learned that the fundamental principle of the Actor Model, that everything is an actor, and that actors communicate using messages, is not a nice to have feature. It is an essential concept.
  • I learned to catch myself when I start implementing behavior in an actor that is not essential to the actor’s concern.
  • I relearned to use unit testing when writing code. Any code that is written should be testable concretely, repeatedly, and beyond visual inspection.

These lessons were important to for me to (re)learn. The Actor Model is not usual Object-Oriented Programming by another name. It is a distinct way of programming and, more importantly, a particular way of thinking. As you move into more advance topics, it is essential that you understand that in the Actor Model, everything is an actor and actors communicate using messages and that you think accordingly. The basic lesson will serve you will as you progress down the road of developing with the Actor Model and Akka.NET.

You can get the code for this article on GitHub here.

Further Reading

How To Unit Test Akka.NET Actors With Akka.TestKit

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read