07 Mar, 2010, Kayle wrote in the 1st comment:
Votes: 0
As many of you no doubt realized by now, my wife and I have been working on a Star Wars MUD, and we'v– I've been writing our Space and Force systems from the ground up. And I'd decided that this was as good a time as any to teach myself the concepts of inheritance and polymorphism, and really start to play with OO design. But after looking it over, I can't help but wonder if I'm maybe over complicating an already complex beast such as Space, by trying to work with something I'm not entirely familiar with.

So I decided tonight, since I got the first stage to compile and run in the MUD for the first time that before I went any further, I'd step back and evaluate and see if this path was truely worthwhile. First off, SW:TSW is being built off of SmaugFUSS, so I'd only be gaining OO Designs in the Space and Force systems without a tremendous amount of work retrofitting SmaugFUSS to be OO itself. Secondly, I'm just learning these concepts so I'm bound to do something wrong, and for something as integral to the theme as Space and Force, I'd really prefer to avoid that. So now on to the meat…

This is the base Ship class, minus the get/set functions, and other public functions and data members.
class Ship 
{
private:
Ship( const Ship & );
Ship &operator = ( const Ship & );

public:
Ship( );
~Ship( );

private:
Room *m_FirstRoom; //First Room the ship takes up.
Room *m_LastRoom; //Last Room the ship takes up.
Room *m_InRoom; //Room Ship is located in.
const char *m_Name; //Name of the ship.
const char *m_Filename; //Filename of the ship.
const char *m_Manufacturer; //Who manufactures it?
const char *m_Shipyard; //Where was it built?
const char *m_Owner; //Who actually owns the ship?
const char *m_Pilot; //Who's the Pilot?
const char *m_CoPilot; //Who's the Co-Pilot?
short m_ShipType; //Mob, Player, Custom.
short m_ShipClass; //Class of ship.
short m_HyperDriveClass; //Hyperdrive class.
short m_ShieldType; //Type of Shields
short m_MaxSpeed; //Maximum Sub-light speed.
short m_CurrSpeed; //Current Speed.
short m_TargetSpeed; //Speed ships trying to reach.
short m_Maneuverability; //How Manueverable is the ship?
short m_ShipState; //What's the ship doing?
int m_MaxEnergy; //Maximum Energy Levels
int m_MaxShields; //Maximum Shield Strength
int m_MaxHull; //Maximum Hull Strength
int m_Cost; //Cost of ship < Billion.
int m_BillCost; //Cost of ship > Billion.

public:
<…snip…>
};



Now the derived classes vary in type based on the Class/Type of ship. A couple examples:
class Fighter : public Ship
{
private:
Fighter( const Fighter & );
Fighter &operator = ( const Fighter & );

public:
Fighter( );
virtual ~Fighter( );

private:
int m_Cockpit; //Vnum of the cockpit of the ship.

public:
//Get Functions
int getCockpit( ) { return m_Cockpit; }

//Set Functions
void setCockpit( int vnum )
{
if( vnum <= getLastRoom( )->vnum && vnum >= getFirstRoom( )->vnum )
m_Cockpit = vnum;
else
{
bug( "%s: %d outside ship room range. Defaulting to First Room.", __FUNCTION__, vnum );
m_Cockpit = getFirstRoom( )->vnum;
}
}

//Messages
void echoTo( const char *fmt, … ); //Send message to the ship.
void systemEcho( const char *fmt, … ); //Send message to outside ship.
};

class Freighter : public Ship
{
private:
Freighter( const Freighter & );
Freighter &operator = ( const Freighter & );

public:
Freighter( );
virtual ~Freighter( );

private:
int m_Cockpit; //Vnum of the cockpit of the ship.
int m_EntryRamp; //Vnum of the entry ramp of the ship.
int m_EngineRoom; //Vnum of the engine room of the ship.

public:
//Messages
void echoTo( const char *fmt, … ); //Send message to the ship.
void echoToControl( const char *fmt, … ); //Send Message to Control areas.
void systemEcho( const char *fmt, … ); //Send message to outside ship.
};

