Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ScalaでつくるInvalid stateのない世界

4e69879387679d5c1791f980e058e1b2?s=47 takkkun
July 02, 2016

ScalaでつくるInvalid stateのない世界

4e69879387679d5c1791f980e058e1b2?s=128

takkkun

July 02, 2016
Tweet

Transcript

  1. 4DBMBͰͭ͘Δ *OWBMJETUBUFͷͳ͍ੈք :"1$"TJB)BDIJPKJNJEJO4IJOBHBXBTUEBZ

  2. ࣗݾ঺հ ͨͬ͘ΜʢUBLLLVOʣ ͲͷαʔϏεͰ΋େମ͜ͷ໊લ 4DBMB 3VCZ &SMBOH

  3. ࠓճ͸Կͷ࿩ʁ 4DBMBͰͭ͘Δ *OWBMJETUBUFͷͳ͍ੈք

  4. ࠓճ͸Կͷ࿩ʁ 4DBMBͰͭ͘Δ *OWBMJETUBUFͷͳ͍ੈք ˣ όϦσʔγϣϯͷ͓࿩

  5. ࠓճ͸Կͷ࿩ʁ 4DBMBͰͭ͘Δ *OWBMJETUBUFͷͳ͍ੈք ˣ όϦσʔγϣϯͷ͓࿩ όϦσʔγϣϯʹࣦഊͨ͠ঢ়ଶΛࢦ͢

  6. ൃදʹࢸͬͨܦҢ w ීஈ4DBMBͰΞϓϦέʔγϣϯΞʔΩςΫνϟ͔ Βݕ౼ͭͭ͠։ൃ w ΦϒδΣΫτͷόϦσʔγϣϯʹؔͯ͠΋݁ߏ಄ Λ೧Δ͜ͱ͕͋ͬͨ w ࢼߦࡨޡ͍ͯͨ͠Βྑ͍ײ͡ͷ͕ग़དྷͨ w

    ౰࣌ʮϠύνʔ΍Δͧʂʯͱ͍͏੠
  7. ͳΒ࿩ͦ͏ʂ

  8. ΦϒδΣΫτΛόϦσʔγϣϯ͢Δ w ͱ͋ΔΦϒδΣΫτ͕͋Δ w ͦͷΦϒδΣΫτ͸ͻͱͭͳ͍͠͸ෳ਺ͷϑΟʔ ϧυΛ࣋ͭ w ͦͷϑΟʔϧυͷ಺༰೗ԿͰΦϒδΣΫτࣗମ ͷ༗ޮແޮΛܾఆ͢Δ

  9. class Person < ActiveRecord::Base validates :name, { presence: true }

    validates :age, { presence: true, numericality: {greater_than_or_equal_to: 0} } end person = Person.new(name: "takkkun", age: -1) if (person.invalid?) { # handle person.errors } ΦϒδΣΫτΛόϦσʔγϣϯ͢Δ "DUJWF3FDPSEͷྫ
  10. ΦϒδΣΫτΛόϦσʔγϣϯ͢Δ 4DBMBͷྫ class Person(val name: String, val age: Int) {

    def validate(): List[String] = { var errors: List[String] = List.empty if (name.isEmpty) { errors :+= "name cannot be blank" } if (age < 0) { errors :+= "age must be zero or a positive number" } errors } def isValid(): Boolean = validate().isEmpty }
  11. ѻ͏ଆ͕όϦσʔγϣϯ࣮ߦͷ੹೚Λ࣋ͭ val person = new Person("takkkun", -1) if (!person.isValid) {

    val errors = person.validate() // handle errors } ѻ͏ଆ͕όϦσʔγϣϯΛݺͼग़͠ɺؾʹ͔͚Δඞཁ͕͋Δ val person = new Person("takkkun", -1) // handle person όϦσʔγϣϯΛݺͼग़͞ͳ͍ϛε΋ى͜ΓಘΔ def handlePerson(person: Person) { if (person.isValid) { // handle person } } ؔ਺ͳͲͰڥքΛ·͍ͨͩ৔߹΋ؾʹ͔͚Δඞཁ͕͋Δ
  12. ΊΜͲ͍͘͞ʂ

  13. w ͜ͷΞϓϩʔν͸ʮΦϒδΣΫτ͕༗ޮແޮͱ ͍͏ঢ়ଶʯΛ࣋ͭ w ঢ়ଶΛ࣋ͭҎ্ؾʹ͔͚Δඞཁ͕͋Δ w ·ͨɺ༗ޮແޮҎ֎ʹ΋ঢ়ଶΛ࣋ͭ͜ͱ͸ߟ͑ ΒΕΔ w ͍ΖΜͳঢ়ଶ͕૊Έ߹Θ͞Δͱʜʜ

    ʮঢ়ଶʯ͸ةݥͳݴ༿
  14. ΊΜͲ͍͘͞ʂʂ

  15. ʮৗʹ༗ޮʯͱ͍͏ߟ͑ํ w ʮΦϒδΣΫτ͕༗ޮແޮͰ͋Δͱ͍͏ঢ়ଶʯ ͕अຐͳΒແͯ͘͠͠·͓͏ w ͢ͳΘͪʮৗʹ༗ޮʯ w ͜ͷΞϓϩʔνࣗମ͸௝͍͠Θ͚Ͱ΋ͳ͍ ʢྫ೔࣌ΫϥεͳͲʣ

  16. ʮৗʹ༗ޮʯͱ͍͏ߟ͑ํ w %%%ͷจ຺ʹ͓͚ΔΤϯςΟςΟͰɺʮৗʹ༗ ޮͳΤϯςΟςΟʯͱͯ͠l"MXBZTWBMJEFOUJUZz ͱ͍͏ݴ༿͕࢖ΘΕ͍ͯΔ w ͦΕʹͳΒ͍ʮৗʹ༗ޮʯ͸l"MXBZTWBMJEzͱশ ͢Δ

  17. "MXBZTWBMJE*OWBMJETUBUF͕ͳ͍

  18. 4DBMBͰͭ͘Δ *OWBMJETUBUFͷͳ͍ੈք

  19. ͪͳΈʹ "MXBZTWBMJEΛ൷൑͢Δߟ͑ํ΋͋Δ IUUQKF⒎SFZQBMFSNPDPNCMPHUIFGBMMBDZPG UIFBMXBZTWBMJEFOUJUZ

  20. class Person(val name: String, val age: Int) { var errors:

    List[String] = List.empty if (name.isEmpty) { errors :+= "name cannot be blank" } if (age < 0) { errors :+= "age must be zero or a positive number" } if (errors.nonEmpty) { throw new Exception(errors.mkString(", ")) } } 4DBMBͰ"MXBZTWBMJEΛ࣮ݱ͢Δ
  21. class Person(val name: String, val age: Int) { var errors:

    List[String] = List.empty if (name.isEmpty) { errors :+= "name cannot be blank" } if (age < 0) { errors :+= "age must be zero or a positive number" } if (errors.nonEmpty) { throw new Exception(errors.mkString(", ")) } } 4DBMBͰ"MXBZTWBMJEΛ࣮ݱ͢Δ όϦσʔγϣϯʹࣦഊͨ͠ͱ͖͸ྫ֎ͱ͢Δ
  22. "MXBZTWBMJE͸ຬ͍ͨͤͯΔ͕ʜʜ w ίϯετϥΫλͰ͸ࣄ্࣮ྫ֎ͰࣦഊΛ஌Βͤ Δ͔͠ͳ͍ͷͰɺऔΓճ͕͠ޮ͔ͳ͍ʢҰԠ TDBMBVUJM5SZͰͳΜͱ͔ग़དྷΔʣ w όϦσʔγϣϯΛॻ͔ͳ͍ϛε͸๷͛ͳ͍ʢς ετॻ͜͏Ͷʣ w ͳΜͱ͔͸ͳΔ͕ɺͳΜ͔ͩΜͩ৽ͨͳ໰୊͕

    ු্ͯ͘͠Δ
  23. 4DBMBͰ"MXBZTWBMJEΛ࣮ݱ͢Δվ class Person private (val name: String, val age: Int)

    object Person { def spawn(name: Indefinite[String], age: Indefinite[Int]): Person = { // ... } }
  24. 4DBMBͰ"MXBZTWBMJEΛ࣮ݱ͢Δվ class Person private (val name: String, val age: Int)

    object Person { def spawn(name: Indefinite[String], age: Indefinite[Int]): Person = { // ... } } ίϯετϥΫλΛQSJWBUFʹ͢ΔʢΫϥεࣗମ͸QVCMJDʣ
  25. 4DBMBͰ"MXBZTWBMJEΛ࣮ݱ͢Δվ class Person private (val name: String, val age: Int)

    object Person { def spawn(name: Indefinite[String], age: Indefinite[Int]): Person = { // ... } } TQBXOϝιουܦ༝ͰͷΈ1FSTPOΫϥεͷΠϯελϯεΛੜ੒Մೳ
  26. 4DBMBͰ"MXBZTWBMJEΛ࣮ݱ͢Δվ class Person private (val name: String, val age: Int)

    object Person { def spawn(name: Indefinite[String], age: Indefinite[Int]): Person = { // ... } } ܕΛม͑ͯ௚઀౉ͤͳ͍Α͏ʹ͢Δ
  27. class Indefinite[A](value: A) { def validateWith(validateValue: A => List[String]): Validity[A]

    = { val errors = validateValue(value) if (errors.isEmpty) Valid(value) else Invalid(errors) } } sealed trait Validity[+A] { def value: A def errors: List[String] } case class Valid[A](value: A) extends Validity[A] { val errors = List.empty } case class Invalid(errors: List[String]) extends Validity[Nothing] { def value = throw new NoSuchElementException("Invalid") } 4DBMBͰ"MXBZTWBMJEΛ࣮ݱ͢Δվ
  28. class Indefinite[A](value: A) { def validateWith(validateValue: A => List[String]): Validity[A]

    = { val errors = validateValue(value) if (errors.isEmpty) Valid(value) else Invalid(errors) } } sealed trait Validity[+A] { def value: A def errors: List[String] } case class Valid[A](value: A) extends Validity[A] { val errors = List.empty } case class Invalid(errors: List[String]) extends Validity[Nothing] { def value = throw new NoSuchElementException("Invalid") } ೚ҙͷܕʢ"ʣͷ஋ΛऔΔɻWBMͰम০͍ͯ͠ͳ͍ͷͰɺ֎෦ʹެ։͞Εͳ͍ 4DBMBͰ"MXBZTWBMJEΛ࣮ݱ͢Δվ
  29. class Indefinite[A](value: A) { def validateWith(validateValue: A => List[String]): Validity[A]

    = { val errors = validateValue(value) if (errors.isEmpty) Valid(value) else Invalid(errors) } } sealed trait Validity[+A] { def value: A def errors: List[String] } case class Valid[A](value: A) extends Validity[A] { val errors = List.empty } case class Invalid(errors: List[String]) extends Validity[Nothing] { def value = throw new NoSuchElementException("Invalid") } WBMJEBUF8JUIϝιουͰ಺แ͍ͯ͠Δ஋ͷ༗ޮແޮΛ͔֬ΊΔ 4DBMBͰ"MXBZTWBMJEΛ࣮ݱ͢Δվ
  30. 4DBMBͰ"MXBZTWBMJEΛ࣮ݱ͢Δվ class Person private (val name: String, val age: Int)

    object Person { def spawn(name: Indefinite[String], age: Indefinite[Int]): Person = { (name.validateWith(validateName), age.validateWith(validateAge)) match { case (Valid(validName), Valid(validAge)) => new Person(validName, validAge) case (validityName, validityAge) => throw new Exception((validityName.errors ++ validityAge.errors).mkString(", ")) } } private def validateName(name: String): List[String] = { if (name.isEmpty) List("name cannot be blank") else List.empty } private def validateAge(age: Int): List[String] = { if (age < 0) List("age must be zero or a positive number") else List.empty } }
  31. 4DBMBͰ"MXBZTWBMJEΛ࣮ݱ͢Δվ class Person private (val name: String, val age: Int)

    object Person { def spawn(name: Indefinite[String], age: Indefinite[Int]): Person = { (name.validateWith(validateName), age.validateWith(validateAge)) match { case (Valid(validName), Valid(validAge)) => new Person(validName, validAge) case (validityName, validityAge) => throw new Exception((validityName.errors ++ validityAge.errors).mkString(", ")) } } private def validateName(name: String): List[String] = { if (name.isEmpty) List("name cannot be blank") else List.empty } private def validateAge(age: Int): List[String] = { if (age < 0) List("age must be zero or a positive number") else List.empty } } WBMJEBUF8JUIʹόϦσʔγϣϯ༻ͷؔ਺Λ ౉͠ɺ಺แ͞Ε͍ͯΔ஋ͷ༗ޮແޮΛಘΔ
  32. 4DBMBͰ"MXBZTWBMJEΛ࣮ݱ͢Δվ class Person private (val name: String, val age: Int)

    object Person { def spawn(name: Indefinite[String], age: Indefinite[Int]): Person = { (name.validateWith(validateName), age.validateWith(validateAge)) match { case (Valid(validName), Valid(validAge)) => new Person(validName, validAge) case (validityName, validityAge) => throw new Exception((validityName.errors ++ validityAge.errors).mkString(", ")) } } private def validateName(name: String): List[String] = { if (name.isEmpty) List("name cannot be blank") else List.empty } private def validateAge(age: Int): List[String] = { if (age < 0) List("age must be zero or a positive number") else List.empty } } ύλʔϯϚονͰ͢΂ͯ༗ޮͷͱ͖ͷΈ1FSTPOΫϥεͷΠϯελϯεΛੜ੒͢Δ
  33. ࢖༻ྫ try { val name = new Indefinite("takkkun") val age

    = new Indefinite(-1) val person = Person.spawn(name, age) // handle person } catch { case e: Exception => // handle exception }
  34. &JUIFSόʔδϣϯ object Person { def spawn(name: Indefinite[String], age: Indefinite[Int]): Either[List[String],

    Person] = { (name.validateWith(validateName), age.validateWith(validateAge)) match { case (Valid(validName), Valid(validAge)) => Right(new Person(validName, validAge)) case (validityName, validityAge) => Left(validityName.errors ++ validityAge.errors) } } // ... }
  35. &JUIFSόʔδϣϯ࢖༻ྫ val name = new Indefinite("takkkun") val age = new

    Indefinite(-1) val errorsOrPerson = Person.spawn(name, age) errorsOrPerson match { case Left(errors) => // handle errors case Right(person) => // handle person }
  36. ·ͱΊ w ʮΦϒδΣΫτ͕༗ޮ͔ແޮ͔ͱ͍͏ঢ়ଶʯΛ ࣋ͭͷ͸໘౗͞૿େͷஹީ w ނʹΦϒδΣΫτ͸ৗʹ༗ޮʢ"MXBZTWBMJEʣͰ ͋Εɺͱ͍͏ߟ͑ํ w 4DBMBͳͲͷ੩తܕ෇͚ݴޠͰ͋Ε͹ɺܕͰόϦ σʔγϣϯʹڧ੍ྗΛ࣋ͨͤΒΕΔ

  37. ϦϥΫͰ͸։ൃऀΛืू͍ͯ͠·͢ w ීஈ͜͏͍͏͜ͱΛߟ͑ͳ͕Β։ൃ͍ͯ͠·͢ w αʔόʔαΠυ͸4DBMB͕ओ w J04 4XJGU "OESPJE΋ w

    ΍Δ͜ͱ͍ͬͺ͍͋Δ w ڵຯ͕͋Δํ͸!UBLLLVO·Ͱ
  38. ͓͠·͍