diff --git a/_overviews/scala3-book/domain-modeling-tools.md b/_overviews/scala3-book/domain-modeling-tools.md index 52ff30139e..2c485e3a5f 100644 --- a/_overviews/scala3-book/domain-modeling-tools.md +++ b/_overviews/scala3-book/domain-modeling-tools.md @@ -8,7 +8,8 @@ previous-page: domain-modeling-intro next-page: domain-modeling-oop --- -Scala 3 provides many different constructs so we can model the world around us: + +Scala provides many different constructs so we can model the world around us: - Classes - Objects @@ -16,23 +17,29 @@ Scala 3 provides many different constructs so we can model the world around us: - Traits - Abstract classes - Enums +Scala 3 only - Case classes - Case objects This section briefly introduces each of these language features. - ## Classes As with other languages, a _class_ in Scala is a template for the creation of object instances. Here are some examples of classes: +{% tabs class_1 %} +{% tab 'Scala 2 and 3' %} + ```scala class Person(var name: String, var vocation: String) class Book(var title: String, var author: String, var year: Int) class Movie(var name: String, var director: String, var year: Int) ``` +{% endtab %} +{% endtabs %} + These examples show that Scala has a very lightweight way to declare classes. All the parameters of our example classes are defined as `var` fields, which means they are mutable: you can read them, and also modify them. @@ -40,37 +47,85 @@ If you want them to be immutable---read only---create them as `val` fields inste Prior to Scala 3, you used the `new` keyword to create a new instance of a class: +{% tabs class_2 %} +{% tab 'Scala 2 and 3' %} + ```scala val p = new Person("Robert Allen Zimmerman", "Harmonica Player") // --- ``` -However, with [creator applications][creator] this isn’t required in Scala 3: +{% endtab %} +{% endtabs %} + +However, with [universal apply methods][creator] this isn’t required in Scala 3: +Scala 3 only + +{% tabs class_3 %} +{% tab 'Scala 3 Only' %} ```scala val p = Person("Robert Allen Zimmerman", "Harmonica Player") ``` +{% endtab %} +{% endtabs %} + Once you have an instance of a class such as `p`, you can access its fields, which in this example are all constructor parameters: +{% tabs class_4 %} +{% tab 'Scala 2 and 3' %} + ```scala p.name // "Robert Allen Zimmerman" p.vocation // "Harmonica Player" ``` +{% endtab %} +{% endtabs %} + As mentioned, all of these parameters were created as `var` fields, so you can also mutate them: +{% tabs class_5 %} +{% tab 'Scala 2 and 3' %} + ```scala p.name = "Bob Dylan" p.vocation = "Musician" ``` +{% endtab %} +{% endtabs %} + ### Fields and methods Classes can also have methods and additional fields that are not part of constructors. They are defined in the body of the class. The body is initialized as part of the default constructor: +{% tabs method class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +class Person(var firstName: String, var lastName: String) { + + println("initialization begins") + val fullName = firstName + " " + lastName + + // a class method + def printFullName: Unit = + // access the `fullName` field, which is created above + println(fullName) + + printFullName + println("initialization ends") +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + ```scala class Person(var firstName: String, var lastName: String): @@ -86,9 +141,26 @@ class Person(var firstName: String, var lastName: String): println("initialization ends") ``` +{% endtab %} +{% endtabs %} + The following REPL session shows how to create a new `Person` instance with this class: +{% tabs demo-person class=tabs-scala-version %} +{% tab 'Scala 2' %} +````scala +scala> val john = new Person("John", "Doe") +initialization begins +John Doe +initialization ends +val john: Person = Person@55d8f6bb + +scala> john.printFullName +John Doe ```` +{% endtab %} +{% tab 'Scala 3' %} +````scala scala> val john = Person("John", "Doe") initialization begins John Doe @@ -98,6 +170,8 @@ val john: Person = Person@55d8f6bb scala> john.printFullName John Doe ```` +{% endtab %} +{% endtabs %} Classes can also extend traits and abstract classes, which we cover in dedicated sections below. @@ -105,13 +179,43 @@ Classes can also extend traits and abstract classes, which we cover in dedicated As a quick look at a few other features, class constructor parameters can also have default values: +{% tabs default-values_1 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +class Socket(val timeout: Int = 5_000, val linger: Int = 5_000) { + override def toString = s"timeout: $timeout, linger: $linger" +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + ```scala class Socket(val timeout: Int = 5_000, val linger: Int = 5_000): override def toString = s"timeout: $timeout, linger: $linger" ``` +{% endtab %} +{% endtabs %} + A great thing about this feature is that it lets consumers of your code create classes in a variety of different ways, as though the class had alternate constructors: +{% tabs default-values_2 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +val s = new Socket() // timeout: 5000, linger: 5000 +val s = new Socket(2_500) // timeout: 2500, linger: 5000 +val s = new Socket(10_000, 10_000) // timeout: 10000, linger: 10000 +val s = new Socket(timeout = 10_000) // timeout: 10000, linger: 5000 +val s = new Socket(linger = 10_000) // timeout: 5000, linger: 10000 +``` + +{% endtab %} +{% tab 'Scala 3' %} + ```scala val s = Socket() // timeout: 5000, linger: 5000 val s = Socket(2_500) // timeout: 2500, linger: 5000 @@ -120,9 +224,29 @@ val s = Socket(timeout = 10_000) // timeout: 10000, linger: 5000 val s = Socket(linger = 10_000) // timeout: 5000, linger: 10000 ``` +{% endtab %} +{% endtabs %} + When creating a new instance of a class, you can also use named parameters. This is particularly helpful when many of the parameters have the same type, as shown in this comparison: +{% tabs default-values_3 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +// option 1 +val s = new Socket(10_000, 10_000) + +// option 2 +val s = new Socket( + timeout = 10_000, + linger = 10_000 +) +``` + +{% endtab %} +{% tab 'Scala 3' %} + ```scala // option 1 val s = Socket(10_000, 10_000) @@ -134,6 +258,9 @@ val s = Socket( ) ``` +{% endtab %} +{% endtabs %} + ### Auxiliary constructors You can define a class to have multiple constructors so consumers of your class can build it in different ways. @@ -146,6 +273,48 @@ While analyzing the requirements you’ve seen that you need to be able to const One way to handle this situation in an OOP style is with this code: +{% tabs structor_1 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +import java.time._ + +// [1] the primary constructor +class Student( + var name: String, + var govtId: String +) { + private var _applicationDate: Option[LocalDate] = None + private var _studentId: Int = 0 + + // [2] a constructor for when the student has completed + // their application + def this( + name: String, + govtId: String, + applicationDate: LocalDate + ) = { + this(name, govtId) + _applicationDate = Some(applicationDate) + } + + // [3] a constructor for when the student is approved + // and now has a student id + def this( + name: String, + govtId: String, + studentId: Int + ) = { + this(name, govtId) + _studentId = studentId + } +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + ```scala import java.time.* @@ -178,6 +347,9 @@ class Student( _studentId = studentId ``` +{% endtab %} +{% endtabs %} + {% comment %} // for testing that code override def toString = s""" @@ -196,17 +368,31 @@ The class has three constructors, given by the numbered comments in the code: Those constructors can be called like this: +{% tabs structor_2 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +val s1 = new Student("Mary", "123") +val s2 = new Student("Mary", "123", LocalDate.now) +val s3 = new Student("Mary", "123", 456) +``` + +{% endtab %} + +{% tab 'Scala 3' %} + ```scala val s1 = Student("Mary", "123") val s2 = Student("Mary", "123", LocalDate.now) val s3 = Student("Mary", "123", 456) ``` +{% endtab %} +{% endtabs %} + While this technique can be used, bear in mind that constructor parameters can also have default values, which make it seem that a class has multiple constructors. This is shown in the previous `Socket` example. - - ## Objects An object is a class that has exactly one instance. @@ -216,20 +402,59 @@ Objects in Scala allow grouping methods and fields under one namespace, similar Declaring an `object` is similar to declaring a `class`. Here’s an example of a “string utilities” object that contains a set of methods for working with strings: +{% tabs object_1 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +object StringUtils { + def truncate(s: String, length: Int): String = s.take(length) + def containsWhitespace(s: String): Boolean = s.object_1es(".*\\s.*") + def isNullOrEmpty(s: String): Boolean = s == null || s.trim.isEmpty +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + ```scala object StringUtils: def truncate(s: String, length: Int): String = s.take(length) - def containsWhitespace(s: String): Boolean = s.matches(".*\\s.*") + def containsWhitespace(s: String): Boolean = s.object_1es(".*\\s.*") def isNullOrEmpty(s: String): Boolean = s == null || s.trim.isEmpty ``` +{% endtab %} +{% endtabs %} + We can use the object as follows: + +{% tabs object_2 %} +{% tab 'Scala 2 and 3' %} + ```scala StringUtils.truncate("Chuck Bartowski", 5) // "Chuck" ``` +{% endtab %} +{% endtabs %} + Importing in Scala is very flexible, and allows us to import _all_ members of an object: +{% tabs object_3 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +import StringUtils._ +truncate("Chuck Bartowski", 5) // "Chuck" +containsWhitespace("Sarah Walker") // true +isNullOrEmpty("John Casey") // false +``` + +{% endtab %} + +{% tab 'Scala 3' %} + ```scala import StringUtils.* truncate("Chuck Bartowski", 5) // "Chuck" @@ -237,8 +462,14 @@ containsWhitespace("Sarah Walker") // true isNullOrEmpty("John Casey") // false ``` +{% endtab %} +{% endtabs %} + or just _some_ members: +{% tabs object_4 %} +{% tab 'Scala 2 and 3' %} + ```scala import StringUtils.{truncate, containsWhitespace} truncate("Charles Carmichael", 7) // "Charles" @@ -246,17 +477,37 @@ containsWhitespace("Captain Awesome") // true isNullOrEmpty("Morgan Grimes") // Not found: isNullOrEmpty (error) ``` +{% endtab %} +{% endtabs %} + Objects can also contain fields, which are also accessed like static members: +{% tabs object_5 class=tabs-scala-version %} +{% tab 'Scala 2' %} + ```scala -object MathConstants: +object MathConstants { val PI = 3.14159 val E = 2.71828 +} println(MathConstants.PI) // 3.14159 ``` +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +object MathConstants: + val PI = 3.14159 + val E = 2.71828 + +println(MathConstants.PI) // 3.14159 +``` +{% endtab %} +{% endtabs %} ## Companion objects @@ -267,10 +518,32 @@ A companion class or object can access the private members of its companion. Companion objects are used for methods and values that are not specific to instances of the companion class. For instance, in the following example the class `Circle` has a member named `area` which is specific to each instance, and its companion object has a method named `calculateArea` that’s (a) not specific to an instance, and (b) is available to every instance: +{% tabs companion class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +import scala.math._ + +class Circle(val radius: Double) { + def area: Double = Circle.calculateArea(radius) +} + +object Circle { + private def calculateArea(radius: Double): Double = Pi * pow(radius, 2.0) +} + +val circle1 = new Circle(5.0) +circle1.area +``` + +{% endtab %} + +{% tab 'Scala 3' %} + ```scala import scala.math.* -case class Circle(radius: Double): +class Circle(val radius: Double): def area: Double = Circle.calculateArea(radius) object Circle: @@ -280,6 +553,9 @@ val circle1 = Circle(5.0) circle1.area ``` +{% endtab %} +{% endtabs %} + In this example the `area` method that’s available to each instance uses the `calculateArea` method that’s defined in the companion object. Once again, `calculateArea` is similar to a static method in Java. Also, because `calculateArea` is private, it can’t be accessed by other code, but as shown, it can be seen by instances of the `Circle` class. @@ -296,6 +572,46 @@ Companion objects can be used for several purposes: Here’s a quick look at how `apply` methods can be used as factory methods to create new objects: +{% tabs companion-use class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +class Person { + var name = "" + var age = 0 + override def toString = s"$name is $age years old" +} + +object Person { + // a one-arg factory method + def apply(name: String): Person = { + var p = new Person + p.name = name + p + } + + // a two-arg factory method + def apply(name: String, age: Int): Person = { + var p = new Person + p.name = name + p.age = age + p + } +} + +val joe = Person("Joe") +val fred = Person("Fred", 29) + +//val joe: Person = Joe is 0 years old +//val fred: Person = Fred is 29 years old +``` + +The `unapply` method isn’t covered here, but it’s covered in the [Language Specification](https://scala-lang.org/files/archive/spec/2.13/08-pattern-matching.html#extractor-patterns). + +{% endtab %} + +{% tab 'Scala 3' %} + ```scala class Person: var name = "" @@ -326,9 +642,10 @@ val fred = Person("Fred", 29) //val fred: Person = Fred is 29 years old ``` -The `unapply` method isn’t covered here, but it’s covered in the [Reference documentation][unapply]. - +The `unapply` method isn’t covered here, but it’s covered in the [Reference documentation]({{ site.scala3ref }}/changed-features/pattern-matching.html). +{% endtab %} +{% endtabs %} ## Traits @@ -339,15 +656,49 @@ If you’re familiar with Java, a Scala trait is similar to an interface in Java In a basic use, a trait can be used as an interface, defining only abstract members that will be implemented by other classes: +{% tabs traits_1 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +trait Employee { + def id: Int + def firstName: String + def lastName: String +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + ```scala trait Employee: def id: Int def firstName: String def lastName: String ``` + +{% endtab %} +{% endtabs %} + However, traits can also contain concrete members. For instance, the following trait defines two abstract members---`numLegs` and `walk()`---and also has a concrete implementation of a `stop()` method: +{% tabs traits_2 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +trait HasLegs { + def numLegs: Int + def walk(): Unit + def stop() = println("Stopped walking") +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + ```scala trait HasLegs: def numLegs: Int @@ -355,8 +706,26 @@ trait HasLegs: def stop() = println("Stopped walking") ``` +{% endtab %} +{% endtabs %} + Here’s another trait with an abstract member and two concrete implementations: +{% tabs traits_3 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +trait HasTail { + def tailColor: String + def wagTail() = println("Tail is wagging") + def stopTail() = println("Tail is stopped") +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + ```scala trait HasTail: def tailColor: String @@ -364,11 +733,30 @@ trait HasTail: def stopTail() = println("Tail is stopped") ``` +{% endtab %} +{% endtabs %} + Notice how each trait only handles very specific attributes and behaviors: `HasLegs` deals only with legs, and `HasTail` deals only with tail-related functionality. Traits let you build small modules like this. Later in your code, classes can mix multiple traits to build larger components: +{% tabs traits_4 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +class IrishSetter(name: String) extends HasLegs with HasTail { + val numLegs = 4 + val tailColor = "Red" + def walk() = println("I’m walking") + override def toString = s"$name is a Dog" +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + ```scala class IrishSetter(name: String) extends HasLegs, HasTail: val numLegs = 4 @@ -377,22 +765,37 @@ class IrishSetter(name: String) extends HasLegs, HasTail: override def toString = s"$name is a Dog" ``` +{% endtab %} +{% endtabs %} + Notice that the `IrishSetter` class implements the abstract members that are defined in `HasLegs` and `HasTail`. Now you can create new `IrishSetter` instances: +{% tabs traits_5 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +val d = new IrishSetter("Big Red") // "Big Red is a Dog" +``` + +{% endtab %} +{% tab 'Scala 3' %} + ```scala val d = IrishSetter("Big Red") // "Big Red is a Dog" ``` +{% endtab %} +{% endtabs %} + This is just a taste of what you can accomplish with traits. For more details, see the remainder of these modeling lessons. - - ## Abstract classes {% comment %} LATER: If anyone wants to update this section, our comments about abstract classes and traits are on Slack. The biggest points seem to be: + - The `super` of a trait is dynamic - At the use site, people can mix in traits but not classes - It remains easier to extend a class than a trait from Java, if the trait has at least a field @@ -410,6 +813,27 @@ In most situations you’ll use traits, but historically there have been two sit Prior to Scala 3, when a base class needed to take constructor arguments, you’d declare it as an `abstract class`: +{% tabs abstract_1 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +abstract class Pet(name: String) { + def greeting: String + def age: Int + override def toString = s"My name is $name, I say $greeting, and I’m $age" +} + +class Dog(name: String, var age: Int) extends Pet(name) { + val greeting = "Woof" +} + +val d = new Dog("Fido", 1) +``` + +{% endtab %} + +{% tab 'Scala 3' %} + ```scala abstract class Pet(name: String): def greeting: String @@ -422,8 +846,17 @@ class Dog(name: String, var age: Int) extends Pet(name): val d = Dog("Fido", 1) ``` +{% endtab %} +{% endtabs %} + +