-
Notifications
You must be signed in to change notification settings - Fork 17
Actors Style Guide
status: Draft
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.
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.
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.
For example a PowerDistributingActor
can receive PowerDistributingActor.RequestMessage
s 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?
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
.
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
.
The messages defined by an actor should have a Message
suffix and be as clear as possible.
For example a PowerDistributingActor
can receive PowerDistributingActor.RequestMessage
s and send PowerDistributingActor.ResultMessage
messages.
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
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