Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 07be4f7

Browse files
committedOct 7, 2022
add code tabs in num21.
1 parent 69a362b commit 07be4f7

File tree

1 file changed

+276
-15
lines changed

1 file changed

+276
-15
lines changed
 

‎_overviews/scala3-book/domain-modeling-oop.md

Lines changed: 276 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@ previous-page: domain-modeling-tools
88
next-page: domain-modeling-fp
99
---
1010

11-
This chapter provides an introduction to domain modeling using object-oriented programming (OOP) in Scala 3.
12-
1311

12+
This chapter provides an introduction to domain modeling using object-oriented programming (OOP) in Scala 3.
1413

1514
## Introduction
1615

@@ -24,20 +23,54 @@ Scala provides all the necessary tools for object-oriented design:
2423
- **Access modifiers** lets you control which members of a class can be accessed by which part of the code.
2524

2625
## Traits
26+
2727
Perhaps different from other languages with support for OOP, such as Java, the primary tool of decomposition in Scala is not classes, but traits.
2828
They can serve to describe abstract interfaces like:
2929

30+
{% tabs traits_1 class=tabs-scala-version %}
31+
{% tab 'Scala 2' for=traits_1 %}
32+
33+
```scala
34+
trait Showable {
35+
def show: String
36+
}
37+
```
38+
39+
{% endtab %}
40+
41+
{% tab 'Scala 3' for=traits_1 %}
42+
3043
```scala
3144
trait Showable:
3245
def show: String
3346
```
47+
{% endtab %}
48+
{% endtabs %}
3449

3550
and can also contain concrete implementations:
51+
52+
{% tabs traits_2 class=tabs-scala-version %}
53+
{% tab 'Scala 2' for=traits_2 %}
54+
55+
```scala
56+
trait Showable {
57+
def show: String
58+
def showHtml = "<p>" + show + "</p>"
59+
}
60+
```
61+
62+
{% endtab %}
63+
64+
{% tab 'Scala 3' for=traits_2 %}
65+
3666
```scala
3767
trait Showable:
3868
def show: String
3969
def showHtml = "<p>" + show + "</p>"
4070
```
71+
{% endtab %}
72+
{% endtabs %}
73+
4174
You can see that we define the method `showHtml` _in terms_ of the abstract method `show`.
4275

4376
[Odersky and Zenger][scalable] present the _service-oriented component model_ and view:
@@ -47,11 +80,29 @@ You can see that we define the method `showHtml` _in terms_ of the abstract meth
4780

4881
We can already see this with our example of `Showable`: defining a class `Document` that extends `Showable`, we still have to define `show`, but are provided with `showHtml`:
4982

83+
{% tabs traits_3 class=tabs-scala-version %}
84+
{% tab 'Scala 2' for=traits_3 %}
85+
86+
```scala
87+
class Document(text: String) extends Showable {
88+
def show = text
89+
}
90+
```
91+
92+
{% endtab %}
93+
94+
{% tab 'Scala 3' for=traits_3 %}
95+
5096
```scala
5197
class Document(text: String) extends Showable:
5298
def show = text
5399
```
100+
101+
{% endtab %}
102+
{% endtabs %}
103+
54104
#### Abstract Members
105+
55106
Abstract methods are not the only thing that can be left abstract in a trait.
56107
A trait can contain:
57108

@@ -63,9 +114,29 @@ A trait can contain:
63114
Each of the above features can be used to specify some form of requirement on the implementor of the trait.
64115

65116
## Mixin Composition
117+
66118
Not only can traits contain abstract and concrete definitions, Scala also provides a powerful way to compose multiple traits: a feature which is often referred to as _mixin composition_.
67119

68120
Let us assume the following two (potentially independently defined) traits:
121+
122+
{% tabs traits_4 class=tabs-scala-version %}
123+
{% tab 'Scala 2' for=traits_4 %}
124+
125+
```scala
126+
trait GreetingService {
127+
def translate(text: String): String
128+
def sayHello = translate("Hello")
129+
}
130+
131+
trait TranslationService {
132+
def translate(text: String): String = "..."
133+
}
134+
```
135+
136+
{% endtab %}
137+
138+
{% tab 'Scala 3' for=traits_4 %}
139+
69140
```scala
70141
trait GreetingService:
71142
def translate(text: String): String
@@ -74,14 +145,27 @@ trait GreetingService:
74145
trait TranslationService:
75146
def translate(text: String): String = "..."
76147
```
148+
149+
{% endtab %}
150+
{% endtabs %}
151+
77152
To compose the two services, we can simply create a new trait extending them:
153+
154+
{% tabs traits_5 class=tabs-scala-version %}
155+
{% tab 'Scala 2 and 3' for=traits_5 %}
156+
78157
```scala
79158
trait ComposedService extends GreetingService, TranslationService
80159
```
160+
161+
{% endtab %}
162+
{% endtabs %}
163+
81164
Abstract members in one trait (such as `translate` in `GreetingService`) are automatically matched with concrete members in another trait.
82165
This not only works with methods as in this example, but also with all the other abstract members mentioned above (that is, types, value definitions, etc.).
83166

