Merge pull request #27326 from akka/wip-22805-style2-patriknw

Style Guide: Passing around too many parameters, #22805
This commit is contained in:
Patrik Nordwall 2019-07-12 11:59:09 +02:00 committed by GitHub
commit 2f7f512625
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 511 additions and 0 deletions

View file

@ -13,6 +13,9 @@ 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
interface StyleGuideDocExamples {
@ -131,4 +134,273 @@ interface StyleGuideDocExamples {
// #oo-style
}
interface FunctionalStyleSetupParams1 {
// #fun-style-setup-params1
// this is an anti-example, better solutions exists
public class Counter {
public interface Command {}
public static class IncrementRepeatedly implements Command {
public final Duration interval;
public IncrementRepeatedly(Duration interval) {
this.interval = interval;
}
}
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;
}
}
public static Behavior<Command> create(String name) {
return Behaviors.setup(
context -> Behaviors.withTimers(timers -> counter(name, context, timers, 0)));
}
private static Behavior<Command> counter(
final String name,
final ActorContext<Command> context,
final TimerScheduler<Command> timers,
final int n) {
return Behaviors.receive(Command.class)
.onMessage(
IncrementRepeatedly.class,
command -> onIncrementRepeatedly(name, context, timers, n, command))
.onMessage(Increment.class, notUsed -> onIncrement(name, context, timers, n))
.onMessage(GetValue.class, command -> onGetValue(n, command))
.build();
}
private static Behavior<Command> onIncrementRepeatedly(
String name,
ActorContext<Command> context,
TimerScheduler<Command> timers,
int n,
IncrementRepeatedly command) {
context
.getLog()
.debug(
"[{}] Starting repeated increments with interval [{}], current count is [{}]",
name,
command.interval,
n);
timers.startTimerWithFixedDelay("repeat", Increment.INSTANCE, command.interval);
return Behaviors.same();
}
private static Behavior<Command> onIncrement(
String name, ActorContext<Command> context, TimerScheduler<Command> timers, int n) {
int newValue = n + 1;
context.getLog().debug("[{}] Incremented counter to [{}]", name, newValue);
return counter(name, context, timers, newValue);
}
private static Behavior<Command> onGetValue(int n, GetValue command) {
command.replyTo.tell(new Value(n));
return Behaviors.same();
}
}
// #fun-style-setup-params1
}
interface FunctionalStyleSetupParams2 {
// #fun-style-setup-params2
// this is better than previous example, but even better solution exists
public class Counter {
// messages omitted for brevity, same messages as above example
// #fun-style-setup-params2
public interface Command {}
public static class IncrementRepeatedly implements Command {
public final Duration interval;
public IncrementRepeatedly(Duration interval) {
this.interval = interval;
}
}
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;
}
}
// #fun-style-setup-params2
private static class Setup {
final String name;
final ActorContext<Command> context;
final TimerScheduler<Command> timers;
private Setup(String name, ActorContext<Command> context, TimerScheduler<Command> timers) {
this.name = name;
this.context = context;
this.timers = timers;
}
}
public static Behavior<Command> create(String name) {
return Behaviors.setup(
context ->
Behaviors.withTimers(timers -> counter(new Setup(name, context, timers), 0)));
}
private static Behavior<Command> counter(final Setup setup, final int n) {
return Behaviors.receive(Command.class)
.onMessage(
IncrementRepeatedly.class, command -> onIncrementRepeatedly(setup, n, command))
.onMessage(Increment.class, notUsed -> onIncrement(setup, n))
.onMessage(GetValue.class, command -> onGetValue(n, command))
.build();
}
private static Behavior<Command> onIncrementRepeatedly(
Setup setup, int n, IncrementRepeatedly command) {
setup
.context
.getLog()
.debug(
"[{}] Starting repeated increments with interval [{}], current count is [{}]",
setup.name,
command.interval,
n);
setup.timers.startTimerWithFixedDelay("repeat", Increment.INSTANCE, command.interval);
return Behaviors.same();
}
private static Behavior<Command> onIncrement(Setup setup, int n) {
int newValue = n + 1;
setup.context.getLog().debug("[{}] Incremented counter to [{}]", setup.name, newValue);
return counter(setup, newValue);
}
private static Behavior<Command> onGetValue(int n, GetValue command) {
command.replyTo.tell(new Value(n));
return Behaviors.same();
}
}
// #fun-style-setup-params2
}
interface FunctionalStyleSetupParams3 {
// #fun-style-setup-params3
// this is better than previous examples
public class Counter {
// messages omitted for brevity, same messages as above example
// #fun-style-setup-params3
public interface Command {}
public static class IncrementRepeatedly implements Command {
public final Duration interval;
public IncrementRepeatedly(Duration interval) {
this.interval = interval;
}
}
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;
}
}
// #fun-style-setup-params3
public static Behavior<Command> create(String name) {
return Behaviors.setup(
context ->
Behaviors.withTimers(timers -> new Counter(name, context, timers).counter(0)));
}
private final String name;
private final ActorContext<Command> context;
private final TimerScheduler<Command> timers;
private Counter(String name, ActorContext<Command> context, TimerScheduler<Command> timers) {
this.name = name;
this.context = context;
this.timers = timers;
}
private Behavior<Command> counter(final int n) {
return Behaviors.receive(Command.class)
.onMessage(IncrementRepeatedly.class, command -> onIncrementRepeatedly(n, command))
.onMessage(Increment.class, notUsed -> onIncrement(n))
.onMessage(GetValue.class, command -> onGetValue(n, command))
.build();
}
private Behavior<Command> onIncrementRepeatedly(int n, IncrementRepeatedly command) {
context
.getLog()
.debug(
"[{}] Starting repeated increments with interval [{}], current count is [{}]",
name,
command.interval,
n);
timers.startTimerWithFixedDelay("repeat", Increment.INSTANCE, command.interval);
return Behaviors.same();
}
private Behavior<Command> onIncrement(int n) {
int newValue = n + 1;
context.getLog().debug("[{}] Incremented counter to [{}]", name, newValue);
return counter(newValue);
}
private Behavior<Command> onGetValue(int n, GetValue command) {
command.replyTo.tell(new Value(n));
return Behaviors.same();
}
}
// #fun-style-setup-params3
}
}

