Sealed classes opened my mind

Sealed classes opened my mind

How we use Kotlin to tame state

74af653995934bb432936a536761be33?s=128

Patrick Cousins

October 05, 2018
Tweet

Transcript

  1. Patrick Cousins @alostpacket Sealed classes opened my mind How we

    use Kotlin to tame state at Etsy
  2. Patrick Cousins @alostpacket Sealed classes opened my mind How we

    use Kotlin to tame state at Etsy a ❤ story
  3. 3 Etsy’s apps have a lot of state

  4. 4 Listings tags attributes materials variations production partners shipping profiles

    payment methods conversations messages images lions tigers snippets
  5. 5 Crochet Amigurumi Teddy Bear pattern By TinyAmigurumi

  6. 6 Listings tags attributes materials variations production partners shipping profiles

    payment methods conversations messages images lions tigers snippets
  7. 7 100% natural wool - Chunky crochet cat bed By

    CommeDesMoutons
  8. 8 Listings tags attributes materials variations production partners shipping profiles

    payment methods conversations messages images lions tigers snippets
  9. 9 Help!

  10. 10 There are numerous techniques out there that help manage

    state.
  11. 11 Engraved wooden sign By IntricateCSigns

  12. Kotlin’s sealed classes 12

  13. 13 MVVM + sealed classes =  Redux + sealed

    classes =  MVI + sealed classes =  Repository + sealed classes = 
  14. 14 What are sealed classes?

  15. 15 Sealed classes are used for representing restricted class hierarchies,

    when a value can have one of the types from a limited set, but cannot have any other type. They are, in a sense, an extension of enum classes: the set of values for an enum type is also restricted, but each enum constant exists only as a single instance, whereas a subclass of a sealed class can have multiple instances which can contain state. Definition of sealed classes from the official documentation
  16. 16 • Restricted hierarchies

  17. 17 • Restricted hierarchies • Set

  18. 18 • Restricted hierarchies • Set • Types

  19. 19 • Restricted hierarchies • Set • Types • Contain

    values
  20. 20 sealed class Result { class Success(val items: List<String>): Result()

    class Failure(val error: Throwable): Result() }
  21. 21 when (result) { is Result.Success -> showItems(result.items) is Result.Failure

    -> result.error.printStackTrace() }
  22. 22 when (result) { is Result.Success -> showItems(result.items) is Result.Failure

    -> result.error.printStackTrace() }
  23. 23 when (result) { is Result.Success -> showItems(result.items) is Result.Failure

    -> result.error.printStackTrace() }
  24. 24 when (result) { is Result.Success -> showItems(result.items) is Result.Failure

    -> result.error.printStackTrace() } Look Ma! No else!
  25. 25 sealed class Result { class Success(val items: List<String>): Result()

    class Failure(val error: Throwable): Result() object Cancelled: Result() }
  26. 26 val done = when (result) { is Result.Success ->

    showList(result.items) is Result.Failure -> result.error.printStackTrace() }
  27. 27 val done = when (result) { is Result.Success ->

    showList(result.items) is Result.Failure -> result.error.printStackTrace() }
  28. 28 val done = when (result) { is Result.Success ->

    showList(result.items) is Result.Failure -> result.error.printStackTrace() }
  29. 29 val done = when (result) { is Result.Success ->

    showList(result.items) is Result.Failure -> result.error.printStackTrace() }
  30. 30 val done = when (result) { is Result.Success ->

    showList(result.items) is Result.Failure -> result.error.printStackTrace() is Result.Cancelled -> TODO() }
  31. 31 sealed class Result { class Success(val items: List<String>): Result()

    class Failure(val error: Throwable): Result() object Cancelled: Result() } val done = when (result) { is Result.Success -> showList(result.items) is Result.Failure -> result.error.printStackTrace() is Result.Cancelled -> hideStuff() }
  32. 32 sealed class Yarn { class Cotton : Yarn() class

    Wool : Yarn() }
  33. 33 sealed class Yarn { class Cotton : Yarn() open

    class Wool : Yarn() } class Merino : Yarn.Wool()
  34. 34 val yarn = when (spool) { is Yarn.Wool ->

    knit() is Yarn.Cotton -> knit() is Yarn.Wool.Merino -> ??? }
  35. 35 val yarn = when (spool) { is Yarn.Wool ->

    knit() is Yarn.Cotton -> knit() }
  36. 36 sealed class Yarn { class Cotton : Yarn() class

    Silk : Yarn() sealed class Wool : Yarn() { class Merino : Wool() class Alpaca : Wool() } }
  37. 37 val yarn = when (spool) { is Yarn.Silk ->

    knit() is Yarn.Cotton -> knit() }
  38. 38 val yarn = when (spool) { is Yarn.Silk ->

    knit() is Yarn.Cotton -> knit() is Yarn.Wool.Merino -> TODO() is Yarn.Wool.Alpaca -> TODO() }
  39. 39 val yarn = when (spool) { is Yarn.Silk ->

    knit() is Yarn.Cotton -> knit() is Yarn.Wool -> knit() }
  40. 40 //same file only! sealed class Yarn { class Cotton

    : Yarn() class Silk : Yarn() } sealed class Wool : Yarn() { class Merino : Wool() class Alpaca : Wool() }
  41. 41 val yarn = when (spool) { is Yarn.Silk ->

    knit() is Yarn.Cotton -> knit() }
  42. 42 val yarn = when (spool) { is Yarn.Silk ->

    knit() is Yarn.Cotton -> knit() is Wool.Merino -> TODO() is Wool.Alpaca -> TODO() }
  43. 43 val yarn = when (spool) { is Yarn.Silk ->

    knit() is Yarn.Cotton -> knit() is Wool -> //NOPE doesn’t work now }
  44. Small Alpaca and Wool Socks, Grown in Michigan By NorthStarAlpacas

    As our state grows in complexity, there are times in which we may only be concerned about a subset of that state. Only wool socks please
  45. 45 val yarnStash: Sequence<Yarn> = listOf( Yarn.Cotton(), Wool.Merino(), Wool.Alpaca(), Yarn.Silk()

    ).asSequence()
  46. 46 val warmWool = yarnStash.filterIsInstance<Wool>()

  47. 47 val warmWool = yarnStash.filterIsInstance<Wool>() warmWool.forEach { val yarn =

    when (it) { is Wool.Merino -> knit() is Wool.Alpaca -> knit() } }
  48. 48 //Using Rx and Redux yarn.asObservable() .map { it.wool }

    .subscribe { val yarn = when (it) { is Wool.Merino -> knit() is Wool.Alpaca -> knit() } }
  49. 49 //Using Rx and Redux yarn.asObservable() .map { it.wool }

    .distinctUntilChanged() .subscribe { val yarn = when (it) { is Wool.Merino -> knit() is Wool.Alpaca -> knit() } }
  50. 50 //Using Rx and Redux yarn.asObservable() .map { it.wool }

    .distinctUntilChanged() .subscribe { val yarn = when (it) { is Wool.Merino -> knit() is Wool.Alpaca -> knit() } }
  51. 51 //Using Rx and Redux yarn.asObservable() .map { it.wool }

    .distinctUntilChanged { wool -> wool.images } .subscribe { val yarn = when (it) { is Wool.Merino -> knit() is Wool.Alpaca -> knit() } }
  52. 52 //Using Rx and Redux yarn.asObservable() .map { it.wool }

    .distinctUntilChanged { old, new -> new.num > 0 } .subscribe { val yarn = when (it) { is Wool.Merino -> knit() is Wool.Alpaca -> knit() } }
  53. 53 Meanwhile

  54. 54 ❤ sealed classes

  55. 55 Social Sign On Facebook Google Username Email Password Two

    Factor Auth Smoke Signal Carrier Pigeon Morse Code Interpretive dance
  56. 56 • Async calls to an External API • Users

    leaving the app • Pausing and waiting for input • Stateful flows
  57. 57

  58. 58 Modeling network call responses makes for a great place

    to start with sealed classes.
  59. 59 sealed class SocialSignInResult { data class Success(val token: String)

    : SocialSignInResult() data class Link(val token: String) : SocialSignInResult() data class Register(val token: String) : SocialSignInResult() data class Error(val errorMessage: String) : SocialSignInResult() }
  60. 60 sealed class SignInResult { data class Success(val token: String)

    : SignInResult() data class TwoFactorAuth(val token: String) : SignInResult() data class Fail(val token: String) : SignInResult() data class RetrySocial(val errorMessage: String) : SignInResult() }
  61. 61 sealed class TwoFAResult { object Success : TwoFAResult() data

    class Retry2FA(val token: String) : TwoFAResult() data class Fail(val errorMsg: String) : TwoFAResult() data class RetrySocial(val errorMsg: String) : TwoFAResult() }
  62. 62 sealed class SocialSignInResult { data class Success(val token: String)

    : SocialSignInResult() data class Link(val token: String) : SocialSignInResult() data class Register(val token: String) : SocialSignInResult() data class Error(val errorMsg: String) : SocialSignInResult() } sealed class SignInResult { data class Success(val token: String) : SignInResult() data class TwoFactorAuth(val token: String) : SignInResult() data class Fail(val token: String) : SignInResult() data class RetrySocial(val errorMsg: String) : SignInResult() } sealed class TwoFAResult { object Success : TwoFAResult() data class Retry2FA(val token: String) : TwoFAResult() data class Fail(val errorMsg: String) : TwoFAResult() data class RetrySocial(val errorMsg: String) : TwoFAResult() }
  63. 63 sealed class SocialSignInResult { data class Success(val token: String)

    : SocialSignInResult() data class Link(val token: String) : SocialSignInResult() data class Register(val token: String) : SocialSignInResult() data class Error(val errorMsg: String) : SocialSignInResult() } sealed class SignInResult { data class Success(val token: String) : SignInResult() data class TwoFactorAuth(val token: String) : SignInResult() data class Fail(val token: String) : SignInResult() data class RetrySocial(val errorMsg: String) : SignInResult() } sealed class TwoFAResult { object Success : TwoFAResult() data class Retry2FA(val token: String) : TwoFAResult() data class Fail(val errorMsg: String) : TwoFAResult() data class RetrySocial(val errorMsg: String) : TwoFAResult() }
  64. 64 sealed class SocialSignInResult { data class Success(val token: String)

    : SocialSignInResult() data class Link(val token: String) : SocialSignInResult() data class Register(val token: String) : SocialSignInResult() data class Error(val errorMsg: String) : SocialSignInResult() } sealed class SignInResult { data class Success(val token: String) : SignInResult() data class TwoFactorAuth(val token: String) : SignInResult() data class Fail(val token: String) : SignInResult() data class RetrySocial(val errorMsg: String) : SignInResult() } sealed class TwoFAResult { object Success : TwoFAResult() data class Retry2FA(val token: String) : TwoFAResult() data class Fail(val errorMsg: String) : TwoFAResult() data class RetrySocial(val errorMsg: String) : TwoFAResult() }
  65. 65 sealed class SocialSignInResult { data class Success(val token: String)

    : SocialSignInResult() data class Link(val token: String) : SocialSignInResult() data class Register(val token: String) : SocialSignInResult() data class Error(val errorMsg: String) : SocialSignInResult() } sealed class SignInResult { data class Success(val token: String) : SignInResult() data class TwoFactorAuth(val token: String) : SignInResult() data class Fail(val token: String) : SignInResult() data class RetrySocial(val errorMsg: String) : SignInResult() } sealed class TwoFAResult { object Success : TwoFAResult() data class Retry2FA(val token: String) : TwoFAResult() data class Fail(val errorMsg: String) : TwoFAResult() data class RetrySocial(val errorMsg: String) : TwoFAResult() }
  66. 66 prefer duplication over the wrong abstraction” -Sandi Metz https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstraction

  67. 67 sealed class SocialSignInResult { data class Success(val token: String)

    : SocialSignInResult() data class Link(val token: String) : SocialSignInResult() data class Register(val token: String) : SocialSignInResult() data class Error(val errorMsg: String) : SocialSignInResult() } sealed class SignInResult { data class Success(val token: String) : SignInResult() data class TwoFactorAuth(val token: String) : SignInResult() data class Fail(val token: String) : SignInResult() data class RetrySocial(val errorMsg: String) : SignInResult() } sealed class TwoFAResult { object Success : TwoFAResult() data class Retry2FA(val token: String) : TwoFAResult() data class Fail(val errorMsg: String) : TwoFAResult() data class RetrySocial(val errorMsg: String) : TwoFAResult() }
  68. 68 sealed class SocialSignInResult { data class Success(val token: String)

    : SocialSignInResult() data class Link(val token: String) : SocialSignInResult() data class Register(val token: String) : SocialSignInResult() data class Error(val errorMsg: String) : SocialSignInResult() } sealed class SignInResult { data class Success(val token: String) : SignInResult() data class TwoFactorAuth(val token: String) : SignInResult() data class Fail(val token: String) : SignInResult() data class RetrySocial(val errorMsg: String) : SignInResult() } sealed class TwoFAResult { object Success : TwoFAResult() data class Retry2FA(val token: String) : TwoFAResult() data class Fail(val errorMsg: String) : TwoFAResult() data class RetrySocial(val errorMsg: String) : TwoFAResult() }
  69. 69 Social Sign in -> Sign in -> Error Got

    an error at any point along the way !=
  70. 70 sealed class SocialSignInResult { data class Success(val token: String)

    : SocialSignInResult() data class Link(val token: String) : SocialSignInResult() data class Register(val token: String) : SocialSignInResult() data class Error(val errorMsg: String) : SocialSignInResult() } sealed class SignInResult { data class Success(val token: String) : SignInResult() data class TwoFactorAuth(val token: String) : SignInResult() data class Fail(val token: String) : SignInResult() data class RetrySocial(val errorMsg: String) : SignInResult() } sealed class TwoFAResult { object Success : TwoFAResult() data class Retry2FA(val token: String) : TwoFAResult() data class Fail(val errorMsg: String) : TwoFAResult() data class RetrySocial(val errorMsg: String) : TwoFAResult() }
  71. 71 Finite State Machine?

  72. 72 Finite State Machine? Sure, if you want! Up to

    you!
  73. 73 sealed class SocialSignInResult { data class Success(val token: String)

    : SocialSignInResult() data class Link(val token: String) : SocialSignInResult() data class Register(val token: String) : SocialSignInResult() data class Error(val errorMsg: String) : SocialSignInResult() } sealed class SignInResult { data class Success(val token: String) : SignInResult() data class TwoFactorAuth(val token: String) : SignInResult() data class Fail(val token: String) : SignInResult() data class RetrySocial(val errorMsg: String) : SignInResult() } sealed class TwoFAResult { object Success : TwoFAResult() data class Retry2FA(val token: String) : TwoFAResult() data class Fail(val errorMsg: String) : TwoFAResult() data class RetrySocial(val errorMsg: String) : TwoFAResult() }
  74. 74 Be pragmatic Do what works for you

  75. 75 sealed class SocialSignIn { sealed class SignIn: SocialSignIn() {

    class Success : SignIn() sealed class TwoFactor : SignIn() { class Success : TwoFactor() class RetryTwoFactor : TwoFactor() class Fail : TwoFactor() class RetrySocial : TwoFactor() } class Fail : SignIn() class RetrySocial : SignIn() } sealed class Link: SocialSignIn() { class Success : Link() sealed class TwoFactor : Link() { class Success : TwoFactor() class RetryTwoFactor : TwoFactor() class Fail : TwoFactor() class RetrySocial : TwoFactor() } class Fail : Link() class RetrySocial : Link() } sealed class Register : SocialSignIn() { class Success : Register() class Fail : Register() class RetrySocial : Register() } class Error(val errorMessage: String) : SocialSignIn() }
  76. 76 val state = SocialSignIn.Link.TwoFactor.RetrySocial()

  77. 77 sealed class SocialSignIn { class Error(val errorMessage: String) :

    SocialSignIn() } sealed class Register : SocialSignIn() { class Success : Register() class Fail : Register() class RetrySocial : Register() } sealed class SignIn: SocialSignIn() { class Success : SignIn() class Fail : SignIn() class RetrySocial : SignIn() } sealed class TwoFactor : SignIn() { class Success : TwoFactor() class RetryTwoFactor : TwoFactor() class Fail : TwoFactor() class RetrySocial : TwoFactor() } sealed class Link: SocialSignIn() { class Success : Link() class Fail : Link() class RetrySocial : Link() }
  78. 78 val state = TwoFactor.RetrySocial()

  79. 79 Why do all this?

  80. 80 If we can rely on contracts and preconditions, we

    can do more Compartmentalize the unknown
  81. Lean on the language Lean on the type system 81

  82. 82 What about other patterns?

  83. 83 We stand on the shoulders of giants” -A pragmatic

    person who likes cheesy quotes “
  84. 84 Visitor pattern

  85. 85 Visitor pattern Great: Type safety Works around Java’s lack

    of dynamic dispatch Polymorphic can keep things loosely coupled
  86. 86 Visitor pattern Not so great: Lots to keep track

    of Lots of indirection Not fluent / symantec Not friendly to beginners
  87. 87 interface CarElement { void accept(CarElementVisitor visitor); } interface CarElementVisitor

    { void visit(Body body); void visit(Car car); void visit(Engine engine); void visit(Wheel wheel); } class Car implements CarElement { CarElement[] elements; public Car() { this.elements = new CarElement[] { new Wheel("front left"), new Wheel("front right"), new Wheel("back left"), new Wheel("back right"), new Body(), new Engine() }; } public void accept(final CarElementVisitor visitor) { for (CarElement elem : elements) { elem.accept(visitor); } visitor.visit(this); } } class Body implements CarElement { public void accept(final CarElementVisitor visitor) { visitor.visit(this); } } class Engine implements CarElement { public void accept(final CarElementVisitor visitor) { visitor.visit(this); } } class Wheel implements CarElement { private String name; public Wheel(final String name) { this.name = name; } public String getName() { return name; } public void accept(final CarElementVisitor visitor) { visitor.visit(this); } } class CarElementDoVisitor implements CarElementVisitor { public void visit(final Body body) { System.out.println("Moving my body"); } public void visit(final Car car) { System.out.println("Starting my car"); } public void visit(final Wheel wheel) { System.out.println("Kicking my " + wheel.getName() + " wheel"); } public void visit(final Engine engine) { System.out.println("Starting my engine"); } } class CarElementPrintVisitor implements CarElementVisitor { public void visit(final Body body) { System.out.println("Visiting body"); } public void visit(final Car car) { System.out.println("Visiting car"); } public void visit(final Engine engine) { System.out.println("Visiting engine"); } public void visit(final Wheel wheel) { System.out.println("Visiting " + wheel.getName() + " whee } } public class VisitorDemo { public static void main(final String[] args) { final Car car = new Car(); car.accept(new CarElementPrintVisitor()); car.accept(new CarElementDoVisitor()); } }
  88. 88 Great video on YouTube: The Visitor Design Pattern is

    obsolete in Kotlin By Fred Overflow https://www.youtube.com/watch?v=4qLj-kSOZLQ
  89. 89 Delegate Adapter pattern

  90. 90 Delegate Adapter pattern Reasonable type safety More symantec Not

    as hard for beginners Works around dynamic dispatch Polymorphic can keep things loosely coupled
  91. 91 Delegate Adapter pattern Lots to keep track of Lots

    of indirection Abstraction over an abstraction Leaky
  92. 92 We stand on the shoulders of giants” -A pragmatic

    person who likes cheesy quotes “
  93. 93 switch (...) { } Is instanceof a code smell?

    Should we prefer polymorphism? Why?
  94. 94 if (item instanceof Wheel) { //downcast and handle item

    as Wheel ((Wheel) item ).turn(); } else if (item instanceof Car) { //downcast and handle item as Car ((Car) item ).drive(); }
  95. 95 Branching on a heterogenous collection of items

  96. 96 But isn’t Kotlin’s is operator is the same as

    instanceof from Java!
  97. 97 Yes! But isn’t Kotlin’s is operator is the same

    as instanceof from Java!
  98. 98 Final classes by default Sealed classes cannot be opened

    No need for polymorphism! Branches can only be defined in the same file But isn’t Kotlin’s is operator is the same as instanceof from Java!
  99. 99 Thanks Patrick Cousins @alostpacket 100% natural wool - Chunky

    crochet cat bed By CommeDesMoutons