class Destroyer : public Ship
{
private:
Destroyer( const Destroyer & );
Destroyer &operator = ( const Destroyer & );

public:
Destroyer( );
virtual ~Destroyer( );

private:
int m_EntryRamp; //Vnum of the entry ramp of the ship.
int m_EngineRoom; //Vnum of the engine room of the ship.
int m_NavSeat; //Vnum of the navigator's station of the ship.
int m_PilotSeat; //Vnum of the Pilot's station of the ship.
int m_CoPilotSeat; //Vnum of the Co-Pilot's station of the ship.
int m_GunnerSeat; //Vnum of the Gunner's station of the ship.
int m_CaptainSeat; //Vnum of the Captain's station of the ship.
int m_Hangar; //Vnum of the Hangar of the ship.

public:
//Messages
void echoTo( const char *fmt, … ); //Send message to the ship.
void echoToControl( const char *fmt, … ); //Send Message to Control areas.
void echoToPilot( const char *fmt, … ); //Send Message to Pilot/Co-Pilot.
void systemEcho( const char *fmt, … ); //Send message to outside ship.
};

class Station : public Ship
{
private:
Station( const Station & );
Station &operator = ( const Station & );

public:
Station( );
virtual ~Station( );

private:
int m_Hangar; //Vnum of the Hangar of the ship.

public:
//Messages
void echoTo( const char *fmt, … ); //Send message to the ship.
void systemEcho( const char *fmt, … ); //Send message to outside ship.
};


There's a ton more of them, and they're no where near complete but this is just a couple of examples. If anyone wants to see the full classes, I'd be happy to show them to you in a PM or a pastebin, but I don't want to post them all because it's about 640 lines just for the classes themselves, not including the meat of the member functions in the .cpp file.

So I guess, based on what I've shown, am I over complicating things? Going a good route? Have I done anything wrong here? Is this a waste of time to have parts of the codebase OO and probably 70% of it be procedural? I guess it really boils down to is it worth all the effort to make these two systems OO while the rest of the base won't be? I had other questions.. but they seem to have slipped my mind… perhaps some responses will jar them loose.. Or maybe not and their doomed to the bowels of the forgotten…
07 Mar, 2010, Runter wrote in the 2nd comment:
Votes: 0
Quote
Is this a waste of time to have parts of the codebase OO and probably 70% of it be procedural?


No, it's not. If this type of design fits the system you're working on well, and it seems to, then it will benefit you just as much since it's largely decoupled from the rest anyways.

What you seem to be really exhibiting here is inheritance. This is a neat concept indeed, but it's not a determining factor to whether or not you are designing your game in an object oriented way. (You can write object oriented code in C.) In my opinion, the two important factors–More important than inheritance–is abstraction and encapsulation.
07 Mar, 2010, Kayle wrote in the 3rd comment:
Votes: 0
Runter said:
In my opinion, the two important factors–More important than inheritance–is abstraction and encapsulation.


I get encapsulation, I've already made heavy use of it with the private fields, etc. But I'm not familiar with abstraction. Can you elaborate a bit?
07 Mar, 2010, Runter wrote in the 4th comment:
Votes: 0
Kayle said:
Runter said:
In my opinion, the two important factors–More important than inheritance–is abstraction and encapsulation.


I get encapsulation, I've already made heavy use of it with the private fields, etc. But I'm not familiar with abstraction. Can you elaborate a bit?


Abstraction is modeling classes appropriate to the problem, and working at the most simple level of inheritance to suite your given problem. In your case, it would be designing your code to interface with the concept of a Ship as much as possible and only delving into the idea of what specific type of ship it may be when you need that interface specifically.

Furthermore, object composition can help achieve this by separating your parts of your ship logically. For example, your cockpit, hyperdrive, blasters, cargo bay, etc etc.
07 Mar, 2010, Kayle wrote in the 5th comment:
Votes: 0
Runter said:
Furthermore, object composition can help achieve this by separating your parts of your ship logically. For example, your cockpit, hyperdrive, blasters, cargo bay, etc etc.