View file

@ -6,10 +6,13 @@ package docs.akka.typed
//#oo-style
//#fun-style
import scala.concurrent.duration.FiniteDuration
import akka.actor.typed.ActorRef
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
@ -81,4 +84,174 @@ object StyleGuideDocExamples {
}
object FunctionalStyleSetupParams1 {
// #fun-style-setup-params1
// this is an anti-example, better solutions exists
object Counter {
sealed trait Command
case object Increment extends Command
final case class IncrementRepeatedly(interval: FiniteDuration) extends Command
final case class GetValue(replyTo: ActorRef[Value]) extends Command
final case class Value(n: Int)
def apply(name: String): Behavior[Command] =
Behaviors.withTimers { timers =>
counter(name, timers, 0)
}
private def counter(name: String, timers: TimerScheduler[Command], n: Int): Behavior[Command] =
Behaviors.receive { (context, message) =>
message match {
case IncrementRepeatedly(interval) =>
context.log.debug(
"[{}] Starting repeated increments with interval [{}], current count is [{}]",
name,
interval,
n)
timers.startTimerWithFixedDelay("repeat", Increment, interval)
Behaviors.same
case Increment =>
val newValue = n + 1
context.log.debug("[{}] Incremented counter to [{}]", name, newValue)
counter(name, timers, newValue)
case GetValue(replyTo) =>
replyTo ! Value(n)
Behaviors.same
}
}
}
// #fun-style-setup-params1
}
object FunctionalStyleSetupParams2 {
// #fun-style-setup-params2
// this is better than previous example, but even better solution exists
object Counter {
sealed trait Command
case object Increment extends Command
final case class IncrementRepeatedly(interval: FiniteDuration) extends Command
final case class GetValue(replyTo: ActorRef[Value]) extends Command
final case class Value(n: Int)
private case class Setup(name: String, context: ActorContext[Command], timers: TimerScheduler[Command])
def apply(name: String): Behavior[Command] =
Behaviors.setup { context =>
Behaviors.withTimers { timers =>
counter(Setup(name, context, timers), 0)
}
}
private def counter(setup: Setup, n: Int): Behavior[Command] =
Behaviors.receiveMessage {
case IncrementRepeatedly(interval) =>
setup.context.log.debug(
"[{}] Starting repeated increments with interval [{}], current count is [{}]",
setup.name,
interval,
n)
setup.timers.startTimerWithFixedDelay("repeat", Increment, interval)
Behaviors.same
case Increment =>
val newValue = n + 1
setup.context.log.debug("[{}] Incremented counter to [{}]", setup.name, newValue)
counter(setup, newValue)
case GetValue(replyTo) =>
replyTo ! Value(n)
Behaviors.same
}
}
// #fun-style-setup-params2
}
object FunctionalStyleSetupParams3 {
// #fun-style-setup-params3
// this is better than previous examples
object Counter {
sealed trait Command
case object Increment extends Command
final case class IncrementRepeatedly(interval: FiniteDuration) extends Command
final case class GetValue(replyTo: ActorRef[Value]) extends Command
final case class Value(n: Int)
def apply(name: String): Behavior[Command] =
Behaviors.setup { context =>
Behaviors.withTimers { timers =>
new Counter(name, context, timers).counter(0)
}
}
}
class Counter private (
name: String,
context: ActorContext[Counter.Command],
timers: TimerScheduler[Counter.Command]) {
import Counter._
private def counter(n: Int): Behavior[Command] =
Behaviors.receiveMessage {
case IncrementRepeatedly(interval) =>
context.log.debug(
"[{}] Starting repeated increments with interval [{}], current count is [{}]",
name,
interval,
n)
timers.startTimerWithFixedDelay("repeat", Increment, interval)
Behaviors.same
case Increment =>
val newValue = n + 1
context.log.debug("[{}] Incremented counter to [{}]", name, newValue)
counter(newValue)
case GetValue(replyTo) =>
replyTo ! Value(n)
Behaviors.same
}
}
// #fun-style-setup-params3
}
object FunctionalStyleSetupParams4 {
// #fun-style-setup-params4
// this works, but previous example is better for structuring more complex behaviors
object Counter {
sealed trait Command
case object Increment extends Command
final case class IncrementRepeatedly(interval: FiniteDuration) extends Command
final case class GetValue(replyTo: ActorRef[Value]) extends Command
final case class Value(n: Int)
def apply(name: String): Behavior[Command] =
Behaviors.setup { context =>
Behaviors.withTimers { timers =>
def counter(n: Int): Behavior[Command] =
Behaviors.receiveMessage {
case IncrementRepeatedly(interval) =>
context.log.debug(
"[{}] Starting repeated increments with interval [{}], current count is [{}]",
name,
interval,
n)
timers.startTimerWithFixedDelay("repeat", Increment, interval)
Behaviors.same
case Increment =>
val newValue = n + 1
context.log.debug("[{}] Incremented counter to [{}]", name, newValue)
counter(newValue)
case GetValue(replyTo) =>
replyTo ! Value(n)
Behaviors.same
}
counter(0)
}
}
}
// #fun-style-setup-params4
}
}

