In this post, we will talk about how to create abstract classes, inheritance and also traits.
<Rant>
One thing I wanted to mention is that in Scala the focus is clearly on creating immutable data structures. Which is what YOU should be trying to achieve WHEREVER possible.
This means that you should be using var sparingly, really think about whether you DO need that field to be mutable, I would say ALWAYS favor val if you can, and only use var as a last resort.
In my last post, I did actually show some code that true Scala devs would probably not do, which is to accept var as constructor parameters. This was to illustrate a point about properties, but in reality this should have been using val instead.
</Rant>
Anyway rant over. Let’s carry on with the guts of this post, which to start of will be abstract
classes.
Abstract Classes
In Scala, much the same as other OO languages (looking at you here .NET), you can declare a class as abstract
using the abstract
keyword.
Unlike .NET, you do NOT mark the individual methods as “abstract
”, you only make the class abstract
. A method is abstract
if the class is abstract
and there is no implementation.
Here is an example of an abstract
class, with a simple abstract
method:
abstract class PersonBase(val firstName: String, val lastName: String) {
def ReportsTo() : String
override def toString: String = {
s"firstname: $firstName, lastname: $lastName, reportsTo : $ReportsTo"
}
}
Inheritance
So now that we have an abstract
class, how do we extend this abstract
class. Let's see a couple of examples that extend the abstract
class we specified above.
Here we have a Supervisor
and CEO
class, both of which want to use the PersonBase
class as a super type.
So how do we do that in Scala?
Well it is actually quite simple, let’s see an example of the Supervisor
and CEO
classes.
class Supervisor(override val firstName: String, override val lastName: String, val budget: BigDecimal)
extends PersonBase(firstName, lastName)
{
override def ReportsTo(): String = "MD"
}
class CEO(override val firstName: String, override val lastName: String, val budget: BigDecimal)
extends PersonBase(firstName, lastName)
{
override def ReportsTo(): String = "None"
}
There are a couple of things to note above:
- We use the
extends
keyword to extend the PersonBase
class (which makes PersonBase
the super type of the current type (i.e., Supervisor
/ CEO
)
- We use the
override
keyword a couple of times in the primary constructor for these new Supervisor
/ CEO
classes
- We use the
override
keyword on the method def
to override the super types ReportsTo()
method
In a nut shell, that is the basics of how to inherit from a class and override methods / primary constructor parameters.
Interfaces
In Scala, there are no interfaces, instead there are traits. We will look at those next.
Traits
Traits are like abstract
classes, and are used to define object types by specifying the signature of the supported methods. Traits may be partially implemented, and a type may also inherit from MULTIPLE traits. What multiple inheritance, yep you can do that.
Yikes!
In contrast to classes, traits may not have constructor parameters.
Let's see an example of a simple trait that mimics a 2 input AND logic gate, which has a NAND
method too.
In this simple example, we have an abstract
method “and
” which needs to implemented, but we also have a nand
method which is already implemented. So the inheritor of this trait ONLY needs to supply an implementation for the “and
” method.
trait AndGate {
def and(x1: Boolean, x2: Boolean): Boolean
def nand(x1: Boolean, x2: Boolean): Boolean = !and(x1, x2)
}
class LogicGate() extends AndGate {
def and(x1: Boolean, x2: Boolean): Boolean =
x1 && x2
}
Which we are able to use like this:
object ClassesDemo {
def main(args: Array[String]) =
{
val lg = new LogicGate()
val and1Result = lg.and(true,false)
val nand1Result = lg.nand(true,false)
System.out.print(s"lg.and : $and1Result,lg.nand : $nand1Result \r\n")
val and2Result = lg.and(true,true)
val nand2Result = lg.nand(true,true)
System.out.print(s"lg.and : $and2Result,lg.nand : $nand2Result \r\n")
System.in.read()
()
}
}
Which when run looks like this:
The other interesting thing about traits is that you may have multiple traits, and they may even have the same method. For example, what about this, what do you think happens here:
trait LoggerBase
{
def log(input : String): Unit
}
trait Logger extends LoggerBase {
override def log(input : String): Unit = {
System.out.print(s"Logger :input = '$input'\r\n")
}
}
trait AnotherLogger extends LoggerBase {
override def log(input : String): Unit = {
System.out.print(s"AnotherLogger input = '$input'\r\n")
}
}
class SomeWeirdLogger() extends Logger with AnotherLogger {
}
Do you think we will see this output:
AnotherLogger input = ‘What gives’
or this output:
Logger input = ‘What gives’
Well, we actually get this output:
But why is this?
What would happen if we reversed the order to this:
trait LoggerBase
{
def log(input : String): Unit
}
trait Logger extends LoggerBase {
override def log(input : String): Unit = {
System.out.print(s"Logger :input = '$input'\r\n")
}
}
trait AnotherLogger extends LoggerBase {
override def log(input : String): Unit = {
System.out.print(s"AnotherLogger input = '$input'\r\n")
}
}
class SomeWeirdLogger() extends AnotherLogger with Logger {
}
Where we have swapped the order of the inherited traits.
Now we get this output. Mmm Strange.
Basically traits DO HAVE an order, and the outer most one wins.