With my new book titled Protocol-Oriented programming Programming with Swift being
released in the next few days I thought that I would write a post comparing
protocol-oriented programming to object-oriented programming to highlight the differences.
Numerous tutorials that I have seen take a very Object-Oriented
approach to the protocol-oriented programming (POP) paradigm. By this statement I mean that they tell us
that with POP we should begin our design with the protocol rather than with the
superclass as we did with OOP however the protocol design tends to mirror the
superclass design of OOP. They also tell us that we should use extensions
to add common functionality to types that conform to a protocol as we did with superclasses in OOP. While protocols and protocol extensions are
arguably two of the most important concepts of POP these tutorials seem to be
missing some other very important concepts.
In my new book, Protocol-Oriented programming with Swift, I cover POP in
depth to show the reader how they can use POP, and it’s related technologies,
to reduce the complexity of their code base and to create applications that are
easy to maintain and update.
In this post I would like to compare Protocol-Oriented design
to Object-Oriented design to highlight some of the conceptual differences. To do this we will look at how we would
define animals types for a video game in both an Object-Oriented approach and also a
Protocol-Oriented approach to see the advantages of both. Lets start off by defining the requirements
for our animals.
Requirements
Here are the requirements for our animals:
- We will have three categories of animals:
sea, land and air
- Animals may be a
member of multiple categories. For
example an alligator can be a member of both the land and sea category.
- Animals may be
able to attack and/or move when they are on a tile that matches the categories
they are in.
- Animals will start off with a certain amount of hit
points and if those hit points reach 0 or less then they will die.
- For our example here we will define two animals (Lion
and Alligator) but we know the number of animal types will grow as we develop
the game
Lets start off by looking at how we would design this with
the Object-Oriented approach.
Object-Oriented Design
Before we start writing code, lets create a class diagram
that shows our design. I usually start
off by doing a very basic diagram that simply shows the classes themselves
without much detail. This helps me
picture the class hierarchy in my mind. The
following diagram shows the class hierarchy for our Object-Oriented design:
This diagram shows that we have one superclass named Animal and two subclasses named Alligator and Lion. We may think that, with the three categories,
we would need to create a larger class hierarchy where the middle layer would
contain the classes for the Land, Air and Sea animals however that is not
possible with our requirements. The
reason this is not possible is because animal types can be members of multiple
categories and with a class hierarchy each class can have only one super
class. This means that our Animal super class will need to contain
the code required for each of the three categories. Lets take a look at the code for the Animal super class.
class Animal {
/* Need to be var so override init() */
/* in animal class can set them */
/* Animal types need to be in
the same physical */
/* file to access private
properties */
private var landAnimal = false
private var landAttack = false
private var landMovement = false
private var seaAnimal = false
private var seaAttack = false
private var seaMovement = false
private var airAnimal = false
private var airAttack = false
private var airMovement = false
private var hitPoints = 0
init() {
landAnimal = false
landAttack = false
landMovement = false
airAnimal = false
airAttack = false
airMovement = false
seaAnimal = false
seaAttack = false
seaMovement = false
hitPoints = 0
}
func isLandAnimal() -> Bool { return landAnimal }
func canLandAttack() -> Bool { return landAttack }
func canLandMove() -> Bool { return landMovement }
func isSeaAnimal() -> Bool { return seaAnimal }
func canSeaAttack() -> Bool { return seaAttack }
func canSeaMove() -> Bool { return seaMovement }
func isAirAnimal() -> Bool { return airAnimal }
func canAirAttack() -> Bool { return airAttack }
func canAirMove() -> Bool { return airMovement }
func doLandAttack() {}
func doLandMovement() {}
func doSeaAttack() {}
func doSeaMovement() {}
func doAirAttack() {}
func doAirMovement() {}
func takeHit(amount: Int) { hitPoints -= amount }
func hitPointsRemaining() -> Int { return hitPoints }
func isAlive() -> Bool { return hitPoints > 0 ? true : false }
}
This class starts off with nine Boolean properties that
define the category (land, sea or air) of the animal and if the animal can
attack/move on the land, sea or air. We
defined these as private variables because we need to set them in the subclass
however we do not want external entities to change them. The preference is for these to be constants
however a subclass can not set/change the value of a constant defined in a
superclass. In order for this to work
the subclass needs to be defined in the same physical file as the
superclass. You can see Apple’s page toread about access controls with Swift.
We define the hitPoints
property similar to how we defined the other properties so only the subclasses will
have access to them. We then define a
default initiator that is used to ensure that all of the properties are set to
their default values. Finally we define
eighteen functions that are used to access the properties, move the animal,
attack or other functionality needed for our animals.
We can now create the Alligator and Lion classes that are
subclasses of the Animal class. The code
for these classes is shown next.
class Lion: Animal {
override init() {
super.init()
landAnimal = true
landAttack = true
landMovement = true
hitPoints = 20
}
override func doLandAttack() { print("Lion Attack") }
override func doLandMovement() { print("Lion Move") }
}
class Alligator: Animal {
override init() {
super.init()
landAnimal = true
landAttack = true
landMovement = true
seaAnimal = true
seaAttack = true
seaMovement = true
hitPoints = 35
}
override func doLandAttack() { print("Alligator Land Attack") }
override func doLandMovement() { print("Alligator Land Move") }
override func doSeaAttack() { print("Alligator Sea Attack") }
override func doSeaMovement() { print("Alligator Sea Move") }
}
As we can see in these classes we override the
functionality needed for each animal.
The Lion class contains the
functionality for a land animal and the Alligator
class contains the functionality for both a land and sea animal. Since both class have the same Animal superclass we can use
polymorphism to access them through the interface provided by the Animal superclass. Lets see how we would do this.
var animals = [Animal]()
var an1 = Alligator()
var an2 = Alligator()
var an3 = Lion()
animals.append(an1)
animals.append(an2)
animals.append(an3)
for (index, animal) in animals.enumerate() {
if animal.isAirAnimal() {
print("Animal
at \(index) is Air")
}
if animal.isLandAnimal() {
print("Animal
at \(index) is Land")
}
if animal.isSeaAnimal() {
print("Animal
at \(index) is Sea")
}
}
How we designed the animal types here would definitely
work but there are several drawbacks to this design. The first drawback is the large monolithic Animal superclass. For those that are familiar with designed
characters for video games you probably realize how much functionality is actually
missing form the Animal superclass and
it’s subclasses. This is on purpose so
we can focus on the design and not all of the functionality. For those who are not familiar with designed
characters for video games, trust me when I say that this class will get much bigger when we add all of the functionality needed.
Another drawback is not being able to define constants in
the superclass that the subclasses can set.
We could define an initiator in the superclass that would set all of the
constants however the initiator would become pretty complex and that is
something we would like to avoid. The
builder pattern could help us with the initiation but as we are about to see, a
protocol-oriented design would be even better.
One final drawback that I am going to point out is the use
of flags (landAnimal, seaAnimal and airAnimal
properties) to define the type of animal.
If we accidently set these flags wrong then the animal will not behave
correctly. As an example, if we set the seaAnimal flag rather than the landAnimal flag in the Lion class then
the lion would not be able to move or attack on land. Trust me it is very easy even for the most
experience developer to set flags like these wrong.
Now lets look at how we would define this same
functionality in a Protocol-Oriented way.
Protocol-Oriented Design
Just like with the Object-Oriented design, we will want to
start off with a type diagram that shows the types needed to create and the
relationships between them. The
following diagram shows our Protocol-Oriented design.
As we can see our POP design is different
from our OOP design. In this design we
use three techniques that make POP significantly different from OOP. These techniques are protocol inheritance, protocol
composition and protocol extensions.
Protocol inheritance is where one protocol can inherit the
requirements from one or more other protocols.
In our example the LandAnimal,
SeaAnimal and AirAnimal protocols will inherit the requirements of the Animal
protocol.
Protocol composition allows types to conform to more than
one protocol. This is one of the many advantages
that POP has of OOP. With OOP a class
can have only one superclass which can lead to very monolithic superclasses as
we just saw. With POP we are encouraged
to create multiple smaller protocols with very specific requirements.
Protocol extensions are arguably one of the most important
parts of the protocol-oriented programming paradigm. They allow us to add functionality to all
types that conform to a given protocol.
Without protocol extensions if we had common functionality that was
needed for all types, that conformed to a particular protocol, then we would of
had to add that functionality to each type.
This would lead to large amounts of duplicate code and that would not be
ideal to say the least.
Lets look at how this design works. We will start off by defining our Animal protocol.
protocol Animal {
var hitPoints: Int {get set}
}
In the Animal protocol, the only item that we are defining
is the hitPoints property. This protocol would also contain any additional
items that are common to all animals. To be consistent with our OOP design, we only need to add the hitPoints property to this protocol.
Next we need to add a protocol extension so we can add the
functionality that will be common for all types that conform to the
protocol. Our Animal
protocol extension would contain the following code:
extension Animal {
mutating func takeHit(amount: Int) { hitPoints -= amount }
func hitPointsRemaining() -> Int { return hitPoints }
func isAlive() -> Bool { return hitPoints > 0 ? true : false }
}
The Animal
protocol extension contains the same takeHit(),
hitPointsRemaining() and isAlive() functions that we saw in
the Animals superclass from
the OOP example. Any type that conforms
to the Animal protocol will
automatically receive these three functions.
Now lets define our LandAnimal,
SeaAnimal and AirAnimal
protocols. These protocols will define
the requirements for the land, sea and air animals respectively.
protocol LandAnimal: Animal {
var landAttack: Bool {get}
var landMovement: Bool {get}
func doLandAttack()
func doLandMovement()
}
protocol SeaAnimal: Animal {
var seaAttack: Bool {get}
var seaMovement: Bool {get}
func doSeaAttack()
func doSeaMovement()
}
protocol AirAnimal: Animal {
var airAttack: Bool {get}
var airMovement: Bool {get}
func doAirAttack()
func doAirMovement()
}
Unlike the Animal
superclass in the OOP example, these three protocols only contain the
functionality needed for their particular type of animal. Each of these protocols only contains four
lines of code while the Animal
superclass, as we saw, contains significantly more.
This makes our protocol design much easier to read and manage. The protocol design is also much safer
because the functionality for the various animal types is isolated in their own
protocol rather than being embedded in a giant superclass.
Now lets look at how we would create our Lion and
Alligator types using the protocol-oriented design.
struct Lion: LandAnimal {
var hitPoints = 20
let landAttack = true
let landMovement = true
func doLandAttack() { print("Lion
Attack") }
func doLandMovement() { print("Lion
Move") }
}
struct Alligator: LandAnimal, SeaAnimal {
var hitPoints = 35
let landAttack = true
let landMovement = true
let seaAttack = true
let seaMovement = true
func doLandAttack() { print("Alligator
Land Attack") }
func doLandMovement() { print("Alligator
Land Move") }
func doSeaAttack() { print("Alligator
Sea Attack") }
func doSeaMovement() { print("Alligator
Sea Move") }
}
Notice that we specify that the Lion
type conforms to the LandAnimal
protocol while the Alligator
type conforms to both the LandAnimal
and SeaAnimal protocols. Having a single type that conforms to
multiple protocols is called protocol composition and is what allows us to use smaller protocols rather than one giant monolithic superclass as in
the OOP example.
Both the Lion
and Alligator types originate
from the Animal protocol thereforel we can still use polymorphism as we did in
the OOP example where we use the Animal
type to store instances of the Lion
and Alligator types. Lets see how this works:
var animals = [Animal]()
var an1 = Alligator()
var an2 = Alligator()
var an3 = Lion()
animals.append(an1)
animals.append(an2)
animals.append(an3)
for (index, animal) in animals.enumerate() {
if let animal = animal as? AirAnimal {
print("Animal
at \(index) is Air")
}
if let animal = animal as? LandAnimal {
print("Animal
at \(index) is Land")
}
if let animal = animal as? SeaAnimal {
print("Animal
at \(index) is Sea")
}
}
In this example we create an array that will contain Animal types names animals. We then create two instances of the Alligator type and one instance of the Lion type and add all three instances to
the animals array. Finally we
use a for-in loop to loop through the array and print out the animal type based
on the protocol that the instance conform too.
Conclusion
As we were reading through this post and seeing some of
the advantages that protocol-oriented programming has over object-oriented
programming, we may think that protocol-oriented programming is clearly
superior to object-oriented programming. However, this assumption may not be
totally correct.
Object-oriented programming has been around since the
1970s and is a tried and true programming paradigm. Protocol-oriented
programming is the new kid on the block and was designed to correct some of the
issues with object-oriented programming. I have personally used the
protocol-oriented programming paradigm in a couple of projects and I am very
excited about its possibilities.
Object-oriented programming and protocol-oriented
programming have similar philosophies like creating custom types that model
real-world objects and polymorphism to use a single interface to interact with
multiple types. The difference is how these philosophies are implemented.
To me, the code base in a project that uses
protocol-oriented programming is much safer and easier to read and maintain as
compared to a project that uses object-oriented programming. This does not mean
that I am going to stop using object-oriented programming all together. I can
still see plenty of need for class hierarchy and inheritance.
Remember, when we are designing our application, we should
always use the right tool for the right job. We would not want to use a chain
saw to cut a piece of 2 x 4 lumber, but we also would not want to use a skilsaw
to cut down a tree. Therefore, the winner is the programmer where we have the
choice of using different programming paradigms rather than being limited to
only one.
Protocol-Oriented Programming with Swift
I only scratched the surface of protocol-oriented
programming in this post. There is a lot
more that is covered in my new book titled Protocol-Oriented Programming withSwift. If you would like to learn more
about protocol-oriented programming you can order my book from Amazon and
Packt’s website.
great
ReplyDeleteThis is a very nice article. Thank you for publishing this. I can understand this easily.!!..iOS Swift Online Course
ReplyDeleteperde modelleri
ReplyDeleteSms onay
mobil ödeme bozdurma
nft nasıl alınır
ankara evden eve nakliyat
trafik sigortası
dedektor
web sitesi kurma
Aşk romanları
üsküdar bosch klima servisi
ReplyDeletebeykoz arçelik klima servisi
ataşehir samsung klima servisi
çekmeköy mitsubishi klima servisi
ümraniye beko klima servisi
beykoz lg klima servisi
tuzla daikin klima servisi
kartal vestel klima servisi
pendik arçelik klima servisi
uc satın al
ReplyDeleteminecraft premium
nft nasıl alınır
özel ambulans
en son çıkan perde modelleri
en son çıkan perde modelleri
lisans satın al
yurtdışı kargo