84167
## Classes
168+
85169
Traits are great to modularize components and describe interfaces (required and provided).
86170
But at some point we’ll want to create instances of them.
87171
When designing software in Scala, it’s often helpful to only consider using classes at the leafs of your inheritance model:
@@ -98,37 +182,87 @@ NOTE: I think “leaves” may technically be the correct word to use, but I pre
98182
This is even more the case in Scala 3, where traits now can also take parameters, further eliminating the need for classes.
99183

100184
#### Defining Classes
185+
101186
Like traits, classes can extend multiple traits (but only one super class):
187+
188+
{% tabs class_1 class=tabs-scala-version %}
189+
{% tab 'Scala 2' for=class_1 %}
190+
191+
```scala
192+
class MyService(name: String) extends ComposedService, Showable {
193+
def show = s"$name says $sayHello"
194+
}
195+
```
196+
197+
{% endtab %}
198+
199+
{% tab 'Scala 3' for=class_1 %}
200+
102201
```scala
103202
class MyService(name: String) extends ComposedService, Showable:
104203
def show = s"$name says $sayHello"
105204
```
205+
206+
{% endtab %}
207+
{% endtabs %}
208+
106209
#### Subtyping
210+
107211
We can create an instance of `MyService` as follows:
212+
213+
{% tabs class_2 class=tabs-scala-version %}
214+
{% tab 'Scala 2 and 3' for=class_2 %}
215+
108216
```scala
109217
val s1: MyService = MyService("Service 1")
110218
```
219+
220+
{% endtab %}
221+
{% endtabs %}
222+
111223
Through the means of subtyping, our instance `s1` can be used everywhere that any of the extended traits is expected:
224+
225+
{% tabs class_3 class=tabs-scala-version %}
226+
{% tab 'Scala 2 and 3' for=class_3 %}
227+
112228
```scala
113229
val s2: GreetingService = s1
114230
val s3: TranslationService = s1
115231
val s4: Showable = s1
116232
// ... and so on ...
117233
```
234+
{% endtab %}
235+
{% endtabs %}
118236

119237
#### Planning for Extension
238+
120239
As mentioned before, it is possible to extend another class:
240+
241+
{% tabs class_4 class=tabs-scala-version %}
242+
{% tab 'Scala 2 and 3' for=class_4 %}
243+
121244
```scala
122245
class Person(name: String)
123246
class SoftwareDeveloper(name: String, favoriteLang: String)
124247
extends Person(name)
125248
```
249+
250+
{% endtab %}
251+
{% endtabs %}
252+
126253
However, since _traits_ are designed as the primary means of decomposition,
127254
a class that is defined in one file _cannot_ be extended in another file.
128255
In order to allow this, the base class needs to be marked as `open`:
256+
257+
{% tabs class_5 class=tabs-scala-version %}
258+
{% tab 'Scala 2 and 3' for=class_5 %}
259+
129260
```scala
130261
open class Person(name: String)
131262
```
263+
{% endtab %}
264+
{% endtabs %}
265+
132266
Marking classes with [`open`][open] is a new feature of Scala 3. Having to explicitly mark classes as open avoids many common pitfalls in OO design.
133267
In particular, it requires library designers to explicitly plan for extension and for instance document the classes that are marked as open with additional extension contracts.
134268

@@ -138,10 +272,27 @@ Unfortunately I can’t find any good links to this on the internet.
138272
I only mention this because I think that book and phrase is pretty well known in the Java world.
139273
{% endcomment %}
140274

