74. The basics of OO
This chapter will provide a whirlwind tour of the various object oriented programming concepts supported by Groovy.
Classes
Object-oriented programmers use classes to classify objects. As such, a class defines the properties (fields)1 and methods of an object.
In Groovy the class keyword if used when declaring a new class:
class Person {}
Objects consist of properties (data) and methods for interacting with that data. For example, the code below describes a Person class with a name property and a getName method:
class Person {
def name
def getName() {
return this.name
}
}
In order to create an instance of the Person class we use the new keyword and assign the result to a variable
def john = new Person(name: 'John')
println john.getName()
The call to new Person(...) is actually using a special method called a “constructor”. Classes can define a variety of constructors to help with creating an instance and Groovy has some handy tricks we’ll explore later.
Instead of using the def keyword the variable could be declared as being of the Person type:
Person john = new Person(name: 'John')
Instance and Class Elements
In the getName method you’ll notice the keyword this is used. You can translate this to mean “this instance”. Groovy supports instance properties and methods - those operating on a specific instance of the class. This means that your code isolates one instance from another and prevents you from inadvertently altering instances.
Static fields/properties and methods are also supported - these apply at a class level. By preceding a field or a method with the keyword static we indicate that it is relevant to all instances of the class. In the example below I set the specie field to be static and this means I access it at the class level by referring to Person.specie.
static keywordclass Person {
static specie = 'homo sapien'
def name
}
println Person.specie
Constants
An (English) language lawyer might think that the keyword static indicates that the value of specie can’t be changed. However, if you try Person.specie = 'tubulidentata' you’ll see that we can turn Person into a different specie! This is definitely not what we want so we need to declare specie as a constant.
The final keyword precedes static/instance fields and methods to make them constant:
finalclass Person {
static final SPECIE = 'homo sapien'
def name
}
Constructors
Constructors are a special type of method that is called when a new instance is created. We saw that Groovy provides a built-in constructor when we called new Person(name: 'John'). This takes a map in which the keys match the fields in the Person object.
To define a custom constructor we define a method with the same name as the class but without the def preceding it. The example below declares a constructor this way (Person(name)):
class Person {
def name
Person(name) {
this.name = name
}
def getName() {
this.name
}
}
def john = new Person('John')
println john.dump()
Overloading
The overloading feature of Groovy classes allows us to create multiple versions of the same method. The parameter list for each version of the method varies to allow callers to provide either a different number of parameters (as in the example below) or with the same number of parameters but with different types. Overloading is useful but also consider using default values for parameters as this can help reduce the number of methods you need to write and maintain.
mean methodclass Math {
static mean(num1, num2) {
(num1 + num2) / 2
}
static mean(... nums) {
nums.sum() / nums.size()
}
}
println Math.mean(10, 20)
println Math.mean(2, 4, 6, 8)
Interfaces
Interfaces provide a method for defining programming interfaces. Interfaces, for the most part, just define method signatures and not their implementation. Classes implement these interfaces in a manner that reflect the class’s role/model/context.
The example below defines an interface named Exercise with a single method run. The Athlete class then implements the interface:
interface Exercise {
def run(int distance)
}
class Athlete implements Exercise {
def name
def run(int distance) {
println "I'm off for a ${distance}km run."
}
}
def robert = new Athlete(name: 'Rob')
robert.run(10)
Inheritance
A superclass is one from which other classes inherit functionality. The “child” classes are referred to as being subclasses. A subclass inherits from a superclass through the use of the extends keyword.
In the code below, StaffMember is a subclass of Person. This allows StaffMember to access the name field defined in Person:
class Person {
def name
}
class StaffMember extends Person {
def staffID
def getIdentification() {
println "${this.name} - ${this.staffID}"
}
}
def sally = new StaffMember(name: 'Sally', staffID: 765)
sally.identification
Unlike interfaces, superclasses can provide implemented methods and fields that subclasses can utilise. However, Superclasses can work somewhat like interfaces and their methods can be declared as abstract to force subclasses to provide their own implementation.
Overriding methods
Subclasses can also override methods and fields implemented by superclasses. This lets subclasses provide more contextual implementations if needed. A subclass can refer directly to superclass fields and methods by using the super keyword.
In the example below, StaffMember is a subclass of Person. The StaffMember class overrides the getName method and prefixes a string to the name returned by the superclass.
class Person {
def name
def getName() {
this.name
}
}
class StaffMember extends Person {
def staffID
@Override
def getName() {
"Team member ${super.name}"
}
def getIdentification() {
println "${this.name} - ${this.staffID}"
}
}
def sally = new StaffMember(name: 'Sally', staffID: 765)
println sally.name
Traits
At first glance, traits can be considered as interfaces with an implementation but they offer a really useful approach to adding features or abilities to a class. A common example may be to add the flying trait to animals or vehicles. The trait may have its own fields and/or methods.
In the example below:
- A very basic
Projectclass is defined. It just stores the project name - An
Agiletrait provides some basic fields used to describe an agile aspect to projects. A very basic method (startIteration) gives us an example method to call. - A
Scrumclass is defined:- It extends the
Projectclass - It implements the
Agiletrait - Two Scrum-specific fields are added (
productOwner&scrumMaster)
- It extends the
- I can then create an instance of
Scrumand provide it information for both theProject/Scrumhierarchy as well as theAgiletrait.- I then call the
Agiletrait’sstartIterationmethod and our project is away!
- I then call the
class Project {
def name
}
trait Agile {
def iterationLength = 4
def backlog = [ ]
def developmentTeam = [ ]
def startIteration() {
println """\
We're staring our $iterationLength week iteration for $name
Team members: $developmentTeam
Backlog: $backlog
"""
}
}
class Scrum
extends Project
implements Agile {
def productOwner
def scrumMaster
}
def project = new Scrum().with {
name = 'Project X'
iterationLength = 2
productOwner = 'Charlie'
scrumMaster = 'Bobby'
developmentTeam = [ 'Dean', 'Sam' ]
backlog << 'As a User I want to press buttons'
backlog << 'As an Admin I want to lockout users'
//This returns 'it' (the new instance of Scrum)
it
}
project.startIteration()
Packages
As discussed earlier, Groovy allows programmers to group objects into packages. In the following snippet the Person class is allocated to the myobjects package:
package myobjects
class Person {}
Packages are central to using others’ code in your programs and allowing your code to be used elsewhere.
Summary
This chapter has provided an extremely brief overview of object-oriented programming supported by small Groovy examples. The following chapters in this section will explore fundamental aspects of fields and methods and the subsequent section will dive far deeper into how to really get OO with Groovy.
- At this stage I’ll use ‘field’ and ‘property’ interchageably - there is a difference but I’ll discuss this soon.↩