Computer Science Canada Classes Part III - Inheritance and Polymorphism |
Author: | Cervantes [ Tue Jan 10, 2006 10:51 pm ] | ||||||||||||||||||
Post subject: | Classes Part III - Inheritance and Polymorphism | ||||||||||||||||||
Classes Part III:
Inheritance and Polymorphism
At the end of this tutorial, you will find an attached .zip. This file contains pretty much all the code we are using in this tutorial, and it is separated into stages, so that I'm not throwing the final code at you all at once. An Introduction We've been doing a lot of class-writing lately, and you may or may not like it. (Be sure to have read Part I and Part II before reading this.) Regardless, no programmer should like to repeat oneself. If you write a Sword class in an RPG you are making, would you want to essentially rewrite that class to make a Crossbow class? Since both are Weapons, they share a lot of the same properties. It makes sense, then, that we should determine what properties they have in common and extract them into a place where both the Sword class and the Crossbow class can access them. Accomplishing this is one of the primary goals of inheritance. The Fundamentals of Inheritance The basic concept of inheritance is that one class inherits things from another class. Data and methods on that data (the definition of a class/object) are what is inherited. In the example from the introduction, both the Sword class and Bow class would inherit from the Weapon class. The class that inherits another class is called the child class, whereas the class that gives its data and methods to the child classes is called the parent class. For example, the Sword and Bow classes are child classes, while the Weapon class is the parent class. Turing supports single-inheritance, but not multiple-inheritance. That is, a child class in Turing can only have one parent. Note that this does not prevent a parent class from having multiple children classes. Recall from the first tutorial that import and export come at the start of the class. So too does inherit, and inherit comes before both import and export. The Appearance of 'Inherit' It's example time, and we'll build our example from scratch. We will make a Weapon class, then create Sword and Bow classes from it. Stage I - The Weapon Class We're not going to make a very complex Weapon class, because that's not the point of this exercise. The Weapon class will have an initialize method, to set all the data, such as damage, range, weapon name, etc. We'll set this data to generic values (this RPG is starting out small: there is only one type of weapon). We'll also give it a display procedure, to show us information on the Weapon. Here is the Weapon class and a demonstration of its use.
Nothing fancy, here. Stage II - The Sword and Bow Classes Inherit Weapon Next, we create the Sword and Bow classes.
You can place this code immediately after the Weapon class, so our code now looks like this:
We could also give each class its own file, which we will look into later. The Sword and Bow classes have inherited the Weapon class. They have gained access to all the data and methods that the Weapon class had. It's almost as if we copied and pasted the code in the Weapon class into the Sword and Bow classes. However, we avoided the atrocity of copy & paste. Let's see if it actually works. We'll test just the Sword (because the Bow behaves identically, at the moment). Add the following code after the Bow class.
Run it, and it should output the following: Output wrote: Weapon Grasp weapon in chosen hand and use to smite thine enemy. Weight: 5 Cost: 30 Damage: 8 Range: 0 This does not describe a Sword, but we had no reason to expect it to. We did not nothing to the Sword class, other than having it inherit the Weapon class. Thus, the child class did indeed inherit all the data and methods of the parent class; the Sword class did indeed inherit all the data and methods of the Weapon class. However, merely making copies of classes gets us no where. We have to be able to modify the child classes in some way. Polymorphism Polymowhat? Poly means "many". Morph means "transform". Polymorphism means "many transforms"; it refers to the ability to have many forms of the same underlying thing. In this case, the "underlying thing" is a method. Stage III - Modifying the Sword Class We want to give the Sword a better description then "Grasp weapon in chosen hand and use to smite thine enemy." We also want to change the other data about it. To accomplish this, we will change the initialize method. But initialize was defined in the Weapon class. If the Sword class has gained all the data and methods of the Weapon class by inheriting it (which it did), then the Sword class already has an initialize method. And we all know we can't define a method twice. So how do we transform the initialize procedure? The answer comes in the keyword deferred. We are going to defer our procedure, then resolve it in the Sword class. To resolve a method means to give it a new definition in the child classes. We will also resolve the initialize method in the Weapon class, so that the Weapon class still works on its own. The syntax for deferring a procedure is very similar to the syntax for forwarding a procedure or function (used when a method calls another method, which in turn calls the first method: mutual recursion). The only difference is that instead of using the keyword forward, we use the keyword deferred. We resolve the methods by placing the keyword body before the procedure definition, just as if we were using forward to solve problems of mutual recursion.
Output wrote: Sword Swords are metallic, double-bladed weapons that vary in length and size. They can be wielded in one or two hands. Weight: 4 Cost: 15 Damage: 8 Range: 0 Bow Bows are made of flexible wood with a string tied to either end. They are used to smite enemies from afar, projecting arrows with great velocity and long distances. Weight: 1 Cost: 10 Damage: 6 Range: 9 As you can see, calling the initialize method for the Sword or Bow objects calls the new version of it, which we created in the Sword and Bow classes, rather than the one created in the Weapon class. The initialize method of the Weapon class has been transformed into a new method, more suitable to a Sword or Bow. Note that exported methods are automatically deferred (and resolved in the same class, of course. Otherwise we wouldn't be able to call them). We could have just as easily written the same code, except removed the deferred line and removed the body in front of the initialize declaration. I encourage you to defer the procedure, anyways, since it makes the general outline of the class/objects clearer, and since you may decide later that you don't want to export that particular method. Problem I - Parameters of Deferred Methods We cannot change the parameters given to the method we are transforming. Consider the initialize method of the Weapon class. What if we wanted more flexibility? What if we wanted to be able to set the value of name, description and so on by passing in values as parameters? Easy enough:
Now, let's create a Sword class that inherits the Weapon class, and try to redefine the initialize method. All swords are melee weapons, so we won't allow the range to be altered.
You'll notice this code produces one error and one warning. The warning is that the initialize method of the Sword class has the incorrect number of parameters. It has to have the exact same parameters as were defined in the deferred line back in the Weapon class. The error is produced when we call the initialize method: we don't have the correct number of parameters, it tell's us. The only way to get around this is to accept it. You take the same parameters; you just ignore the ones you don't like. In calling the method, you pass whatever you like into those parameters (though they still must be of the correct type, of course). Problem II - Completely Redefining Methods with no Assistance We must fully redefine the method, without the ability to call the version of the method that the parent class has. Say we wanted to change the display method of the sword; instead of outputting 0 for the range, we want it to output "Melee". We have to re-code it entirely, like this:
If this were Ruby, for example, I would use super to call the display method of the parent class (Weapon), which has already done most of the work for me. But to really do this in Ruby, I would have to have the method return an array of data, pop off the last element (the "range" element), and replace it with my own version, "Range: Melee". An Example of Problems I and II Let's look at another example--one that involves the initialize method, and will combine the first and second problems. In discussing Problem I, we wanted to change the initialize method of the Sword class so that range was zero, no matter what. The name, description, weight, cost, and damage are still going to be passed in as parameters, however. Thus, the first five lines of the initialize method are the same in both the Weapon class and the Sword class. They both look like this:
What's the point in repeating this code in both methods? It would be easier to code it once in the Weapon class. If these two problems discussed above didn't exist in Turing, the initialize method of the Sword class would take 5 parameters, and it would call the initialize method of the Weapon class, passing it (name_, description_, weight_, cost_, damage_, 0), and that would be that. Zero is passed as the last parameter to the Weapon's initialize method; range is set to 0; there is no repeat of code. But alas, Turing has no equivalent of Ruby's "super". |
Author: | Cervantes [ Tue Jan 10, 2006 10:54 pm ] | ||||||||||||||||||
Post subject: | |||||||||||||||||||
Expansion to Multiple Files Earlier I mentioned that we could give the Sword and Bow class their own files. The advantage to doing this is merely that it keeps our code tidy, and it prevents us from having to search for the end of a class and the beginning of another. To do this, we have to make each class into a Turing Unit (see step 5 of the modules tutorial). Just put the keyword unit at the start of the class, and ensure you only have one class per file. Save each file as <className>.tu. For example, I would save the Weapon unit as Weapon.tu. Stage IV - Expanding to Multiple Files You should have three .tu files, all in the same folder. You don't have to do anything special to link the files; the inherit Weapon line will do it all for you. (If you haven't looked at the .zip file at the end of this post, this would be the ideal time to do so.) Next, you need a file to run. This will contain the code that creates the objects, initializes them, and displays them. Create a "Run.t" file (in the same folder as your Turing Units) and give it this code:
This is the code we were using previously, except that we had to import the Sword and Bow classes. Note that we did not import Weapon, since we don't directly create any Weapon objects in this code. Also, note that just importing Weapon will not give you access to Sword or Bow. Problem III - Importing Many Units We must import each unit manually. If expanded our Weapon system to include class definitions for 50 types of Weapons, we would need to import 50 units into our "Run.t" program. There is no way to make a package of classes. Object Relationships: "is a" The inherit clause does more than give the child class access to the data and methods of the parent class. Inheritance creates a relationship between child and parent: between Sword and Weapon, and between Bow and Weapon. It creates a parent-child relationship. It creates an "is a" relationship. Sword "is a" Weapon. Bow "is a" Weapon. Let's withdraw for a moment, and say this in even more English style terms: Both Swords and Bows are types of Weapons. Wherever a Weapon can go, a Sword or a Bow can go. Stage V - A Player Character Equips a Weapon Consider the RPG we are building this for. You want to be able to equip a Weapon--to put a Weapon in your hand to beat the Monsters with. You may wish to wield a Sword, engaging in melee combat; or you may wish to wield a Bow, picking the Goblins off from afar. You should be able to equip either a Sword or a Bow, but we don't want to have to code that into the equip_weapon procedure (this procedure may be found in the PlayerCharacter class, which could inherit the Character class, which could inherit the Object class, perhaps). Instead, we use the "is a" relationship. Since a Sword "is a" weapon, a Sword can go anywhere a Weapon can. Thus, in attempting to equip a Weapon to bash Monsters with, we expect to receive a Weapon object. A Sword object is a Weapon object too, so we can easily give the equip_weapon procedure a Sword object, though it expects a Weapon object. Let's code this. We'll make a PlayerCharacter class, and it will have initialize, equip_weapon, and display procedures. The initialize and display procedures will be quite simple. The equip_weapon procedure will take a parameter which is a Weapon object, and store it in the variable, weapon. weapon is a pointer to the Weapon class (make sure you had a good understanding of Classes: Part II for this part). We will initially set the value of the weapon variable to nil: it will initially be a nil pointer, not pointing to any particular class, not referencing any object. The variable type, however, is still pointer to Weapon. As a final note, since we are working with Weapon objects, we have to import the Weapon class into the PlayerCharacter class. Since we are using units, this is as simple as putting import Weapon at the start of the PlayerCharacter class, and making certain that both Turing Units are in the same directory. Here is the PlayerCharacter unit:
We had to make a slight modification to the Weapon class for this to work: we had to export the name variable. The export line of the Weapon class now looks like this:
And finally, the code to create a PlayerCharacter object and equip him with a Weapon. We will create Minsc, the human ranger, and he will wield a Sword.
Now, let's examine this a bit. minsc_sword is a Sword object. A Sword "is a" Weapon. The equip_weapon procedure of the PlayerCharacter class asks for a Weapon object as its parameter. But we gave equip_weapon a Sword object. Since a Sword "is a" Weapon, a Sword can go anywhere a Weapon can. A Sword can be passed to equip_weapon because of this relationship. Problem IV - Anonymous Objects In order to create an object, we must go through the process of making a pointer to the class, then creating a new instance of the class, referenced to the pointer. There is no other way. Thus, to give Minsc a Weapon, we had to create a Sword object, then pass that Sword object to the equip_weapon procedure. But what do I need minsc_sword in my main program for? I don't: The data of minsc_sword's existence is stored within the minsc object. Things would be easier if I could create anonymous objects: objects without a name. If this were Ruby, the code would look roughly like this:
Sword.new creates an anonymous Sword object, an object without a name (I didn't do minsc_sword = Sword.new). This saves us from creating a Sword object in the global namespace and then passing it to equip_weapon. Creating an Inventory - More of "is a" In Part I of this tutorial, we discussed the following scenario: creating a pointer to ClassName, then creating a new instance of ClassName, linked to pointer_name. In code, it looks like this:
Does it seem repetitive? It does, but it gives us flexibility: The class in the first line does not have to match the class in the second line. We'll see why soon. Stage VI - Creating an Inventory An inventory is nothing more than an array of objects that the PlayerCharacter object is holding on to. For our purposes, let's assume Weapons are the only type of Items in our game. (We could expand our Item system somewhat to include an Item class; Weapon, Armour, Scroll, Book, Boots, etc. could inherit Item. However, we don't need to do that to illustrate the current subject.) The inventory is an array of Weapon objects; it is an array of pointer to Weapon.
The inventory should be initialized each element being nil: pointing to nothing; referencing no object. We will give PlayerCharacter add_to_inventory and remove_from_inventory methods, and we'll make display show the contents of the inventory as well. Here's the code:
The "Run.t" code is the following:
There is an important trick here. When creating a Weapon object such as minsc_sword, the pointer points to Weapon, but a new instance of Sword was created, linked to the minsc_sword pointer. To understand this, we have to use the "is a" relationship. Sword "is a" Weapon, so a Sword can go anywhere a Weapon can. This extends even to creating a Sword object linked to a pointer that points to the Weapon class. Why did we have to do this? The answer is that the inventory holds an array of Weapon objects--an array of pointers to Weapon. The data stored in the array must be of the data type, pointer to Weapon. But, we don't want to create Weapon objects all over the place; we want to use our Sword and Bow classes, because they make our life easier. Thus, we create instances of the Sword and Bow classes, with linked to pointers that point to the Weapon class. Stage VII - Expansion of the Item System There are no new concepts in Stage VII, so this section will be short. Essentially, Stage VII expanded the Item system so that a new Item class was the top-dog. Weapon inherits Item, Sword and Bow inherit Weapon. Other types of Items could be added, such as Armour, Boots, Helmets, etc. You can view the code for Stage VII in the .zip file, attached to the end of this post. Questions (but no Answers) To compliment this tutorial, it is recommended that you write a program that involves inheritance and polymorphism. I suggest expanding the example we have built in this tutorial, though I recommend staying away from graphics. If you truly want to make an RPG, make it a text RPG. Begin by planning out all the objects you'll need (Item -> Weapon/Armour, Character -> PlayerCharacter/NonPlayerCharacter, Map -> Terrain, Object -> Door/Chest). Determine object relationships. Plan a Class Hierarchy. A Conclusion Inheritance and polymorphism are powerful programming concepts. They are about reusing code that is common to different objects; it is about creating a logical, object-oriented-sensical structure; it is about creating relationships between objects, including the "is a" relationship. By now, we've had a good look at inheritance and polymorphism. You can see how Turing lacks functionality in this area. This is because Turing is not a strong object-oriented nature; rather, object-orientedness was thrown in as an afterthought, and it was left in a haggard state. There are so many basic aspects of inheritance and polymorphism that Turing gets wrong that it gives a strong push to learn a more object-oriented programming language. It is an interesting thought that the last important part of Turing to learn is also the one to push the programmer away from the Turing language the most. Despite this, inheritance and polymorphism are still powerful programming concepts, even in Turing. Learn to love them! |
Author: | TokenHerbz [ Tue Apr 04, 2006 5:55 pm ] |
Post subject: | |
OMG, Cervantes, your crazy to have so much knowlend in CLASS's i read your 3 class tuts a few times, and still don't know how to use them well :S |
Author: | MysticVegeta [ Tue Apr 04, 2006 5:58 pm ] |
Post subject: | |
Read part 1 a few times, then you will undersatnd whats going on in Part 2 and 3 |
Author: | TokenHerbz [ Wed Apr 05, 2006 2:34 pm ] |
Post subject: | |
Mistic i did, as i said: I read your 3 tuts (class1, class2, class3), a few times (each)... Sorry to confuse you :S |
Author: | MysticVegeta [ Wed Apr 05, 2006 5:18 pm ] |
Post subject: | |
Yes I know thats why I said "Read Part 1 a few times", "then", "read 2 and 3" So read Part 1 a few times before even you glance at part 2, cause I remember reading them a long time ago so I had to read part 2 a few times cause I already knew the content of part 1. |
Author: | Cervantes [ Wed Apr 05, 2006 7:18 pm ] |
Post subject: | |
I wouldn't recommend reading the part I tutorial (read: any tutorial) a few times. By which I mean the following: Read it once, well. Go through it slowly enough that you understand everything. Just reading it, even if you read it ten times, will not help you understand it if you're reading too fast and thinking not enough. This tutorial explains it all, but there is no substitute to practice. Really, make something with classes. Something not too hard, like a Button class. Once you can make a single class and make objects from it, move on to the next part of the tutorial. Make something for part II. An easy thing to make for part II is some class that has a method that accepts as a parameter another object of the same class. For example, a Vector class. It defines a function, add, that takes another Vector object as a parameter, adds the two vectors, then returns a new Vector object that is the resultant of the two given vectors (one vector was passed in as a parameter; one you called the add function on). |
Author: | md [ Wed Apr 05, 2006 7:25 pm ] |
Post subject: | |
Less pointless debate about how inept each of you is, and more praise for Cervantes! Congrats on putting together such a wonderful tutorial on a subject so rarely used (and poorly implemented) in turing. |
Author: | Cervantes [ Wed Apr 05, 2006 7:45 pm ] |
Post subject: | |
Thanks! |
Author: | ericfourfour [ Sat Dec 09, 2006 11:29 pm ] | ||||
Post subject: | |||||
Sorry to necro-post but I found a very useful concept using classes in Turing by using the implement and implement by command. I'm sure you've known about:
in c++. I don't know what it is called but I know it is very useful. Did you know however you could do that in Turing? In Turing you can set it up like this:
There is more and I can expand on it if you want. The only thing you have to watch out for is that Foo can inherit but FooBody cannot. They also must be in different Turing Unit files. |
Author: | wtd [ Sat Dec 09, 2006 11:45 pm ] | ||
Post subject: | |||
ericfourfour wrote: Sorry to necro-post but I found a very useful concept using classes in Turing by using the implement and implement by command.
I'm sure you've known about:
This is simply forward declaration of a class. |
Author: | CodeMonkey2000 [ Wed Dec 13, 2006 5:36 pm ] | ||
Post subject: | |||
theres something wrong, with my long bow class, if i want to specify that the type of arrow you use affects the damage and rang, how would i use polymorphisism for it?
|
Author: | Clayton [ Wed Dec 13, 2006 8:24 pm ] |
Post subject: | |
Try posting in [Turing Help] next time But anyways... You would have to have some sort of pointer in your longbow class that keeps track of the type of ammunition you use in your bow. Then, have some sort of function in the parent class of your bow ammunition have a way to calculate the damage (bonus) that it would have to your bows power. |
Author: | Cervantes [ Thu Dec 14, 2006 12:55 am ] | ||
Post subject: | |||
spearmonkey2000 wrote:
Turing only supports single inheritance, not multi inheritance. So your LongBow can't inherit both from Bow and LongArrow. But then again, why should it inherit from LongArrow? Look at the "is a" relationship. Is a LongBow a LongArrow? Certainly not. I'd recommend making an Ammunition class. From that you can make classes for Arrows, Bolts, ThrowingDaggers, what have you... A Bow object can then have a method called fire that takes in an Arrow object, possibly among other things. You could then create child classes from Arrow, such as FireArrow or PiercingArrow. |
Author: | CodeMonkey2000 [ Thu Dec 14, 2006 6:47 pm ] |
Post subject: | |
so u cant morph 2 parents classes? then wat do u do when u are in that senerio? are you forced to copy and paste? or is there another way around this? |
Author: | Cervantes [ Thu Dec 14, 2006 9:43 pm ] |
Post subject: | |
That's correct. You cannot have a class inherit from two parent classes. In Turing, our objects are asexual. Ruby is similar, in that it only supports single inheritance, but in Ruby we have mixins. Basically, it allows us to import code from modules into a class. modules in Ruby can have module/class variables/methods as well as instance variables/methods, so importing a module into a class makes sense, unlike in Turing, where classes only have instance variables/methods and modules only have module variables/methods. I ask you, do you really need to use multiple inheritance? |