Merge pull request #27337 from akka/wip-22805-style4-patriknw
Style Guide: enum msg, ask, tot fun, naming, meth ref, intro, #22805
This commit is contained in:
commit
d27be3fa48
5 changed files with 857 additions and 13 deletions
|
|
@ -6,18 +6,19 @@ package jdocs.akka.typed;
|
|||
|
||||
// #oo-style
|
||||
// #fun-style
|
||||
import akka.actor.typed.ActorRef;
|
||||
import akka.actor.typed.Behavior;
|
||||
import akka.actor.typed.javadsl.ActorContext;
|
||||
import akka.actor.typed.javadsl.Behaviors;
|
||||
// #fun-style
|
||||
import akka.actor.typed.javadsl.AbstractBehavior;
|
||||
import akka.actor.typed.javadsl.Receive;
|
||||
import akka.actor.typed.javadsl.TimerScheduler;
|
||||
|
||||
import java.time.Duration;
|
||||
// #oo-style
|
||||
|
||||
import akka.actor.typed.ActorRef;
|
||||
import akka.actor.typed.javadsl.TimerScheduler;
|
||||
import akka.Done;
|
||||
import java.time.Duration;
|
||||
|
||||
interface StyleGuideDocExamples {
|
||||
|
||||
interface FunctionalStyle {
|
||||
|
|
@ -76,13 +77,17 @@ interface StyleGuideDocExamples {
|
|||
interface OOStyle {
|
||||
|
||||
// #oo-style
|
||||
|
||||
// #messages
|
||||
public class Counter extends AbstractBehavior<Counter.Command> {
|
||||
|
||||
public interface Command {}
|
||||
|
||||
// #message-enum
|
||||
public enum Increment implements Command {
|
||||
INSTANCE
|
||||
}
|
||||
// #message-enum
|
||||
|
||||
public static class GetValue implements Command {
|
||||
public final ActorRef<Value> replyTo;
|
||||
|
|
@ -99,6 +104,7 @@ interface StyleGuideDocExamples {
|
|||
this.value = value;
|
||||
}
|
||||
}
|
||||
// #messages
|
||||
|
||||
public static Behavior<Command> create() {
|
||||
return Behaviors.setup(Counter::new);
|
||||
|
|
@ -115,7 +121,9 @@ interface StyleGuideDocExamples {
|
|||
@Override
|
||||
public Receive<Command> createReceive() {
|
||||
return newReceiveBuilder()
|
||||
// #message-enum-match
|
||||
.onMessage(Increment.class, notUsed -> onIncrement())
|
||||
// #message-enum-match
|
||||
.onMessage(GetValue.class, this::onGetValue)
|
||||
.build();
|
||||
}
|
||||
|
|
@ -130,7 +138,9 @@ interface StyleGuideDocExamples {
|
|||
command.replyTo.tell(new Value(n));
|
||||
return this;
|
||||
}
|
||||
// #messages
|
||||
}
|
||||
// #messages
|
||||
// #oo-style
|
||||
|
||||
}
|
||||
|
|
@ -403,4 +413,316 @@ interface StyleGuideDocExamples {
|
|||
}
|
||||
// #fun-style-setup-params3
|
||||
}
|
||||
|
||||
interface FactoryMethod {
|
||||
// #behavior-factory-method
|
||||
public class CountDown extends AbstractBehavior<CountDown.Command> {
|
||||
|
||||
public interface Command {}
|
||||
|
||||
public enum Down implements Command {
|
||||
INSTANCE
|
||||
}
|
||||
|
||||
// factory for the initial `Behavior`
|
||||
public static Behavior<Command> create(int countDownFrom, ActorRef<Done> notifyWhenZero) {
|
||||
return Behaviors.setup(context -> new CountDown(countDownFrom, notifyWhenZero));
|
||||
}
|
||||
|
||||
private final ActorRef<Done> notifyWhenZero;
|
||||
private int remaining;
|
||||
|
||||
private CountDown(int countDownFrom, ActorRef<Done> notifyWhenZero) {
|
||||
this.remaining = countDownFrom;
|
||||
this.notifyWhenZero = notifyWhenZero;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive<Command> createReceive() {
|
||||
return newReceiveBuilder().onMessage(Down.class, notUsed -> onDown()).build();
|
||||
}
|
||||
|
||||
private Behavior<Command> onDown() {
|
||||
remaining--;
|
||||
if (remaining == 0) {
|
||||
notifyWhenZero.tell(Done.getInstance());
|
||||
return Behaviors.stopped();
|
||||
} else {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
// #behavior-factory-method
|
||||
|
||||
public class Usage {
|
||||
private ActorContext<?> context = null;
|
||||
private ActorRef<Done> doneRef = null;
|
||||
|
||||
{
|
||||
// #behavior-factory-method-spawn
|
||||
ActorRef<CountDown.Command> countDown =
|
||||
context.spawn(CountDown.create(100, doneRef), "countDown");
|
||||
// #behavior-factory-method-spawn
|
||||
|
||||
// #message-prefix-in-tell
|
||||
countDown.tell(CountDown.Down.INSTANCE);
|
||||
// #message-prefix-in-tell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Messages {
|
||||
// #message-protocol
|
||||
interface CounterProtocol {
|
||||
interface Command {}
|
||||
|
||||
public static class Increment implements Command {
|
||||
public final int delta;
|
||||
private final ActorRef<OperationResult> replyTo;
|
||||
|
||||
public Increment(int delta, ActorRef<OperationResult> replyTo) {
|
||||
this.delta = delta;
|
||||
this.replyTo = replyTo;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Decrement implements Command {
|
||||
public final int delta;
|
||||
private final ActorRef<OperationResult> replyTo;
|
||||
|
||||
public Decrement(int delta, ActorRef<OperationResult> replyTo) {
|
||||
this.delta = delta;
|
||||
this.replyTo = replyTo;
|
||||
}
|
||||
}
|
||||
|
||||
interface OperationResult {}
|
||||
|
||||
enum Confirmed implements OperationResult {
|
||||
INSTANCE
|
||||
}
|
||||
|
||||
public static class Rejected implements OperationResult {
|
||||
public final String reason;
|
||||
|
||||
public Rejected(String reason) {
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
}
|
||||
// #message-protocol
|
||||
}
|
||||
|
||||
interface PublicVsPrivateMessages1 {
|
||||
// #on-message-lambda-anti
|
||||
// this is an anti-pattern, don't use lambdas with a large block of code
|
||||
// #on-message-lambda-anti
|
||||
// #public-private-messages-1
|
||||
public class Counter extends AbstractBehavior<Counter.Command> {
|
||||
|
||||
public interface Command {}
|
||||
|
||||
public enum Increment implements Command {
|
||||
INSTANCE
|
||||
}
|
||||
|
||||
public static class GetValue implements Command {
|
||||
public final ActorRef<Value> replyTo;
|
||||
|
||||
public GetValue(ActorRef<Value> replyTo) {
|
||||
this.replyTo = replyTo;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Value {
|
||||
public final int value;
|
||||
|
||||
public Value(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Tick is private so can't be sent from the outside
|
||||
private enum Tick implements Command {
|
||||
INSTANCE
|
||||
}
|
||||
|
||||
public static Behavior<Command> create(String name, Duration tickInterval) {
|
||||
return Behaviors.setup(
|
||||
context ->
|
||||
Behaviors.withTimers(
|
||||
timers -> {
|
||||
timers.startTimerWithFixedDelay("tick", Tick.INSTANCE, tickInterval);
|
||||
return new Counter(name, context);
|
||||
}));
|
||||
}
|
||||
|
||||
private final String name;
|
||||
private final ActorContext<Command> context;
|
||||
private int count;
|
||||
|
||||
private Counter(String name, ActorContext<Command> context) {
|
||||
this.name = name;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
// #on-message-lambda
|
||||
// #on-message-method-ref
|
||||
@Override
|
||||
// #on-message-lambda-anti
|
||||
public Receive<Command> createReceive() {
|
||||
// #on-message-lambda-anti
|
||||
return newReceiveBuilder()
|
||||
// #on-message-method-ref
|
||||
.onMessage(Increment.class, notUsed -> onIncrement())
|
||||
// #on-message-lambda
|
||||
.onMessage(Tick.class, notUsed -> onTick())
|
||||
// #on-message-method-ref
|
||||
.onMessage(GetValue.class, this::onGetValue)
|
||||
// #on-message-lambda
|
||||
.build();
|
||||
}
|
||||
|
||||
// #on-message-lambda
|
||||
// #on-message-method-ref
|
||||
|
||||
// #on-message-lambda
|
||||
private Behavior<Command> onIncrement() {
|
||||
count++;
|
||||
context.getLog().debug("[{}] Incremented counter to [{}]", name, count);
|
||||
return this;
|
||||
}
|
||||
// #on-message-lambda
|
||||
|
||||
private Behavior<Command> onTick() {
|
||||
count++;
|
||||
context.getLog().debug("[{}] Incremented counter by background tick to [{}]", name, count);
|
||||
return this;
|
||||
}
|
||||
|
||||
// #on-message-method-ref
|
||||
private Behavior<Command> onGetValue(GetValue command) {
|
||||
command.replyTo.tell(new Value(count));
|
||||
return this;
|
||||
}
|
||||
// #on-message-method-ref
|
||||
|
||||
// #public-private-messages-1
|
||||
// anti-pattern, don't do like this
|
||||
public Receive<Command> createReceiveAnti() {
|
||||
// #on-message-lambda-anti
|
||||
return newReceiveBuilder()
|
||||
.onMessage(
|
||||
Increment.class,
|
||||
notUsed -> {
|
||||
count++;
|
||||
context.getLog().debug("[{}] Incremented counter to [{}]", name, count);
|
||||
return this;
|
||||
})
|
||||
.onMessage(
|
||||
Tick.class,
|
||||
notUsed -> {
|
||||
count++;
|
||||
context
|
||||
.getLog()
|
||||
.debug("[{}] Incremented counter by background tick to [{}]", name, count);
|
||||
return this;
|
||||
})
|
||||
.onMessage(
|
||||
GetValue.class,
|
||||
command -> {
|
||||
command.replyTo.tell(new Value(count));
|
||||
return this;
|
||||
})
|
||||
.build();
|
||||
}
|
||||
// #on-message-lambda-anti
|
||||
// #public-private-messages-1
|
||||
}
|
||||
// #public-private-messages-1
|
||||
|
||||
}
|
||||
|
||||
interface PublicVsPrivateMessages2 {
|
||||
// #public-private-messages-2
|
||||
// above example is preferred, but this is possible and not wrong
|
||||
public class Counter extends AbstractBehavior<Counter.PrivateCommand> {
|
||||
|
||||
public interface PrivateCommand {}
|
||||
|
||||
public interface Command extends PrivateCommand {}
|
||||
|
||||
public enum Increment implements Command {
|
||||
INSTANCE
|
||||
}
|
||||
|
||||
public static class GetValue implements Command {
|
||||
public final ActorRef<Value> replyTo;
|
||||
|
||||
public GetValue(ActorRef<Value> replyTo) {
|
||||
this.replyTo = replyTo;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Value {
|
||||
public final int value;
|
||||
|
||||
public Value(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Tick is a PrivateCommand so can't be sent to an ActorRef<Command>
|
||||
enum Tick implements PrivateCommand {
|
||||
INSTANCE
|
||||
}
|
||||
|
||||
public static Behavior<Command> create(String name, Duration tickInterval) {
|
||||
return Behaviors.setup(
|
||||
(ActorContext<PrivateCommand> context) ->
|
||||
Behaviors.withTimers(
|
||||
timers -> {
|
||||
timers.startTimerWithFixedDelay("tick", Tick.INSTANCE, tickInterval);
|
||||
return new Counter(name, context);
|
||||
}))
|
||||
.narrow(); // note narrow here
|
||||
}
|
||||
|
||||
private final String name;
|
||||
private final ActorContext<PrivateCommand> context;
|
||||
private int count;
|
||||
|
||||
private Counter(String name, ActorContext<PrivateCommand> context) {
|
||||
this.name = name;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Receive<PrivateCommand> createReceive() {
|
||||
return newReceiveBuilder()
|
||||
.onMessage(Increment.class, notUsed -> onIncrement())
|
||||
.onMessage(Tick.class, notUsed -> onTick())
|
||||
.onMessage(GetValue.class, this::onGetValue)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Behavior<PrivateCommand> onIncrement() {
|
||||
count++;
|
||||
context.getLog().debug("[{}] Incremented counter to [{}]", name, count);
|
||||
return this;
|
||||
}
|
||||
|
||||
private Behavior<PrivateCommand> onTick() {
|
||||
count++;
|
||||
context.getLog().debug("[{}] Incremented counter by background tick to [{}]", name, count);
|
||||
return this;
|
||||
}
|
||||
|
||||
private Behavior<PrivateCommand> onGetValue(GetValue command) {
|
||||
command.replyTo.tell(new Value(count));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
// #public-private-messages-2
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,15 +4,23 @@
|
|||
|
||||
package docs.akka.typed
|
||||
|
||||
//#oo-style
|
||||
//#fun-style
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.Future
|
||||
|
||||
import akka.actor.typed.ActorSystem
|
||||
import akka.actor.typed.ActorRef
|
||||
import akka.actor.typed.scaladsl.TimerScheduler
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
import akka.actor.typed.ActorRef
|
||||
import akka.Done
|
||||
import com.github.ghik.silencer.silent
|
||||
|
||||
//#oo-style
|
||||
//#fun-style
|
||||
|
||||
import akka.actor.typed.Behavior
|
||||
import akka.actor.typed.scaladsl.ActorContext
|
||||
import akka.actor.typed.scaladsl.Behaviors
|
||||
import akka.actor.typed.scaladsl.TimerScheduler
|
||||
//#fun-style
|
||||
import akka.actor.typed.scaladsl.AbstractBehavior
|
||||
//#oo-style
|
||||
|
|
@ -23,11 +31,13 @@ object StyleGuideDocExamples {
|
|||
|
||||
//#fun-style
|
||||
|
||||
//#messages
|
||||
object Counter {
|
||||
sealed trait Command
|
||||
case object Increment extends Command
|
||||
final case class GetValue(replyTo: ActorRef[Value]) extends Command
|
||||
final case class Value(n: Int)
|
||||
//#messages
|
||||
|
||||
def apply(): Behavior[Command] =
|
||||
counter(0)
|
||||
|
|
@ -44,7 +54,9 @@ object StyleGuideDocExamples {
|
|||
Behaviors.same
|
||||
}
|
||||
}
|
||||
//#messages
|
||||
}
|
||||
//#messages
|
||||
//#fun-style
|
||||
|
||||
}
|
||||
|
|
@ -52,6 +64,7 @@ object StyleGuideDocExamples {
|
|||
object OOStyle {
|
||||
|
||||
//#oo-style
|
||||
|
||||
object Counter {
|
||||
sealed trait Command
|
||||
case object Increment extends Command
|
||||
|
|
@ -254,4 +267,253 @@ object StyleGuideDocExamples {
|
|||
// #fun-style-setup-params4
|
||||
}
|
||||
|
||||
object FactoryMethod {
|
||||
//#behavior-factory-method
|
||||
object CountDown {
|
||||
sealed trait Command
|
||||
case object Down extends Command
|
||||
|
||||
// factory for the initial `Behavior`
|
||||
def apply(countDownFrom: Int, notifyWhenZero: ActorRef[Done]): Behavior[Command] =
|
||||
new CountDown(notifyWhenZero).counter(countDownFrom)
|
||||
}
|
||||
|
||||
private class CountDown(notifyWhenZero: ActorRef[Done]) {
|
||||
import CountDown._
|
||||
|
||||
private def counter(remaining: Int): Behavior[Command] = {
|
||||
//#exhastivness-check
|
||||
Behaviors.receiveMessage {
|
||||
case Down =>
|
||||
if (remaining == 1) {
|
||||
notifyWhenZero.tell(Done)
|
||||
Behaviors.stopped
|
||||
} else
|
||||
counter(remaining - 1)
|
||||
}
|
||||
//#exhastivness-check
|
||||
}
|
||||
|
||||
}
|
||||
//#behavior-factory-method
|
||||
|
||||
object Usage {
|
||||
val context: ActorContext[_] = ???
|
||||
val doneRef: ActorRef[Done] = ???
|
||||
|
||||
//#behavior-factory-method-spawn
|
||||
val countDown = context.spawn(CountDown(100, doneRef), "countDown")
|
||||
//#behavior-factory-method-spawn
|
||||
|
||||
//#message-prefix-in-tell
|
||||
countDown ! CountDown.Down
|
||||
//#message-prefix-in-tell
|
||||
}
|
||||
}
|
||||
|
||||
object Messages {
|
||||
//#message-protocol
|
||||
object CounterProtocol {
|
||||
sealed trait Command
|
||||
|
||||
final case class Increment(delta: Int, replyTo: ActorRef[OperationResult]) extends Command
|
||||
final case class Decrement(delta: Int, replyTo: ActorRef[OperationResult]) extends Command
|
||||
|
||||
sealed trait OperationResult
|
||||
case object Confirmed extends OperationResult
|
||||
final case class Rejected(reason: String)
|
||||
}
|
||||
//#message-protocol
|
||||
}
|
||||
|
||||
object PublicVsPrivateMessages1 {
|
||||
//#public-private-messages-1
|
||||
object Counter {
|
||||
sealed trait Command
|
||||
case object Increment extends Command
|
||||
final case class GetValue(replyTo: ActorRef[Value]) extends Command
|
||||
final case class Value(n: Int)
|
||||
|
||||
// Tick is private so can't be sent from the outside
|
||||
private case object Tick extends Command
|
||||
|
||||
def apply(name: String, tickInterval: FiniteDuration): Behavior[Command] =
|
||||
Behaviors.setup { context =>
|
||||
Behaviors.withTimers { timers =>
|
||||
timers.startTimerWithFixedDelay("tick", Tick, tickInterval)
|
||||
new Counter(name, context).counter(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Counter private (name: String, context: ActorContext[Counter.Command]) {
|
||||
import Counter._
|
||||
|
||||
private def counter(n: Int): Behavior[Command] =
|
||||
Behaviors.receiveMessage {
|
||||
case Increment =>
|
||||
val newValue = n + 1
|
||||
context.log.debug("[{}] Incremented counter to [{}]", name, newValue)
|
||||
counter(newValue)
|
||||
case Tick =>
|
||||
val newValue = n + 1
|
||||
context.log.debug("[{}] Incremented counter by background tick to [{}]", name, newValue)
|
||||
counter(newValue)
|
||||
case GetValue(replyTo) =>
|
||||
replyTo ! Value(n)
|
||||
Behaviors.same
|
||||
}
|
||||
}
|
||||
//#public-private-messages-1
|
||||
}
|
||||
|
||||
object PublicVsPrivateMessages2 {
|
||||
//#public-private-messages-2
|
||||
// above example is preferred, but this is possible and not wrong
|
||||
object Counter {
|
||||
sealed trait PrivateCommand
|
||||
sealed trait Command extends PrivateCommand
|
||||
case object Increment extends Command
|
||||
final case class GetValue(replyTo: ActorRef[Value]) extends Command
|
||||
final case class Value(n: Int)
|
||||
|
||||
// Tick is a PrivateCommand so can't be sent to an ActorRef[Command]
|
||||
case object Tick extends PrivateCommand
|
||||
|
||||
def apply(name: String, tickInterval: FiniteDuration): Behavior[Command] = {
|
||||
Behaviors
|
||||
.setup[Counter.PrivateCommand] { context =>
|
||||
Behaviors.withTimers { timers =>
|
||||
timers.startTimerWithFixedDelay("tick", Tick, tickInterval)
|
||||
new Counter(name, context).counter(0)
|
||||
}
|
||||
}
|
||||
.narrow // note narrow here
|
||||
}
|
||||
}
|
||||
|
||||
class Counter private (name: String, context: ActorContext[Counter.PrivateCommand]) {
|
||||
import Counter._
|
||||
|
||||
private def counter(n: Int): Behavior[PrivateCommand] =
|
||||
Behaviors.receiveMessage {
|
||||
case Increment =>
|
||||
val newValue = n + 1
|
||||
context.log.debug("[{}] Incremented counter to [{}]", name, newValue)
|
||||
counter(newValue)
|
||||
case Tick =>
|
||||
val newValue = n + 1
|
||||
context.log.debug("[{}] Incremented counter by background tick to [{}]", name, newValue)
|
||||
counter(newValue)
|
||||
case GetValue(replyTo) =>
|
||||
replyTo ! Value(n)
|
||||
Behaviors.same
|
||||
}
|
||||
}
|
||||
//#public-private-messages-2
|
||||
}
|
||||
|
||||
object Ask {
|
||||
import Messages.CounterProtocol._
|
||||
|
||||
val system: ActorSystem[Nothing] = ???
|
||||
|
||||
//#ask-1
|
||||
import akka.actor.typed.scaladsl.AskPattern._
|
||||
import akka.util.Timeout
|
||||
|
||||
implicit val timeout = Timeout(3.seconds)
|
||||
implicit val scheduler = system.scheduler
|
||||
val counter: ActorRef[Command] = ???
|
||||
|
||||
val result: Future[OperationResult] = counter.ask(replyTo => Increment(delta = 2, replyTo))
|
||||
//#ask-1
|
||||
|
||||
//#ask-2
|
||||
val result2: Future[OperationResult] = counter.ask(Increment(delta = 2, _))
|
||||
//#ask-2
|
||||
|
||||
/*
|
||||
//#ask-3
|
||||
// doesn't compile
|
||||
val result3: Future[OperationResult] = counter ? Increment(delta = 2, _)
|
||||
//#ask-3
|
||||
*/
|
||||
|
||||
//#ask-4
|
||||
val result3: Future[OperationResult] = counter ? (Increment(delta = 2, _))
|
||||
//#ask-4
|
||||
}
|
||||
|
||||
object ExhaustivenessCheck {
|
||||
|
||||
object CountDown {
|
||||
//#messages-sealed
|
||||
sealed trait Command
|
||||
case object Down extends Command
|
||||
final case class GetValue(replyTo: ActorRef[Value]) extends Command
|
||||
final case class Value(n: Int)
|
||||
//#messages-sealed
|
||||
|
||||
def apply(countDownFrom: Int, notifyWhenZero: ActorRef[Done]): Behavior[Command] =
|
||||
new CountDown(notifyWhenZero).counterWithGuard(countDownFrom)
|
||||
}
|
||||
|
||||
private class CountDown(notifyWhenZero: ActorRef[Done]) {
|
||||
import CountDown._
|
||||
|
||||
private def counterWithGuard(remaining: Int): Behavior[Command] = {
|
||||
//#pattern-match-guard
|
||||
// no exhaustiveness check because of guard condition
|
||||
Behaviors.receiveMessage {
|
||||
case Down if remaining == 1 =>
|
||||
notifyWhenZero.tell(Done)
|
||||
zero
|
||||
case Down =>
|
||||
counter(remaining - 1)
|
||||
}
|
||||
//#pattern-match-guard
|
||||
}
|
||||
|
||||
@silent
|
||||
private def counter(remaining: Int): Behavior[Command] = {
|
||||
//#pattern-match-without-guard
|
||||
Behaviors.receiveMessage {
|
||||
case Down =>
|
||||
if (remaining == 1) {
|
||||
notifyWhenZero.tell(Done)
|
||||
zero
|
||||
} else
|
||||
counter(remaining - 1)
|
||||
}
|
||||
//#pattern-match-without-guard
|
||||
}
|
||||
|
||||
//#pattern-match-unhandled
|
||||
private val zero: Behavior[Command] = {
|
||||
Behaviors.receiveMessage {
|
||||
case GetValue(replyTo) =>
|
||||
replyTo ! Value(0)
|
||||
Behaviors.same
|
||||
case Down =>
|
||||
Behaviors.unhandled
|
||||
}
|
||||
}
|
||||
//#pattern-match-unhandled
|
||||
|
||||
@silent
|
||||
object partial {
|
||||
//#pattern-match-partial
|
||||
private val zero: Behavior[Command] = {
|
||||
Behaviors.receiveMessagePartial {
|
||||
case GetValue(replyTo) =>
|
||||
replyTo ! Value(0)
|
||||
Behaviors.same
|
||||
}
|
||||
}
|
||||
//#pattern-match-partial
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ import akka.util.OptionVal
|
|||
*
|
||||
* Instances of this behavior should be created via [[Behaviors.setup]] and if
|
||||
* the [[ActorContext]] is needed it can be passed as a constructor parameter
|
||||
* from the factory function.
|
||||
* from the factory function. This is important because a new instance
|
||||
* should be created when restart supervision is used.
|
||||
*
|
||||
* @see [[Behaviors.setup]]
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ import akka.actor.typed.{ Behavior, ExtensibleBehavior, Signal, TypedActorContex
|
|||
*
|
||||
* Instances of this behavior should be created via [[Behaviors.setup]] and if
|
||||
* the [[ActorContext]] is needed it can be passed as a constructor parameter
|
||||
* from the factory function.
|
||||
* from the factory function. This is important because a new instance
|
||||
* should be created when restart supervision is used.
|
||||
*
|
||||
* @see [[Behaviors.setup]]
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
# Style guide
|
||||
|
||||
## Functional vs object-oriented style
|
||||
This is a style guide with recommendations of idioms and pattern for writing Akka Typed actors.
|
||||
|
||||
As with all style guides, treat this as a list of rules to be broken. There are certainly times
|
||||
when alternative styles should be preferred over the ones given here.
|
||||
|
||||
## Functional versus object-oriented style
|
||||
|
||||
There are two flavors of the Actor APIs.
|
||||
|
||||
|
|
@ -51,7 +56,7 @@ A few differences to note:
|
|||
the message. That said, `Behaviors.setup` is often used in the functional style as well, and then
|
||||
often together with `Behaviors.receiveMessage` that doesn't pass in the context with the message.]
|
||||
@java[The `ActorContext` is accessed with `Behaviors.setup` but then kept in different ways.
|
||||
As an instance field vs. a method parameter.]
|
||||
As an instance field versus a method parameter.]
|
||||
|
||||
Which style you choose to use is a matter of taste and both styles can be mixed depending on which is best
|
||||
for a specific actor. An actor can switch between behaviors implemented in different styles.
|
||||
|
|
@ -169,7 +174,7 @@ That's nice. One thing to be cautious with here is that it's important that you
|
|||
each spawned actor, since those parameters must not be shared between different actor instances. That comes natural
|
||||
when creating the instance from `Behaviors.setup` as in the above example. Having a
|
||||
@scala[`apply` factory method in the companion object and making the constructor private is recommended.]
|
||||
@java[static `create` factory method and making the constructor private is highly recommended.]
|
||||
@java[static `create` factory method and making the constructor private is recommended.]
|
||||
|
||||
This can also be useful when testing the behavior by creating a test subclass that overrides certain methods in the
|
||||
class. The test would create the instance without the @scala[`apply` factory method]@java[static `create` factory method].
|
||||
|
|
@ -190,3 +195,256 @@ Scala
|
|||
: @@snip [StyleGuideDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala) { #fun-style-setup-params4 }
|
||||
|
||||
@@@
|
||||
|
||||
## Behavior factory method
|
||||
|
||||
The initial behavior should be created via @scala[a factory method in the companion object]@java[a static factory method].
|
||||
Thereby the usage of the behavior doesn't change when the implementation is changed, for example if
|
||||
changing between object-oriented and function style.
|
||||
|
||||
The factory method is a good place for retrieving resources like `Behaviors.withTimers`, `Behaviors.withStash`
|
||||
and `ActorContext` with `Behaviors.setup`.
|
||||
|
||||
When using the object-oriented style, `AbstractBehavior`, a new instance should be created from a `Behaviors.setup`
|
||||
block in this factory method even though the `ActorContext` is not needed. This is important because a new
|
||||
instance should be created when restart supervision is used. Typically, the `ActorContext` is needed anyway.
|
||||
|
||||
The naming convention for the factory method is @scala[`apply` (when using Scala)]@java[`create` (when using Java)].
|
||||
Consistent naming makes it easier for readers of the code to find the "starting point" of the behavior.
|
||||
|
||||
In the functional style the factory could even have been defined as a @scala[`val`]@java[`static field`]
|
||||
if all state is immutable and captured by the function, but since most behaviors need some initialization
|
||||
parameters it is preferred to consistently use a method @scala[(`def`)] for the factory.
|
||||
|
||||
Example:
|
||||
|
||||
Scala
|
||||
: @@snip [StyleGuideDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala) { #behavior-factory-method }
|
||||
|
||||
Java
|
||||
: @@snip [StyleGuideDocExamples.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/StyleGuideDocExamples.java) { #behavior-factory-method }
|
||||
|
||||
When spawning an actor from this initial behavior it looks like:
|
||||
|
||||
Scala
|
||||
: @@snip [StyleGuideDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala) { #behavior-factory-method-spawn }
|
||||
|
||||
Java
|
||||
: @@snip [StyleGuideDocExamples.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/StyleGuideDocExamples.java) { #behavior-factory-method-spawn }
|
||||
|
||||
|
||||
## Where to define messages
|
||||
|
||||
When sending messages to another actor or receiving responses the messages should be prefixed with the name
|
||||
of the actor/behavior that defines the message to make it clear and avoid ambiguity.
|
||||
|
||||
Scala
|
||||
: @@snip [StyleGuideDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala) { #message-prefix-in-tell }
|
||||
|
||||
Java
|
||||
: @@snip [StyleGuideDocExamples.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/StyleGuideDocExamples.java) { #message-prefix-in-tell }
|
||||
|
||||
That is preferred over using @scala[importing `Down` and using `countDown ! Down`]
|
||||
@java[importing `Down` and using `countDown.tell(Down.INSTANCE);`].
|
||||
In the implementation of the `Behavior` that handle these messages the short names can be used.
|
||||
|
||||
That is a reason for not defining the messages as top level classes in a package.
|
||||
|
||||
An actor typically has a primary `Behavior` or it's only using one `Behavior` and then it's good to define
|
||||
the messages @scala[in the companion object]@java[as static inner classes] together with that `Behavior`.
|
||||
|
||||
Scala
|
||||
: @@snip [StyleGuideDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala) { #messages }
|
||||
|
||||
Java
|
||||
: @@snip [StyleGuideDocExamples.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/StyleGuideDocExamples.java) { #messages }
|
||||
|
||||
Sometimes several actors share the same messages, because they have a tight coupling and using message adapters
|
||||
would introduce to much boilerplate and duplication. If there is no "natural home" for such messages they can be
|
||||
be defined in a separate @scala[`object`]@java[`interface`] to give them a naming scope.
|
||||
|
||||
Example of shared message protocol:
|
||||
|
||||
Scala
|
||||
: @@snip [StyleGuideDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala) { #message-protocol }
|
||||
|
||||
Java
|
||||
: @@snip [StyleGuideDocExamples.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/StyleGuideDocExamples.java) { #message-protocol }
|
||||
|
||||
## Public versus private messages
|
||||
|
||||
Often an actor has some messages that are only for it's internal implementation and not part of the public
|
||||
message protocol. For example, it can be timer messages or wrapper messages for `ask` or `messageAdapter`.
|
||||
|
||||
That can be be achieved by defining those messages with `private` visibility. Then they can't be accessed
|
||||
and sent from the outside of the actor. The private messages must still @scala[extend]@java[implement] the
|
||||
public `Command` @scala[trait]@java[interface].
|
||||
|
||||
Example of a private visibility for internal message:
|
||||
|
||||
Scala
|
||||
: @@snip [StyleGuideDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala) { #public-private-messages-1 }
|
||||
|
||||
Java
|
||||
: @@snip [StyleGuideDocExamples.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/StyleGuideDocExamples.java) { #public-private-messages-1 }
|
||||
|
||||
There is another approach, which is valid but more complicated. It's not relying on visibility from the programming
|
||||
language but instead only exposing part of the message class hierarchy to the outside, by using `narrow`. The
|
||||
former approach is recommended but it can be good to know this "trick", for example it can be useful when
|
||||
using shared message protocol classes as described in @ref:[Where to define messages](#where-to-define-messages).
|
||||
|
||||
Example of not exposing internal message in public `Behavior` type:
|
||||
|
||||
Scala
|
||||
: @@snip [StyleGuideDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala) { #public-private-messages-2 }
|
||||
|
||||
Java
|
||||
: @@snip [StyleGuideDocExamples.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/StyleGuideDocExamples.java) { #public-private-messages-2 }
|
||||
|
||||
@@@ div {.group-java}
|
||||
|
||||
### Singleton messages
|
||||
|
||||
For messages without parameters the `enum` singleton pattern is recommended:
|
||||
|
||||
Java
|
||||
: @@snip [StyleGuideDocExamples.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/StyleGuideDocExamples.java) { #message-enum }
|
||||
|
||||
In the `ReceiveBuilder` it can be matched in same way as other messages:
|
||||
|
||||
Java
|
||||
: @@snip [StyleGuideDocExamples.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/StyleGuideDocExamples.java) { #message-enum-match }
|
||||
|
||||
@@@
|
||||
|
||||
@@@ div {.group-java}
|
||||
|
||||
## Lamdas versus method references
|
||||
|
||||
It's recommended to keep the message matching with the `ReceiveBuilder` as short and clean as possible
|
||||
and delegate to methods. This improves readability and ease of method navigation with an IDE.
|
||||
|
||||
The delegation can be with lambdas or [method references](https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html).
|
||||
|
||||
Example of delegation using a lambda:
|
||||
|
||||
Java
|
||||
: @@snip [StyleGuideDocExamples.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/StyleGuideDocExamples.java) { #on-message-lambda }
|
||||
|
||||
When possible it's preferred to use method references instead of lambdas. The benefit is less verbosity and
|
||||
in some cases it can actually give better type inference.
|
||||
|
||||
Java
|
||||
: @@snip [StyleGuideDocExamples.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/StyleGuideDocExamples.java) { #on-message-method-ref }
|
||||
|
||||
`this::onGetValue` is a method reference in above example. It corresponds to `command -> onGetValue(command)`.
|
||||
|
||||
If you are using IntelliJ IDEA it has support for converting lambdas to method references.
|
||||
|
||||
More important than the choice between lambdas or method references is to avoid lambdas with a large block of code.
|
||||
An anti-pattern would be to inline all message handling inside the lambdas like this:
|
||||
|
||||
Java
|
||||
: @@snip [StyleGuideDocExamples.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/StyleGuideDocExamples.java) { #on-message-lambda-anti }
|
||||
|
||||
In a real application it would often be more than 3 lines for each message.
|
||||
It's not only making it more difficult to get an overview of the message matching, but compiler errors related
|
||||
to lambdas can sometimes be difficult to understand.
|
||||
|
||||
Ideally, lambdas should be written in one line of code. Two lines can be ok, but three is probably too much.
|
||||
Also, don't use braces and return statements in one-line lambda bodies.
|
||||
|
||||
@@@
|
||||
|
||||
@@@ div {.group-scala}
|
||||
|
||||
## Partial versus total Function
|
||||
|
||||
It's recommended to use a `sealed` trait as the super type of the commands (incoming messages) of a an actor
|
||||
because then the Scala compiler will emit a warning if a message type is forgotten in the pattern match.
|
||||
|
||||
Scala
|
||||
: @@snip [StyleGuideDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala) { #messages-sealed }
|
||||
|
||||
That is the main reason for why `Behaviors.receive`, `Behaviors.receiveMessage` takes a total `Function` and
|
||||
not a `PartialFunction`.
|
||||
|
||||
The compiler warning if `GetValue` is not handled:
|
||||
|
||||
```
|
||||
[warn] ... Counter.scala:45:34: match may not be exhaustive.
|
||||
[warn] It would fail on the following input: GetValue(_)
|
||||
[warn] Behaviors.receiveMessage {
|
||||
[warn] ^
|
||||
```
|
||||
|
||||
Note that a `MatchError` will be thrown at runtime if a message is not handled, so it's important to pay
|
||||
attention to those. If a `Behavior` should not handle certain messages you can still include them
|
||||
in the pattern match and return `Behaviors.unhandled`.
|
||||
|
||||
Scala
|
||||
: @@snip [StyleGuideDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala) { #pattern-match-unhandled }
|
||||
|
||||
One thing to be aware of is the exhaustiveness check is not enabled when there is a guard condition in the
|
||||
pattern match cases.
|
||||
|
||||
Scala
|
||||
: @@snip [StyleGuideDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala) { #pattern-match-guard }
|
||||
|
||||
Therefore it can be better to not use the guard and instead move the `if` after the `=>`.
|
||||
|
||||
Scala
|
||||
: @@snip [StyleGuideDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala) { #pattern-match-without-guard }
|
||||
|
||||
It's recommended to use the `sealed` trait and total functions with exhaustiveness check to detect mistakes
|
||||
of forgetting to handle some messages. Sometimes that can be inconvenient and then you can use a `PartialFunction`
|
||||
with `Behaviors.receivePartial` or `Behaviors.receiveMessagePartial`
|
||||
|
||||
Scala
|
||||
: @@snip [StyleGuideDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala) { #pattern-match-partial }
|
||||
|
||||
@@@
|
||||
|
||||
@@@ div {.group-scala}
|
||||
|
||||
## ask versus ?
|
||||
|
||||
When using the `AskPattern` it's recommended to use the `ask` method rather than the `?` operator.
|
||||
|
||||
Scala
|
||||
: @@snip [StyleGuideDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala) { #ask-1 }
|
||||
|
||||
Instead of the `replyTo` you can use `_` for less verbosity.
|
||||
|
||||
Scala
|
||||
: @@snip [StyleGuideDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala) { #ask-2 }
|
||||
|
||||
When using `?` the following doesn't compile because of type inference problem:
|
||||
|
||||
Scala
|
||||
: @@snip [StyleGuideDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala) { #ask-3 }
|
||||
|
||||
By adding parentheses it works but is rather ugly, and therefore better to stick with `ask`.
|
||||
|
||||
Scala
|
||||
: @@snip [StyleGuideDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala) { #ask-4 }
|
||||
|
||||
Note that `AskPattern` is only intended for request-response interaction from outside an actor. If the requester is
|
||||
inside an actor, prefer `ActorContext.ask` as it provides better thread-safety by not involving
|
||||
@scala[`Future`]@java[`CompletionStage`] inside the actor.
|
||||
|
||||
@@@
|
||||
|
||||
## Additional naming conventions
|
||||
|
||||
Some naming conventions have already been mentioned in the context of other recommendations, but here
|
||||
is a list of additional conventions:
|
||||
|
||||
* `replyTo` is the typical name for the @scala[`ActorRef[Reply]`]@java[`ActorRef<Reply>`] parameter in
|
||||
messages to which a reply or acknowledgement should be sent.
|
||||
|
||||
* Incoming messages to an actor are typically called commands, and therefore the super type of all
|
||||
messages that an actor can handle is typically @scala[`sealed trait Command`]@java[`interface Command {}`].
|
||||
|
||||
* Use past tense for the events persisted by an `EventSourcedBehavior` since those represent facts that has happened,
|
||||
e.g. `Incremented`.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue