Object-oriented design

Before we start writing code, let's create a very basic diagram that shows how we would design the Animal class hierarchy. In this diagram, we will simply show the classes without much detail. This diagram will help us picture the class hierarchy in our mind. The following diagram shows the class hierarchy for the object-oriented design:

This diagram shows that we have one superclass named Animal, and two subclasses named Alligator and Lion. We may think with the three categories (land, air, and sea) that we would want to create a larger class hierarchy where the middle layer would contain the classes for the land, air, and sea animals. This would allow us to separate the code for each animal category; however, that is not possible with our requirements. The reason this is not possible is that any of the animal types can be members of multiple categories, and with a class hierarchy, each class can have one and only one superclass. This means that our Animal superclass will need to contain the code required for each of the three categories. Let's begin by looking at the code for the Animal superclass.

We will start the Animal superclass by defining 10 properties. These properties will define what type of animal it is and what type of attacks/movements it can do. We also define a property that will keep track of the hit points for the animal.

We defined these properties as fileprivate variables. We will need to set these properties in the subclasses that we defined in the same source file; however, we do not want external entities to change them. The preference is for these to be constants, but with an object-oriented approach; a subclass cannot set/change the value of a constant defined in a superclass. For this to work, the subclass will need to be defined in the same physical file as the superclass. You can read about fileprivate access control within the proposal at https://github.com/apple/swift-evolution/blob/master/proposals/25-scoped-access-level.md:

class Animal { 
  fileprivate var landAnimal = false 
  fileprivate var landAttack = false 
  fileprivate var landMovement = false 
 
  fileprivate var seaAnimal = false 
  fileprivate var seaAttack = false 
  fileprivate var seaMovement = false 
 
  fileprivate var airAnimal = false 
  fileprivate var airAttack = false 
  fileprivate var airMovement = false 
 
  fileprivate var hitPoints = 0 
} 

Next, we will define an initializer that will set the properties. We will set all the properties to false by default, and the hit points to zero. It will be up to the subclasses to set the appropriate properties that apply:

init() { 
  landAnimal = false 
  landAttack = false 
  landMovement = false 
  airAnimal = false 
  airAttack = false 
  airMovement = false 
  seaAnimal = false 
  seaAttack = false 
  seaMovement = false 
  hitPoints = 0 
} 

Since our properties are fileprivate, we need to create some getter methods so that we can retrieve their values. We will also create a couple of additional methods that will see if the animal is alive. We will need another method that will deduct hit points when the animal takes a hit:

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 }

One big disadvantage of this design, as noted previously, is that all the subclasses need to be in the same physical file as the Animal superclass. Considering how large the animal classes can be once we get in all of the game logic, we probably do not want all of these types in the same file. To avoid this, we could set the properties to internal or public, but that would not prevent the values from being changed by instances of other types. This is a major drawback of our object-oriented design.

Now that we have our Animal superclass, we can create the Alligator and Lion classes, which will be subclasses of the Animal class:

 

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, these classes set 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 land and sea animals.

Another disadvantage of this object-oriented design is that we do not have a single point that defines what type of animal (air, land, or sea) this is. It is very easy to set the wrong flag or add the wrong function when we cut and paste, or type in the code. This may lead us to have an animal like this:

 

class landAnimal: Animal { 
  override init() { 
    super.init() 
    landAnimal = true 
    airAttack = true 
    landMovement = true 
    hitPoints = 20 
  } 
  override func doLandAttack() { 
    print("Lion Attack") 
  } 
  override func doLandMovement() { 
    print("Lion Move") 
  } 
} 

In the previous code, we set the landAnimal property to true; however, we accidentally set the airAttack to true as well. This will give us an animal that can move on land, but cannot attack, since the landAttack property is not set. Hopefully, we would catch these types of errors in testing; however, as we will see later in this chapter, a protocol-oriented approach would help prevent coding errors like this.

Since both classes have the same Animal superclass, we can use polymorphism to access them through the interface provided by the Animal superclass:

var animals = [Animal]() 
 
animals.append(Alligator())  
animals.append(Alligator()) 
animals.append(Lion()) 
 
for (index, animal) in animals.enumerated() {  
  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") 
  }
}

The way we designed the animal types here would work; however, there are several drawbacks in this design. The first drawback is the large monolithic Animal superclass. Those who are familiar with designing characters for video games probably realize how much functionality is missing from this superclass and its subclasses. This is on purpose, so that we can focus on the design and not the functionality. For those who are not familiar with designing characters for video games, trust me when I say that this class may get very large.

Another drawback is not being able to define constants in the superclass that the subclasses can set. We could define various initializers for the superclass that would correctly set the constants for the different animal categories; however, these initializers will become pretty complex and hard to maintain as we add more animals. The builder pattern could help us with the initialization, 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, and the type of attack and movements an animal can perform. If we do not correctly set these flags, 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 will not be able to move or attack on land. Trust me, it is very easy, even for the most experienced developers, to set flags wrongly.

Now let's look at how we would define this same functionality in a protocol-oriented way.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset