Skip to content
Mathias L. Baumann edited this page Mar 10, 2023 · 2 revisions

status: Draft

Actors Style Guide

Introduction

This is a style guide, not an unbreakable truth. Unless there is a very good reason, we should stick to it so there is some consistency across the code and it is quite obvious what's going on by just reading a class name for example. But if something doesn't make sense at all for some particular case, it is OK to go around this guide. It is extremely recommended to add a comment in these cases, like:

# This class doesn't follow the naming convention because of <whatever>
class IHaveAWeirdName:
    ...

So other people reading the code can know this is by design and not just an overlook, or legacy code that needs to be renamed.

Design

Actors should be thin wrappers

Actors should always be a thin wrapper over a regular class. The actor should only focus on communication (passing messages via channels) and only forward those as calls to the underlying class instance.

This is because actors are basically workers (maybe even a separate process in the future). Sometimes we will want a worker using more than one algorithm.

For example, we first implement a InstantaneousPeakShavingActor. Then we need a more complicated way to do peak shaving and we implement a GmmPeakShavingActor. But then we realize that when the app starts, is good to use the instantaneous peak shaving, but once we gathered enough data, we switch to GMM peak shaving.

In this case we might want to merge both algorithms into one worker (actor), so we could have a PeakShavingActor that internally uses either a InstantaneousPeakShaver or a GmmPeakShaver class/algorithm based on the current circumstances. Of course this actor could also just use the other actors, but that would imply more message passing and possible more memory and CPU usage.

Actors should receive channel sender and receivers in the constructor

Instead of creating channels themselves, actors should receive channel sender and receivers used for communications in the constructor.

This way we avoid circular dependencies is actors need to communicate bidirectionally or initialization conflicts. First all channels are created and then the corresponding senders and receivers are passed to the actors.

Actors should define messages they send as attributes

For example a PowerDistributingActor can receive PowerDistributingActor.RequestMessages and send PowerDistributingActor.ResultMessage messages.

If a message is just an int or any other built-in type, a NewType should be defined to provide a meaningful name and get proper type checking that differentiate messages from values. For example:

from typing import NewType

class PeakShavingActor:
    AppliedPowerMessage = NewType('AppliedPowerMessage', int)
    ...

Open question: Should be a NewType or just a type alias?

Naming

Actor class name

Actors class name should have the form <noun><verb-participle>Actor. For example: TimeseriesAggregatingActor, PeakShavingActor, EvChargingActor, EnergyForecastingActor.

If the meaning is clear without a <noun>, it could be skipped (for example: DispatchingActor), but we should try as hard as possible to use a noun too to make it very explicit (what is the DispatchingActor dispatching?).

One related note, in general when CamelCasing, acronyms should only get the first letter upper-cased, like Ev instead of EV, otherwise is impossible to distinguish if EV is CamelCase or ALL_CAPS.

Underlying class name

Actors underlying classes should be named <noun><verb-as-noun>, using the same noun and verb as the actor.

Following the same examples: TimeseriesAggregator, PeakShaver, EvCharger, EnergyForecaster.

Message names

The messages defined by an actor should have a Message suffix and be as clear as possible.

For example a PowerDistributingActor can receive PowerDistributingActor.RequestMessages and send PowerDistributingActor.ResultMessage messages.

Output channel sender name

If an actor produces some output in a channel, the channel sender should be called output_sender:

@actor
class OutputProducingActor:
    def __init__(self, output_sender: Sender[OutputType]):
        self._output_sender = output_sender

Input channels receivers names

If an actor receives instructions from other actors via channels, the channel receiver should be called <actor-without-suffix>_<message-type-without-suffix>_receiver. For example if an input carries a PowerDistributingActor.ResultMessage, the receiver should be called power_distributing_result_receiver:

@actor
class InputConsumingActor:
    def __init__(self, power_distributing_result_receiver: Receiver[PowerDistributingActor.ResultMessage]):
        self._power_distributing_result_receiver = power_distributing_result_receiver