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

Mehr Sicherheit durch Refined Types

Mehr Sicherheit durch Refined Types

Ein kurzer Abriß was Refined Types eigentlich sind und anschließend anhand von Beispielen Anwendungsfälle, die von deren Einsatz profitieren.

Jens Grassel

December 12, 2019
Tweet

More Decks by Jens Grassel

Other Decks in Programming

Transcript

  1. Was sind Types? • einfach nur Datentypen • val myString:

    String • val myNumber: Int • und so weiter...
  2. Wo ist das Problem? Dann ist doch alles schick, oder?

    def parsePort(s: String): Int = s.toInt parsePort("Ups!") def startSocket(port: Int): Unit = { val serverSocket = new ServerSocket(port) // ... } startSocket(parsePort("-1")) // Ups... Dies ist nur ein Beispiel von vielen.
  3. Und was machen Refined Types? Wir erg¨ anzen die verf¨

    ugbaren Typen durch weitere Beschr¨ ankungen. type PortNumber = Int Refined Interval.Closed[W.‘1‘.T, W.‘65535‘.T] def startSocket(p: PortNumber): Unit = { val serverSocket = new ServerSocket(p) // ... } startSocket(-1) // Compiliert nicht!
  4. Also was machen Refined Types nun genau? Sie verwandeln Laufzeitprobleme

    in Compilierzeitprobleme! Und mehr, aber dazu sp¨ ater...
  5. Reduzierung des Implementierungsraums ( ) Je strikter die Typen sind,

    desto einfacher ist die Implementierung. final case class Service(host: String, port: Int) def hashName(name: String) versus final case class Service(host: String Refined IPv4, port: PortNumber) def hashName(name: String Refined NonEmpty)
  6. Reduzierung des Implementierungsraums ( ) Beispiel: Integration in ein bestehendes

    System • Produktdaten von extern • weitgehend kompatibel • Altsystem unterst¨ utzt nur Artikelnummern gr¨ oßer final case class ArtNo(n: Int) { require(n > 4096, "Illegal article number!") } Das ist beschissen! Tut das nicht!
  7. Reduzierung des Implementierungsraums ( ) Das hier ist etwas besser:

    final case class ArtNo(n: Int) object ArtNo { def apply(n: Int): Option[ArtNo] = if (n > 4096) Option(new ArtNo(n)) else None } Aber wir k¨ onnen das hier haben... :-) type ArtNo = Int Refined Greater[4096]
  8. Konfigurationsdaten ( ) Konfigurationsdateien bieten eine Vielzahl an potentiellen Fehlerquellen

    database { driver = "org.postgresql.Driver" url = "jdbc:postgresql://localhost/db" user = "username" pass = "secret" }
  9. Konfigurationsdaten ( ) Absicherung durch refined und pureconfig final case

    class DatabaseConfig(driver: DatabaseDriverName, url: DatabaseUrl, user: DatabaseLogin, pass: DatabasePassword) object DatabaseConfig { implicit val configReader: ConfigReader[DatabaseConfig] = deriveReader[DatabaseConfig] } ⇒ Validierung zur Laufzeit! (kein eigener Code n¨ otig!)
  10. Serialisierung und Deserialisierung ¨ ahnelt der Konfigurationsproblematik • Parsen oder

    Erstellen von diversen Formaten • heutzutage zumeist JSON • auch hier erh¨ ohte Sicherheit durch strengere Typisierung der Modelle • Integration von refined mit Circe (JSON Bibiliothek) • Decoder und Encoder frei Haus
  11. Weniger Tests durch bessere Typisierung ( ) Was testen wir

    eigentlich? • sehr oft Eingabewerte (erlaubte Wertebereich) "Creating a user" when { "name is empty" must { "throw an exception" in { the [IllegalArgumentException] thrownBy createUser(name = "", age = 18) must have message "Name must be present!" } } }
  12. Weniger Tests durch bessere Typisierung ( ) type UserName =

    String Refined NonEmpty type UserAge = Int Refined Positive def createUser(n: UserName)(a: UserAge): User = ??? Problem gel¨ ost! :-)
  13. Generelles Vermeidet ”stringified programming”! final case class Foo(login: String, email:

    String, collectedStars: Int) versus final case class Foo(login: LoginName, email: EMail, stars: CollectedStars) Der Compiler kann euch dann helfen!