Sunday, February 14, 2016

POP and OOP

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. 




No comments:

Post a Comment