View file

@ -124,3 +124,69 @@ Some reasons why you may want to use the functional style:
@@@
## Passing around too many parameters
One thing you will quickly run into when using the functional style is that you need to pass around many parameters.
Let's add `name` parameter and timers to the previous `Counter` example. A first approach would be to just add those
as separate parameters:
Scala
: @@snip [StyleGuideDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala) { #fun-style-setup-params1 }
Java
: @@snip [StyleGuideDocExamples.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/StyleGuideDocExamples.java) { #fun-style-setup-params1 }
Ouch, that doesn't look good. More things may be needed, such as stashing or application specific "constructor"
parameters. As you can imagine, that will be too much boilerplate.
As a first step we can place all these parameters in a class so that we at least only have to pass around one thing.
Still good to have the "changing" state, the @scala[`n: Int`]@java[`final int n`] here, as a separate parameter.
Scala
: @@snip [StyleGuideDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala) { #fun-style-setup-params2 }
Java
: @@snip [StyleGuideDocExamples.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/StyleGuideDocExamples.java) { #fun-style-setup-params2 }
That's better. Only one thing to carry around and easy to add more things to it without rewriting everything.
@scala[Note that we also placed the `ActorContext` in the `Setup` class, and therefore switched from
`Behaviors.receive` to `Behaviors.receiveMessage` since we already have access to the `context`.]
It's still rather annoying to have to pass the same thing around everywhere.
We can do better by introducing an enclosing class, even though it's still using the functional style.
The "constructor" parameters can be @scala[immutable]@java[`final`] instance fields and can be accessed from
member methods.
Scala
: @@snip [StyleGuideDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala) { #fun-style-setup-params3 }
Java
: @@snip [StyleGuideDocExamples.java](/akka-actor-typed-tests/src/test/java/jdocs/akka/typed/StyleGuideDocExamples.java) { #fun-style-setup-params3 }
That's nice. One thing to be cautious with here is that it's important that you create a new instance for
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.]
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].
Then you need to relax the visibility constraints of the constructor and methods.
It's not recommended to place mutable state and @scala[`var` members]@java[non-final members] in the enclosing class.
It would be correct from an actor thread-safety perspective as long as the same instance of the enclosing class
is not shared between different actor instances, but if that is what you need you should rather use the
object-oriented style with the `AbstractBehavior` class.
@@@ div {.group-scala}
Similar can be achieved without an enclosing class by placing the `def counter` inside the `Behaviors.setup`
block. That works fine, but for more complex behaviors it can be better to structure the methods in a class.
For completeness, here is how it would look like:
Scala
: @@snip [StyleGuideDocExamples.scala](/akka-actor-typed-tests/src/test/scala/docs/akka/typed/StyleGuideDocExamples.scala) { #fun-style-setup-params4 }
@@@