Module 3: Objects and classes
Objects
Objects in Scala are used for singletons and utility methods.
object MySingleton {
var n: Int = 3
def utilityMethod1(): String = ???
}
Other languages have static methods or fields instead.
Classes
Scala supports OOP concepts, like classes:
// Definition
class Person(val firstName: String,
val lastName: String,
var age: Int)
// Usage
val p = new Person("Clark", "Kent", 30)
- Class names should be capitalized
- Primary constructor is in the class signature
Check Classes tour.
Companion Object
An object of the same name than a class in the same source file
class Account
// Companion object
object Account {
}
Case classes
Case classes example
case class Person(firstName: String,
lastName: String,
age: Int)
Advantages over simple class
- Optimized for pattern matching
- Every parameter constructor becomes a
val
. apply
method automatically provided (no need to usenew
)unapply
method automatically provided (pattern matching)- Automatic
toString
,equals
,hashCode
, andcopy
unless explicitly provided.
Simple pattern matching
Programmers and Editors
sealed trait ProgrammingLanguage
case object CSharp extends ProgrammingLanguage
case object JavaScript extends ProgrammingLanguage
case object Python extends ProgrammingLanguage
case object Scala extends ProgrammingLanguage
sealed trait Editor
case object IntelliJ extends Editor
case object Emacs extends Editor
case object VSCode extends Editor
case object VisualStudio extends Editor
case class Programmer(name: String, progLang: ProgrammingLanguage)
One possibility
def chooseEditor(p: Programmer): Editor =
p.progLang match {
case CSharp => VisualStudio
case JavaScript => VSCode
case Python => Emacs
case Scala => IntelliJ
}
Scala OOP support
- Abstraction
- Encapsulation (information hiding)
- Inheritance
- Polymorphism
Abstraction
trait Shape {
def draw(): Unit
}
Traits can encode “interfaces”
Encapsulation
class Person(val firstName: String,
val lastName: String,
var age: Int)
- You get a getter and setter automatically for
age
field. firstName
andlastName
have “getters” automatically.
Subtyping
Called dynamic polymorphism also.
abstract class Person {
def name: String
def age: Int
}
case class Employer(val name: String,
val age: Int,
val taxno: Int)
extends Person
case class Employee(val name: String,
val age: Int,
val salary: Int)
extends Person
Review of some OOP patterns
Gamma et al. 1994
Creational | Structural | Behavioral | Behavioral (cont) |
---|---|---|---|
Factory | Adapter | Interpreter | Memento |
Abstract Factory | Bridge | Generics/Template | Flyweight |
Builder | Composite | Chain of Responsibility | Observer |
Prototype | Decorator | Command | State |
Singleton | Facade | Iterator | Strategy |
Proxy | Mediator | Visitor |
Creational Pattern: Builder
Java example:
import java.util.OptionalInt;
public class NutritionFacts {
private final int servingSize; // (mL) required
private final int servings; // (per container) required
private final OptionalInt calories; // (per serving)
private final OptionalInt fat; // (g/serving)
private final OptionalInt sodium; // (mg/serving)
private final OptionalInt carbohydrate; // (g/serving)
// Code Elided
}
Private constructor:
private NutritionFacts(int servingSize,
int servings,
OptionalInt calories,
OptionalInt fat,
OptionalInt sodium,
OptionalInt carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
Builder pattern for NutritionFacts
:
static public class Builder {
private int servingSize = 0;
private int servings = 0;
private OptionalInt calories = OptionalInt.empty();
private OptionalInt fat = OptionalInt.empty();
private OptionalInt sodium = OptionalInt.empty();
private OptionalInt carbohydrate = OptionalInt.empty();
// Code elided
A method per field:
public Builder servingSize(int servingSize) {
this.servingSize = servingSize;
return this;
}
public Builder servings(int servings) {
this.servings = servings;
return this;
}
public Builder calories(int calories) {
this.calories = OptionalInt.of(calories);
return this;
}
public Builder fat(int fat) {
this.fat = OptionalInt.of(fat);
return this;
}
public Builder sodium(int sodium) {
this.sodium = OptionalInt.of(sodium);
return this;
}
public Builder carbohydrate(int carbohydrate) {
this.carbohydrate = OptionalInt.of(carbohydrate);
return this;
}
build()
method calling NutritionFacts
constructor.
public NutritionFacts build() {
return new NutritionFacts(servingSize,
servings,
calories,
fat,
sodium,
carbohydrate);
}
Not necessary with Scala’s case classes and default parameter values.
case class SNutritionFacts(servingSize: Int,
servings: Int,
calories: Option[Int] = None,
fat: Option[Int] = None,
sodium: Option[Int] = None,
carbohydrate: Option[Int] = None)
C# equivalent:
using LanguageExt;
using static LanguageExt.Prelude;
namespace DesignPatterns
{
public class NutritionFacts
{
public int ServingSize { get; } = 0;
public int Servings { get; } = 0;
public Option<int> Calories { get; } = None;
public Option<int> Fat { get; } = None;
public Option<int> Sodium { get; } = None;
public Option<int> Carbohydrate { get; } = None;
public NutritionFacts(int servingSize,
int servings,
int? calories = null,
int? fat = null,
int? sodium = null,
int? carbohydrate = null) {
ServingSize = servingSize;
Servings = servings;
Calories = Optional(calories);
Fat = Optional(fat);
Sodium = Optional(sodium);
Carbohydrate = Optional(carbohydrate);
}
}
}
FSharp implementation (different solutions may be possible):
type FNutritionFactsRecord =
{ ServingSize: int
Servings: int
Calories: int Option
Fat: int Option
Sodium: int Option
Carbohydrate: int Option
}
let defaultNutritionFacts =
{ ServingSize = 0
Servings = 0
Calories = None
Fat = None
Sodium = None
Carbohydrate = None
}
let cocaCola =
{ defaultNutritionFacts with
ServingSize = 1
Servings = 2
}
Creational Pattern: Prototype
Problem and Solution
- Problem. Need to duplicate an object for some reason, but creating the object by
new
is not appropriate. - Solution. Design an abstract base class that specifies a puret virtual clone method.
- Consequences. Configure an application with classes dynamically. Each subclass must implement
the
clone
method
Exercise
We are going to present a simple problem and its solution using Scala case classes, then compare with other languages implementations you will finish.
Assume the following Point
and Triangle
definitions:
case class Point(x: Double, y: Double)
trait Shape {
def draw(): Unit
}
case class Triangle(p1: Point, p2: Point, p3: Point)
extends Shape {
override def draw(): Unit = ???
}
case class Square(p1: Point, p2: Point, p3: Point, p4: Point)
extends Shape {
override def draw(): Unit = ???
}
(A different encoding with typeclasses is possible)
Because both Point
and Triangle
are immutable case classes, you can simply share the instance
references, and no clone would be necessary.
val t1: Triangle = // assume definition here
val t2: Triangle = t1 // sharing instance reference here
- However, we could use named parameters to modify some of the properties while doing the copy/clone.
- You will have to implement this copy with a modified field in your favorite language.
// The rest of the vertices remain unmodified.
val t3: Triangle = t1.copy(p1 = Point(0.0, 0.0))
Using Java (with Lombok!)
public interface Shape {
void draw();
}
// A point
import lombok.Value;
import lombok.experimental.Wither;
@Value
public class Point {
@Wither float X;
@Wither float Y;
}
Exercise: try implementing this with vanilla Java :)
import lombok.Value;
import lombok.experimental.Wither;
@Value
public class Triangle implements Shape {
@Wither Point p1;
@Wither Point p2;
@Wither Point p3;
@Override
public void draw() {
// Draw this triangle
}
}
Exercise: try implementing this with vanilla Java :)
import lombok.Value;
import lombok.experimental.Wither;
@Value public class Square implements Shape {
@Wither float p1;
@Wither float p2;
@Wither float p3;
@Wither float p4;
@Override public void draw() {
// draw this square
}
}
Implementation 1 (F#)
Complete the exercise with the following starting point.
open System;
module Shape =
type IShape =
abstract member Draw: unit
type Point =
{ x: float; y: float }
type Triangle =
{ p1: Point; p2: Point; p3: Point
} interface IShape with
member x.Draw: unit = raise (new NotImplementedException "Triangle.draw")
Implementation 2 (F#)
Complete the exercise with the following starting point.
open System
open System.Drawing
type Triangle =
{ p1: Point; p2: Point; p3: Point }
type Square =
{ p1: Point; p2: Point; p3: Point; p4: Point }
type Shape =
| STriangle of Triangle
| SSquare of Square
let draw (s: Shape): unit =
match s with
| STriangle t -> raise (new NotImplementedException("draw a Triangle"))
| SSquare s -> raise (new NotImplementedException("draw a Square"))
- Haskell solution using typeclasses.
- Complete the exercise with the following starting point.
{-# LANGUAGE DuplicateRecordFields #-}
data Point = Point { x :: Float, y :: Float }
data Triangle = Triangle
{ p1 :: Point, p2 :: Point, p3 :: Point }
data Square = Square
{ p1 :: Point, p2 :: Point
, p3 :: Point, p4 :: Point }
class Shape a where
draw :: a -> ()
instance Shape Triangle where
draw t = () -- Not implemented
Typeclasses will be reviewed in Module 8.
- Other patterns can be implemented more easily in Scala than other OOP languages.
- Some of them might be innecessary o trivially implemented in Scala (e.g. Singleton).