Object Oriented Programming in Swift
Learn how object oriented programming works in Swift by breaking things down into objects that can be inherited and composed from. By Cosmin Pupăză.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Object Oriented Programming in Swift
25 mins
Composition
Now that you have a handy amplifier component, it’s time to use it in an electric guitar. Add the ElectricGuitar
class implementation at the end of the playground right after the Amplifier
class declaration:
// 1
class ElectricGuitar: Guitar {
// 2
let amplifier: Amplifier
// 3
init(brand: String, stringGauge: String = "light", amplifier: Amplifier) {
self.amplifier = amplifier
super.init(brand: brand, stringGauge: stringGauge)
}
// 4
override func tune() -> String {
amplifier.plugIn()
amplifier.volume = 5
return "Tune \(brand) electric with E A D G B E"
}
// 5
override func play(_ music: Music) -> String {
let preparedNotes = super.play(music)
return "Play solo \(preparedNotes) at volume \(amplifier.volume)."
}
}
Taking this step by step:
-
ElectricGuitar
is a concrete type that derives from the abstract, intermediate base classGuitar
. - An electric guitar contains an amplifier. This is a has-a relationship and not an is-a relationship as with inheritance.
- A custom initializer that initializes all of the stored properties and then calls the super class.
- A reasonable
tune()
method. - A reasonable
play()
method.
In a similar vain, add the BassGuitar
class declaration at the bottom of the playground right after the ElectricGuitar
class implementation:
class BassGuitar: Guitar {
let amplifier: Amplifier
init(brand: String, stringGauge: String = "heavy", amplifier: Amplifier) {
self.amplifier = amplifier
super.init(brand: brand, stringGauge: stringGauge)
}
override func tune() -> String {
amplifier.plugIn()
return "Tune \(brand) bass with E A D G"
}
override func play(_ music: Music) -> String {
let preparedNotes = super.play(music)
return "Play bass line \(preparedNotes) at volume \(amplifier.volume)."
}
}
This creates a bass guitar which also utilizes a (has-a) amplifier. Class containment in action. Time for another challenge!
[spoiler title=”Electric Guitar”]Add the following test code at the bottom of the playground right after the BassGuitar
class declaration:
[/spoiler]
[spoiler title=”Electric Guitar”]Add the following test code at the bottom of the playground right after the BassGuitar
class declaration:
let amplifier = Amplifier()
let electricGuitar = ElectricGuitar(brand: "Gibson", stringGauge: "medium", amplifier: amplifier)
electricGuitar.tune()
let bassGuitar = BassGuitar(brand: "Fender", stringGauge: "heavy", amplifier: amplifier)
bassGuitar.tune()
// Notice that because of class reference semantics, the amplifier is a shared
// resource between these two guitars.
bassGuitar.amplifier.volume
electricGuitar.amplifier.volume
bassGuitar.amplifier.unplug()
bassGuitar.amplifier.volume
electricGuitar.amplifier.volume
bassGuitar.amplifier.plugIn()
bassGuitar.amplifier.volume
electricGuitar.amplifier.volume
[/spoiler]
let amplifier = Amplifier()
let electricGuitar = ElectricGuitar(brand: "Gibson", stringGauge: "medium", amplifier: amplifier)
electricGuitar.tune()
let bassGuitar = BassGuitar(brand: "Fender", stringGauge: "heavy", amplifier: amplifier)
bassGuitar.tune()
// Notice that because of class reference semantics, the amplifier is a shared
// resource between these two guitars.
bassGuitar.amplifier.volume
electricGuitar.amplifier.volume
bassGuitar.amplifier.unplug()
bassGuitar.amplifier.volume
electricGuitar.amplifier.volume
bassGuitar.amplifier.plugIn()
bassGuitar.amplifier.volume
electricGuitar.amplifier.volume
Polymorphism
One of the great strengths of object oriented programming is the ability to use different objects through the same interface while each behaves in its own unique way. This is polymorphism meaning “many forms”. Add the Band
class implementation at the end of the playground:
class Band {
let instruments: [Instrument]
init(instruments: [Instrument]) {
self.instruments = instruments
}
func perform(_ music: Music) {
for instrument in instruments {
instrument.perform(music)
}
}
}
The Band
class has an instruments
array stored property which you set in the initializer. The band performs live on stage by going through the instruments
array in a for in
loop and calling the perform(_:)
method for each instrument in the array.
Now go ahead and prepare your first rock concert. Add the following block of code at the bottom of the playground right after the Band
class implementation:
let instruments = [piano, acousticGuitar, electricGuitar, bassGuitar]
let band = Band(instruments: instruments)
band.perform(music)
You first define an instruments
array from the Instrument
class instances you’ve previously created. Then you declare the band
object and configure its instruments
property with the Band
initializer. Finally you use the band
instance’s perform(_:)
method to make the band perform live music (print results of tuning and playing).
Notice that although the instruments
array’s type is [Instrument]
, each instrument performs accordingly depending on its class type. This is how polymorphism works in practice: you now perform in live gigs like a pro! :]
Access Control
You have already seen private
in action as a way to hide complexity and protect your classes from inadvertently getting into invalid states (i.e. breaking the invariant). Swift goes further and provides four levels of access control including:
- private: Visible just within the class.
- fileprivate: Visible from anywhere in the same file.
- internal: Visible from anywhere in the same module or app.
- public: Visible anywhere outside the module.
There are additional access control related keywords:
- open: Not only can it be used anywhere outside the module but also can be subclassed or overridden from outside.
- final: Cannot be overridden or subclassed.
If you don’t specify the access of a class, property or method, it defaults to internal
access. Since you typically only have a single module starting out, this lets you ignore access control concerns at the beginning. You only really need to start worrying about it when your app gets bigger and more complex and you need to think about hiding away some of that complexity.
Making a Framework
Suppose you wanted to make your own music and instrument framework. You can simulate this by adding definitions to the compiled sources of your playground. First, delete the definitions for Music
and Instrument
from the playground. This will cause lots of errors that you will now fix.
Make sure the Project Navigator is visible in Xcode by going to View\Navigators\Show Project Navigator. Then right-click on the Sources folder and select New File from the menu. Rename the file MusicKit.swift and delete everything inside it. Replace the contents with:
// 1
final public class Music {
// 2
public let notes: [String]
public init(notes: [String]) {
self.notes = notes
}
public func prepared() -> String {
return notes.joined(separator: " ")
}
}
// 3
open class Instrument {
public let brand: String
public init(brand: String) {
self.brand = brand
}
// 4
open func tune() -> String {
fatalError("Implement this method for \(brand)")
}
open func play(_ music: Music) -> String {
return music.prepared()
}
// 5
final public func perform(_ music: Music) {
print(tune())
print(play(music))
}
}
Save the file and switch back to the main page of your playground. This will continue to work as before. Here are some notes for what you’ve done here:
-
final public
means that is going to be visible by all outsiders but you cannot subclass it. - Each stored property, initializer, method must be marked
public
if you want to see it from an outside source. - The class
Instrument
is markedopen
because subclassing is allowed. - Methods can also be marked
open
to allow overriding. - Methods can be marked
final
so no one can override them. This can be a useful guarantee.