You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This chapter provides an introduction to domain modeling using object-oriented programming (OOP) in Scala 3.
12
-
13
11
12
+
This chapter provides an introduction to domain modeling using object-oriented programming (OOP) in Scala 3.
14
13
15
14
## Introduction
16
15
@@ -24,20 +23,54 @@ Scala provides all the necessary tools for object-oriented design:
24
23
-**Access modifiers** lets you control which members of a class can be accessed by which part of the code.
25
24
26
25
## Traits
26
+
27
27
Perhaps different from other languages with support for OOP, such as Java, the primary tool of decomposition in Scala is not classes, but traits.
28
28
They can serve to describe abstract interfaces like:
29
29
30
+
{% tabs traits_1 class=tabs-scala-version %}
31
+
{% tab 'Scala 2' for=traits_1 %}
32
+
33
+
```scala
34
+
traitShowable {
35
+
defshow:String
36
+
}
37
+
```
38
+
39
+
{% endtab %}
40
+
41
+
{% tab 'Scala 3' for=traits_1 %}
42
+
30
43
```scala
31
44
traitShowable:
32
45
defshow:String
33
46
```
47
+
{% endtab %}
48
+
{% endtabs %}
34
49
35
50
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
+
traitShowable {
57
+
defshow:String
58
+
defshowHtml="<p>"+ show +"</p>"
59
+
}
60
+
```
61
+
62
+
{% endtab %}
63
+
64
+
{% tab 'Scala 3' for=traits_2 %}
65
+
36
66
```scala
37
67
traitShowable:
38
68
defshow:String
39
69
defshowHtml="<p>"+ show +"</p>"
40
70
```
71
+
{% endtab %}
72
+
{% endtabs %}
73
+
41
74
You can see that we define the method `showHtml`_in terms_ of the abstract method `show`.
42
75
43
76
[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
47
80
48
81
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`:
49
82
83
+
{% tabs traits_3 class=tabs-scala-version %}
84
+
{% tab 'Scala 2' for=traits_3 %}
85
+
86
+
```scala
87
+
classDocument(text: String) extendsShowable {
88
+
defshow= text
89
+
}
90
+
```
91
+
92
+
{% endtab %}
93
+
94
+
{% tab 'Scala 3' for=traits_3 %}
95
+
50
96
```scala
51
97
classDocument(text: String) extendsShowable:
52
98
defshow= text
53
99
```
100
+
101
+
{% endtab %}
102
+
{% endtabs %}
103
+
54
104
#### Abstract Members
105
+
55
106
Abstract methods are not the only thing that can be left abstract in a trait.
56
107
A trait can contain:
57
108
@@ -63,9 +114,29 @@ A trait can contain:
63
114
Each of the above features can be used to specify some form of requirement on the implementor of the trait.
64
115
65
116
## Mixin Composition
117
+
66
118
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_.
67
119
68
120
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
+
traitGreetingService {
127
+
deftranslate(text: String):String
128
+
defsayHello= translate("Hello")
129
+
}
130
+
131
+
traitTranslationService {
132
+
deftranslate(text: String):String="..."
133
+
}
134
+
```
135
+
136
+
{% endtab %}
137
+
138
+
{% tab 'Scala 3' for=traits_4 %}
139
+
69
140
```scala
70
141
traitGreetingService:
71
142
deftranslate(text: String):String
@@ -74,14 +145,27 @@ trait GreetingService:
74
145
traitTranslationService:
75
146
deftranslate(text: String):String="..."
76
147
```
148
+
149
+
{% endtab %}
150
+
{% endtabs %}
151
+
77
152
To compose the two services, we can simply create a new trait extending them:
Abstract members in one trait (such as `translate` in `GreetingService`) are automatically matched with concrete members in another trait.
82
165
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.).
83
166
84
167
## Classes
168
+
85
169
Traits are great to modularize components and describe interfaces (required and provided).
86
170
But at some point we’ll want to create instances of them.
87
171
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
98
182
This is even more the case in Scala 3, where traits now can also take parameters, further eliminating the need for classes.
99
183
100
184
#### Defining Classes
185
+
101
186
Like traits, classes can extend multiple traits (but only one super class):
However, since _traits_ are designed as the primary means of decomposition,
127
254
a class that is defined in one file _cannot_ be extended in another file.
128
255
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
+
129
260
```scala
130
261
openclassPerson(name: String)
131
262
```
263
+
{% endtab %}
264
+
{% endtabs %}
265
+
132
266
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.
133
267
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.
134
268
@@ -138,10 +272,27 @@ Unfortunately I can’t find any good links to this on the internet.
138
272
I only mention this because I think that book and phrase is pretty well known in the Java world.
139
273
{% endcomment %}
140
274
141
-
142
-
143
275
## Instances and Private Mutable State
276
+
144
277
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
+
classCounter {
284
+
// can only be observed by the method `count`
285
+
privatevarcurrentCount=0
286
+
287
+
deftick():Unit= currentCount +=1
288
+
defcount:Int= currentCount
289
+
}
290
+
```
291
+
292
+
{% endtab %}
293
+
294
+
{% tab 'Scala 3' for=instance_6 %}
295
+
145
296
```scala
146
297
classCounter:
147
298
// can only be observed by the method `count`
@@ -150,7 +301,15 @@ class Counter:
150
301
deftick():Unit= currentCount +=1
151
302
defcount:Int= currentCount
152
303
```
304
+
305
+
{% endtab %}
306
+
{% endtabs %}
307
+
153
308
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
+
154
313
```scala
155
314
valc1=Counter()
156
315
c1.count // 0
@@ -159,49 +318,87 @@ c1.tick()
159
318
c1.count // 2
160
319
```
161
320
321
+
{% endtab %}
322
+
{% endtabs %}
323
+
162
324
#### Access Modifiers
325
+
163
326
By default, all member definitions in Scala are publicly visible.
164
327
To hide implementation details, it’s possible to define members (methods, fields, types, etc.) to be `private` or `protected`.
165
328
This way you can control how they are accessed or overridden.
166
329
Private members are only visible to the class/trait itself and to its companion object.
167
330
Protected members are also visible to subclasses of the class.
168
331
169
-
170
332
## Advanced Example: Service Oriented Design
333
+
171
334
In the following, we illustrate some advanced features of Scala and show how they can be used to structure larger software components.
172
335
The examples are adapted from the paper ["Scalable Component Abstractions"][scalable] by Martin Odersky and Matthias Zenger.
173
336
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.
174
337
175
338
Our goal is to define a software component with a _family of types_ that can be refined later in implementations of the component.
176
339
Concretely, the following code defines the component `SubjectObserver` as a trait with two abstract type members, `S` (for subjects) and `O` (for observers):
177
340
341
+
{% tabs example_1 class=tabs-scala-version %}
342
+
{% tab 'Scala 2' for=example_1 %}
343
+
178
344
```scala
179
-
traitSubjectObserver:
345
+
traitSubjectObserver {
180
346
181
347
typeS<:Subject
182
348
typeO<:Observer
183
349
184
350
traitSubject { self: S=>
185
351
privatevarobservers:List[O] =List()
186
-
defsubscribe(obs: O):Unit=
352
+
defsubscribe(obs: O):Unit= {
187
353
observers = obs :: observers
188
-
defpublish() =
189
-
for obs <- observers do obs.notify(this)
354
+
}
355
+
defpublish() = {
356
+
for ( obs <- observers ) obs.notify(this)
357
+
}
190
358
}
191
359
192
360
traitObserver {
193
361
defnotify(sub: S):Unit
194
362
}
363
+
}
195
364
```
365
+
366
+
{% endtab %}
367
+
368
+
{% tab 'Scala 3' for=example_1 %}
369
+
370
+
```scala
371
+
traitSubjectObserver:
372
+
373
+
typeS<:Subject
374
+
typeO<:Observer
375
+
376
+
traitSubject:
377
+
self: S=>
378
+
privatevarobservers:List[O] =List()
379
+
defsubscribe(obs: O):Unit=
380
+
observers = obs :: observers
381
+
defpublish() =
382
+
for obs <- observers do obs.notify(this)
383
+
384
+
traitObserver:
385
+
defnotify(sub: S):Unit
386
+
```
387
+
388
+
{% endtab %}
389
+
{% endtabs %}
390
+
196
391
There are a few things that need explanation.
197
392
198
393
#### Abstract Type Members
394
+
199
395
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`.
200
396
However, the type is not completely unknown: we know at least that it is _some subtype_ of the trait `Subject`.
201
397
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`.
202
398
The `<: Subject` part of the declaration is also referred to as an _upper bound on `S`_.
203
399
204
400
#### Nested Traits
401
+
205
402
_Within_ trait `SubjectObserver`, we define two other traits.
206
403
Let us begin with trait `Observer`, which only defines one abstract method `notify` that takes an argument of type `S`.
207
404
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.
211
408
Again, the type of parameter `obs` is `O`, not `Observer`.
212
409
213
410
#### Self-type Annotations
411
+
214
412
Finally, you might have wondered what the `self: S =>` on trait `Subject` is supposed to mean.
215
413
This is called a _self-type annotation_.
216
414
It requires subtypes of `Subject` to also be subtypes of `S`.
217
415
This is necessary to be able to call `obs.notify` with `this` as an argument, since it requires a value of type `S`.
218
416
If `S` was a _concrete_ type, the self-type annotation could be replaced by `trait Subject extends S`.
219
417
220
418
### Implementing the Component
419
+
221
420
We can now implement the above component and define the abstract type members to be concrete types:
Specifically, we define a _singleton_ object `SensorReader` that extends `SubjectObserver`.
240
471
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`.
241
472
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
252
483
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`.
253
484
254
485
### Using the Component
486
+
255
487
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
+
importSensorReader._
494
+
495
+
// setting up a network
496
+
vals1=Sensor("sensor1")
497
+
vals2=Sensor("sensor2")
498
+
vald1=Display()
499
+
vald2=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
+
256
519
```scala
257
520
importSensorReader.*
258
521
@@ -274,18 +537,16 @@ s2.changeValue(3)
274
537
// sensor1 has value 2.0
275
538
// sensor2 has value 3.0
276
539
```
540
+
541
+
{% endtab %}
542
+
{% endtabs %}
543
+
277
544
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.
278
545
279
546
{% comment %}
280
547
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.
0 commit comments