141-
142-
143275
## Instances and Private Mutable State
276+
144277
Like in other languages with support for OOP, traits and classes in Scala can define mutable fields:
278+
279+
{% tabs instance_6 class=tabs-scala-version %}
280+
{% tab 'Scala 2' for=instance_6 %}
281+
282+
```scala
283+
class Counter {
284+
// can only be observed by the method `count`
285+
private var currentCount = 0
286+
287+
def tick(): Unit = currentCount += 1
288+
def count: Int = currentCount
289+
}
290+
```
291+
292+
{% endtab %}
293+
294+
{% tab 'Scala 3' for=instance_6 %}
295+
145296
```scala
146297
class Counter:
147298
// can only be observed by the method `count`
@@ -150,7 +301,15 @@ class Counter:
150301
def tick(): Unit = currentCount += 1
151302
def count: Int = currentCount
152303
```
304+
305+
{% endtab %}
306+
{% endtabs %}
307+
153308
Every instance of the class `Counter` has its own private state that can only be observed through the method `count`, as the following interaction illustrates:
309+
310+
{% tabs instance_7 class=tabs-scala-version %}
311+
{% tab 'Scala 2 and 3' for=instance_7 %}
312+
154313
```scala
155314
val c1 = Counter()
156315
c1.count // 0
@@ -159,49 +318,87 @@ c1.tick()
159318
c1.count // 2
160319
```
161320

321+
{% endtab %}
322+
{% endtabs %}
323+
162324
#### Access Modifiers
325+
163326
By default, all member definitions in Scala are publicly visible.
164327
To hide implementation details, it’s possible to define members (methods, fields, types, etc.) to be `private` or `protected`.
165328
This way you can control how they are accessed or overridden.
166329
Private members are only visible to the class/trait itself and to its companion object.
167330
Protected members are also visible to subclasses of the class.
168331

169-
170332
## Advanced Example: Service Oriented Design
333+
171334
In the following, we illustrate some advanced features of Scala and show how they can be used to structure larger software components.
172335
The examples are adapted from the paper ["Scalable Component Abstractions"][scalable] by Martin Odersky and Matthias Zenger.
173336
Don’t worry if you don’t understand all the details of the example; it’s primarily intended to demonstrate how to use several type features to construct larger components.
174337

175338
Our goal is to define a software component with a _family of types_ that can be refined later in implementations of the component.
176339
Concretely, the following code defines the component `SubjectObserver` as a trait with two abstract type members, `S` (for subjects) and `O` (for observers):
177340

341+
{% tabs example_1 class=tabs-scala-version %}
342+
{% tab 'Scala 2' for=example_1 %}
343+
178344
```scala
179-
trait SubjectObserver:
345+
trait SubjectObserver {
180346

181347
type S <: Subject
182348
type O <: Observer
183349

184350
trait Subject { self: S =>
185351
private var observers: List[O] = List()
186-
def subscribe(obs: O): Unit =
352+
def subscribe(obs: O): Unit = {
187353
observers = obs :: observers
188-
def publish() =
189-
for obs <- observers do obs.notify(this)
354+
}
355+
def publish() = {
356+
for ( obs <- observers ) obs.notify(this)
357+
}
190358
}
191359

192360
trait Observer {
193361
def notify(sub: S): Unit
194362
}
363+
}
195364
```
365+
366+
{% endtab %}
367+
368+
{% tab 'Scala 3' for=example_1 %}
369+
370+
```scala
371+
trait SubjectObserver:
372+
373+
type S <: Subject
374+
type O <: Observer
375+
376+
trait Subject:
377+
self: S =>
378+
private var observers: List[O] = List()
379+
def subscribe(obs: O): Unit =
380+
observers = obs :: observers
381+
def publish() =
382+
for obs <- observers do obs.notify(this)
383+
384+
trait Observer:
385+
def notify(sub: S): Unit
386+
```
387+
388+
{% endtab %}
389+
{% endtabs %}
390+
196391
There are a few things that need explanation.
197392

198393
#### Abstract Type Members
394+
199395
The declaration `type S <: Subject` says that within the trait `SubjectObserver` we can refer to some _unknown_ (that is, abstract) type that we call `S`.
200396
However, the type is not completely unknown: we know at least that it is _some subtype_ of the trait `Subject`.
201397
All traits and classes extending `SubjectObserver` are free to choose any type for `S` as long as the chosen type is a subtype of `Subject`.
202398
The `<: Subject` part of the declaration is also referred to as an _upper bound on `S`_.
203399

204400
#### Nested Traits
401+
205402
_Within_ trait `SubjectObserver`, we define two other traits.
206403
Let us begin with trait `Observer`, which only defines one abstract method `notify` that takes an argument of type `S`.
207404
As we will see momentarily, it is important that the argument has type `S` and not type `Subject`.
@@ -211,15 +408,45 @@ Subscribing to a subject simply stores the object into this list.
211408
Again, the type of parameter `obs` is `O`, not `Observer`.
212409