Kiasyn mentioned something like this on IMC, and while I get the general premise, I'm having issues wrapping my head around how it would actually work, and I can't find any similar examples in eithe rof the books I've been using to teach myself the ins and outs of C++ and OO Design.

And honestly this probably stems from not being as diligent in my learning/teaching as I should be, and a lack of formal education in the matter certainly doesn't aid things, but I always have issues learning something without an example of some kind. Which oddly enough is why I like this textbook I found on C++, it uses the same pair of examples throughout the entire book, the one it uses is a GradeBook program that it actually walks you through, and the second is an ATM like program that you use the skills in the chapter to write, or enhance and then there's a working example using the stuff from that chapter on the CD that came with the book… But I digress.

This Parts idea intrigues me, and I think it would definitely work better than what I've got so far, but I'm just not grokking it. (I hope I used that right… I'm sure Crat'll correct me if I didn't. :P)
07 Mar, 2010, Runter wrote in the 6th comment:
Votes: 0
Kayle said:
Runter said:
Furthermore, object composition can help achieve this by separating your parts of your ship logically. For example, your cockpit, hyperdrive, blasters, cargo bay, etc etc.


Kiasyn mentioned something like this on IMC, and while I get the general premise, I'm having issues wrapping my head around how it would actually work, and I can't find any similar examples in eithe rof the books I've been using to teach myself the ins and outs of C++ and OO Design….

This Parts idea intrigues me, and I think it would definitely work better than what I've got so far, but I'm just not grokking it. (I hope I used that right… I'm sure Crat'll correct me if I didn't. :P)


Well, you already seem to have parts variables in your aforementioned code. It's common in newer high level languages to let you define interfaces for object composition. Here's an example in Ruby.

class HighCommunications ; end
class Communications ; end
class Hyperdrive ; end
class Cockpit ; end
class Bridge ; end

class Ship
include Hyperdrive
include Cockpit
include Communications
end

class SpaceStation
include Hyperdrive # I guess? :p
include Bridge # Maybe different from cockpit? :p
include HighCommunications
end

class Satellite
include HighCommunications
end


Very contrived example which may not even properly apply, but here's a basic concept. I didn't define the methods and variables for each component, but they'd be hidden behind an abstraction layer which simplifies the receiving class interacting with them. They're also highly decoupled.
07 Mar, 2010, Runter wrote in the 7th comment:
Votes: 0
I should also mention that where I went to school it was frowned upon to use inheritance for the purpose of code re-use. Although, I think that is one of the obvious benefits that new programmers see when they learn about inheritance. This, in my opinion, can eclipse the real useful applications of it.

This brought back some memories from years past of debates over Inheritance vs Composition over Inheritance. I don't have a firm stance on what way you should take it. I just know how I find myself coding more often these days. :p

Also, keep in mind there's a clear difference between the "is-a" and "has-a" concepts that are common around these types of discussions. To elaborate on what I mean let's take some of my previous example:

class Weapons ; end
class Communications ; end
class Hyperdrive ; end
class Cockpit ; end

class Space_Object ; end # Anything in space?

class Ship < Space_Object
include Cockpit
end

class Fighter < Ship
include Weapons
end

class Satellite < Space_Object
include Communications
end


Everything in space is a Space_Object. A Fighter is a Ship which is a Space_Object. Etc. Do we need to take it this far? Probably not. But this is a clear way to see the difference between "is a" and "has a" relationships.

This may starting to get too complex for our problem, but space can be a complex problem. I'll leave it to you to decide what your game really needs modeled and how much complexity it really needs. A friend of mine says this often, and I often tell him it applies to careers other than his own (architecture) so I'll repeat the quote here: A designer knows he has achieved perfection not when there is nothing left to add, but when there is nothing left to take away.

That doesn't mean composite objects are bound to this "has a" relationship or true inheritance is exclusive to "is a" relationships. Indeed, here is an example:

class Hominid 
include Head
include Torso
include Legs
include Arms
end
class Person < Hominid
include Student
include Biker
include Programmer
include PokerPlayer
end


My apologies for the tl;dr'r.

Also, one more thing. I would suggest reading about some object oriented patterns. It's a great thing to learn from IMO. A few that come to mind… Composite, adapter, flyweight, proxy, observer, iterator, visitor, singleton, prototype. There's many more. Learning patterns will help you greatly at the end of the day.

I'll be happy to help you with any questions you may have. I'm sure there are others here who will give you their insights on the subject as well.
07 Mar, 2010, quixadhal wrote in the 8th comment:
Votes: 0
One point I'll toss in here is that your interfaces should be identical, wherever possible. For example, I note that your Fighter class has getCockpit(), setCockpit(), echoTo() and systemEcho(). Your Freighter has echoTo(), systemEcho(), and echoToControl(). While you may not have a cockpit in your Frieghter, nor a operations deck for systemEcho() to work in your Fighter, this creates two totally seperate API's which future code now has to special-case when interacting with your ships.

Ideally, you should make virtual methods for all of these in your base Ship class, and then the subclasses of ships which make use of them will have the proper code to make them work. Ships which don't have them could call the closest matching function (if possible), or return some kind of message/result indicating it doesn't work for this kind of ship.

If you do it that way, you can write code that works for ALL ships, rather than making the future code have to figure out what kind of ship it is before trying to call a method on it.

The argument can be made for not cluttering up the base class with lots of stubs, but I think the uniform API is more useful over the years than the small effort of having to add to the base class every time you modify a sub class's API.

A good example might be to consider a Shape class, which has a Circle and Square as subclasses. The Shape can be drawn, resized, erased, and filled. A Circle can return its circumference. A Square can return its perimeter, and can also be rotated. It makes no sense to get the circumference of a Square, yet that could be considered the perimeter. It also doesn't do anything useful to rotate a circle, yet it also doesn't hurt to do so.
07 Mar, 2010, flumpy wrote in the 9th comment:
Votes: 0
IMHO, integration aside, this looks the right way to go.

Some OO basic good practice to follow:

1. Prefer composition over inheritance
2. Create interfaces only when you have more than one class that will implement it
3. keep your methods short and, if possible, only have them perform one operation on the data
4. If you need to, draw interactions between objects to model complex interactions
5. Don't over generalise, or over engineer the solution. (This one's tricky, especially if you're having fun)
6. Keep it DRY
7. (General rule, of which 1 and 3 are derived from) remember: low coupling (objects tied to each other) , high cohesion

Have fun!
07 Mar, 2010, David Haley wrote in the 10th comment:
Votes: 0
The idea behind inheritance is that you have a bunch of stuff that understand the same basic set of methods – the so-called "interface" – but they each might reply to those messages in different ways. In other words, they implement different behaviors to the same interface.

While a lot of textbooks use cutesy examples with animals and ATMs and stuff, I like concrete ones that show real problems people solve. They're less contrived, and they are complete examples. Java shows inheritance very quickly with its collection interfaces. Let's start with a simple one: the list. You have a list of things; this is an abstract notion completely free from implementation that represents a sequence of elements. This sequence can be manipulated in certain predefined ways: you can add to it, remove from it, and so forth. This is all defined as an interface.

You then have implementations of the list like the ArrayList and the LinkedList. These do pretty much what they say: the ArrayList implements the sequence as an array in memory, resizing it as elements are added and so forth. The linked list implements its storage as a linked list of nodes.

Why would you care about this difference? In this case, different implementations have different properties when it comes to performance. With a linked list, for example, removing the first element is a fast operation, whereas with an array you need to copy all the elements down one slot. (In technical terms, it's constant time vs. linear time.) By contrast, with a linked list, the only way to say "get the twelfth element" is to go hop through twelve links, whereas an array lets you address it directly.

But most of the time, you just care about the fact that something is a list of stuff. So, functions are defined to take and return 'List' objects rather than ArrayLists or LinkedLists. When you create a list object, that is when you choose which implementation to use based on predicted usage and performance analysis.

Similar things happen with the Set and Map classes, which can be instantiated as Tree/Hash Set/Map, where the tree implements the storage as a binary tree whereas the hash uses a hash table under the hood.

This example illustrates many concepts of object oriented programming, but the most important I think is abstraction. As a user of these containers, you don't care how the methods are implemented. You just call "give me the element", and under the hood the right thing happens. Unless you're doing performance tuning, you should in general be thinking much more about the logical consequences of what happens rather than implementation details. You have enough to worry about when it comes to designing your code; worry about implementation separately and in its own time.

As a note, the composition examples given in Ruby using 'include' aren't really like composition as it's usually understood i.... It's more akin to multiple inheritance, sort of. Whereas mix-ins work at the class level by adding in extra functionality, object composition is typically achieved by instances themselves containing instances of other things. For example, a space station might not implement the "bridge" interface: it would contain a "bridge" object. In the former case, the space station exposes the bridge interface, whereas in the latter case, the space station has a bridge object available internally to manipulate. Which approach is more appropriate depends on what you're doing. Without knowing all the details of your system, I would personally not use mix-ins (or multiple inheritance in C++) here because I don't feel that it's appropriate for a space station to behave like a bridge; it has one but is not one. (NOTE: I know that Runter was using a completely contrived example, so this isn't criticism and I am well aware that he knows this is more complex than his self-described simple example)
07 Mar, 2010, Kayle wrote in the 11th comment:
Votes: 0
Hrm. Still a lot to learn it seems. And it also seems like I've been going at this the entirely wrong way. What I've succeeded in doing is making Subtypes of Ship, which adds extra work for me in the core of the system because I have to check for the type before calling anything.

Back to the drawing board I guess…
07 Mar, 2010, David Haley wrote in the 12th comment:
Votes: 0
Kayle said:
which adds extra work for me in the core of the system because I have to check for the type before calling anything.

Yes, this basically defeats the purpose of type abstraction, because you need to constantly check the type for something before you decide which method to call. This doesn't mean it's "wrong" – it might just be how things are – but perhaps things could be refactored.
07 Mar, 2010, quixadhal wrote in the 13th comment:
Votes: 0
Kayle said:
Hrm. Still a lot to learn it seems. And it also seems like I've been going at this the entirely wrong way. What I've succeeded in doing is making Subtypes of Ship, which adds extra work for me in the core of the system because I have to check for the type before calling anything.

Back to the drawing board I guess…


I don't think the design is a bad one… it makes perfect sense to want Fighters, Freighters, Bombers, etc… to all be specialized versions of generic Ships. You just need to nail down a consistent interface so you don't need to care what kind of ship something is before deciding to try and do something with it.

Just think about what you want to happen for each action, and what the results should be for various types of ships. If one action is lockTarget(), and the ship in question has no targeting computers (shuttle? one of those bike-speeders they used on Endor?), you probably want to do nothing, or inform the caller that it didn't work… but at the same time you don't want to have to check the ship type first.

One way I've seen this solved is to just have a virtual method in the base class, lockTarget(), and then ships which can do so will attempt to, returning the result. Ships that can't, just always return failure.

Another way, is to split the various Foo() actions into canFoo() and doFoo(). So, instead of saying BigShip->lockTarget(JarJar), you'd say if(BigShip->canTarget(JarJar)) BigShip->lockTarget(JarJar);

Ships that have the ability to target would put the checks for in-range, LOS, etc, in the canTarget() method. Ships that can't target at all would just return false. Now, *I* would still implement an interface for lockTarget() in every ship anyways… if the ship really can't do it, that would also just return false. If the ship could do it, but can't because of something checked in canTarget(), that gives you a god-mode "do it anyways" API.

Dunno if that rambling helps much, but… it's free.
07 Mar, 2010, David Haley wrote in the 14th comment:
Votes: 0
quixadhal said:
I don't think the design is a bad one… it makes perfect sense to want Fighters, Freighters, Bombers, etc… to all be specialized versions of generic Ships.

Not necessarily. Things that behave the same way probably shouldn't be different subclasses. If the differences are not in behavior but in data (e.g., size of cargo hold, number of guns, shield strength, etc.) it's probably better to not use inheritance but rather simply data. There's no need for a class hierarchy to match exactly our perception of the world, and sometimes this actually hurts things by adding complexity where none was needed.
07 Mar, 2010, flumpy wrote in the 15th comment:
Votes: 0
Davids right

You're car doesn't stop being a car because of how many doors it has or when you get a new stereo or add an extra headlamp (or even a gun :)).

The defining characteristic of a car is that it has 3> wheels and a motor, and this gives it it's fundamental carryness. Abstraction is a tricky business…
07 Mar, 2010, Scandum wrote in the 16th comment:
Votes: 0
The subclasses can be functionally the same and only set the initial data differently. The advantage would be that you don't have to initialize a ship from the ground up whenever you create a ship object.
07 Mar, 2010, David Haley wrote in the 17th comment:
Votes: 0
If the only goal is to initialize differently, it's silly to add complexity to the system. Different initialization is exactly what different static factory functions are for. E.g., Ship::MakeBomber(), Ship::MakeFighter(), etc.

EDIT: even without factory functions, you would still encapsulate it. Doing the same thing many times in many places is always a bad idea, whether you're writing an OOP framework or something else entirely. You would never keep initialization the same stuff "from the ground up".
07 Mar, 2010, Scandum wrote in the 18th comment:
Votes: 0
Sounds kind of messy to end up with 50-100 odd MakeSomething functions, even if Ship::MakeStealthBomber() calls Ship::MakeBomber() which calls Ship::MakeShip() so you inherit the data properly. I guess that's why you'd want to use proper sub classes to begin with, so the entire thing doesn't get top heavy.

It's time to mention shared pointer grids and call it a day.
08 Mar, 2010, Runter wrote in the 19th comment:
Votes: 0
DavidHaley said:
As a note, the composition examples given in Ruby using 'include' aren't really like composition as it's usually understood in OOP. It's more akin to multiple inheritance, sort of. Whereas mix-ins work at the class level by adding in extra functionality, object composition is typically achieved by instances themselves containing instances of other things. For example, a space station might not implement the "bridge" interface: it would contain a "bridge" object. In the former case, the space station exposes the bridge interface, whereas in the latter case, the space station has a bridge object available internally to manipulate. Which approach is more appropriate depends on what you're doing. Without knowing all the details of your system, I would personally not use mix-ins (or multiple inheritance in C++) here because I don't feel that it's appropriate for a space station to behave like a bridge; it has one but is not one. (NOTE: I know that Runter was using a completely contrived example, so this isn't criticism and I am well aware that he knows this is more complex than his self-described simple example)


I was unclear in my example and he's right. Although in this example I'll show how it could have been leading to composition. And yes, the interfaces could have been baked into the device classes. I think sometimes you may want to be more flexible with the interfacing for your specific needs. Not to mention just redefining functionality on the fly. In any event, this is an outside interface using composition.

class Communications   
def initialize
@comm_array = CommDevice.new(:low_communications)
end
### array isn't exposed.
end

class Hyperdrive
def initialize
@hyperdrive = FTLDevice.new()
end
def self.hyperdrive # Perhaps we want to expose the object.
return @hyperdrive
end
end

class Ship
include Hyperdrive
include Communications
end
08 Mar, 2010, Tyche wrote in the 20th comment:
Votes: 0
"Prefer composition over inheritance" - Eric Gamma
"Inheritance is composition." - Eric Gamma

*chuckles*
0.0/80