213410
#### Self-type Annotations
411+
214412
Finally, you might have wondered what the `self: S =>` on trait `Subject` is supposed to mean.
215413
This is called a _self-type annotation_.
216414
It requires subtypes of `Subject` to also be subtypes of `S`.
217415
This is necessary to be able to call `obs.notify` with `this` as an argument, since it requires a value of type `S`.
218416
If `S` was a _concrete_ type, the self-type annotation could be replaced by `trait Subject extends S`.
219417

220418
### Implementing the Component
419+
221420
We can now implement the above component and define the abstract type members to be concrete types:
222421

422+
{% tabs example_2 class=tabs-scala-version %}
423+
{% tab 'Scala 2' for=example_2 %}
424+
425+
```scala
426+
object SensorReader extends SubjectObserver {
427+
type S = Sensor
428+
type O = Display
429+
430+
class Sensor(val label: String) extends Subject {
431+
private var currentValue = 0.0
432+
def value = currentValue
433+
def changeValue(v: Double) = {
434+
currentValue = v
435+
publish()
436+
}
437+
}
438+
439+
class Display extends Observer {
440+
def notify(sub: Sensor) =
441+
println(s"${sub.label} has value ${sub.value}")
442+
}
443+
}
444+
```
445+
446+
{% endtab %}
447+
448+
{% tab 'Scala 3' for=example_2 %}
449+
223450
```scala
224451
object SensorReader extends SubjectObserver:
225452
type S = Sensor
@@ -236,6 +463,10 @@ object SensorReader extends SubjectObserver:
236463
def notify(sub: Sensor) =
237464
println(s"${sub.label} has value ${sub.value}")
238465
```
466+
467+
{% endtab %}
468+
{% endtabs %}
469+
239470
Specifically, we define a _singleton_ object `SensorReader` that extends `SubjectObserver`.
240471
In the implementation of `SensorReader`, we say that type `S` is now defined as type `Sensor`, and type `O` is defined to be equal to type `Display`.
241472
Both `Sensor` and `Display` are defined as nested classes within `SensorReader`, implementing the traits `Subject` and `Observer`, correspondingly.
@@ -252,7 +483,39 @@ NOTE: You might say “the abstract method `notify`” in that last sentence, bu
252483
It is important to point out that the implementation of `notify` can only safely access the label and value of `sub`, since we originally declared the parameter to be of type `S`.
253484

254485
### Using the Component
486+
255487
Finally, the following code illustrates how to use our `SensorReader` component:
488+
489+
{% tabs example_3 class=tabs-scala-version %}
490+
{% tab 'Scala 2' for=example_3 %}
491+
492+
```scala
493+
import SensorReader._
494+
495+
// setting up a network
496+
val s1 = Sensor("sensor1")
497+
val s2 = Sensor("sensor2")
498+
val d1 = Display()
499+
val d2 = Display()
500+
s1.subscribe(d1)
501+
s1.subscribe(d2)
502+
s2.subscribe(d1)
503+
504+
// propagating updates through the network
505+
s1.changeValue(2)
506+
s2.changeValue(3)
507+
508+
// prints:
509+
// sensor1 has value 2.0
510+
// sensor1 has value 2.0
511+
// sensor2 has value 3.0
512+
513+
```
514+
515+
{% endtab %}
516+
517+
{% tab 'Scala 3' for=example_3 %}
518+
256519
```scala
257520
import SensorReader.*
258521

@@ -274,18 +537,16 @@ s2.changeValue(3)
274537
// sensor1 has value 2.0
275538
// sensor2 has value 3.0
276539
```
540+
541+
{% endtab %}
542+
{% endtabs %}
543+
277544
With all the object-oriented programming utilities under our belt, in the next section we will demonstrate how to design programs in a functional style.
278545

279546
{% comment %}
280547
NOTE: One thing I occasionally do is flip things like this around, so I first show how to use a component, and then show how to implement that component. I don’t have a rule of thumb about when to do this, but sometimes it’s motivational to see the use first, and then see how to create the code to make that work.
281548
{% endcomment %}
282549

283-
284-
285550
[scalable]: https://doi.org/10.1145/1094811.1094815
286551
[open]: {{ site.scala3ref }}/other-new-features/open-classes.html
287552
[trait-params]: {{ site.scala3ref }}/other-new-features/trait-parameters.html
288-
289-
290-
291-

0 commit comments

Comments
 (0)
Please sign in to comment.