Top Mud Sites Forum

Top Mud Sites Forum (http://www.topmudsites.com/forums/index.php)
-   MUD Coding (http://www.topmudsites.com/forums/forumdisplay.php?f=9)
-   -   OO Encapsulation: Visitors (http://www.topmudsites.com/forums/showthread.php?t=392)

Yui Unifex 05-09-2002 06:57 PM

Greetings,

I can't tell you how many times I've looked at some code I'd written and instantly wanted to rewrite it. Some things look good when you're writing/designing them, but then you gain a crucial bit of knowledge and understand how it could be done better.

This will be the first in a (hopefully) series of articles that will parallel my own development and design of an object oriented codebase. I encourage anybody that's gone through similar development phases to post here with insights and knowledge about the way their muds are designed. Spreading this knowledge is vital for next-generation codebases to avoid, or at least shorten, the write-learn-rewrite cycle that I've had to go through in my journey from a procedural to an object-oriented designer.

---

Many OO designers misunderstand the nature of encapsulation. A properly encapsulated class will have accessors for some delegated items. But writing getters and setters for each variable only triples the amount of code one has to write for each data member of a class.

Sometimes there seems to be no way around it -- a class must reveal a non-const reference to a datamember in the name of flexibility. The only alternative, to many minds, is to write the operation as a function of the class itself, which breaks encapsulation of algorithms elsewhere. But even this exposing method is quite fallible, because the calling function must have detailed knowledge of the nature and amount of the datamember it applies to.

Visitors are a wonderful method for getting around these flaws and promoting encapsulation. A visitor is an object that consists of at least one virtual member: The 'visit' operation. Visitors are generally seperated by the type of object they visit upon, not the type of object they are passed to. An ElementVisitor might have a "virtual void visit (Element *);" member function.

Let's see how this visitor would work with some unrefactored sample code:
[code] // Map;;sendText; Sends a string to all objects in the given tile.
// Does not send the string if the object is deaf.
void Map;;sendText (Tile &tile, const string &str) {
Tile;;ObjectList;;iterator current = tile.getObjectList().begin();
Tile;;ObjectList;;iterator end = tile.getObjectList().end();
for (; current != end; ++current) {
if (!(*current)->isDeaf())
(*current)->send(str);
}
}[/quote]

One can plainly see that the tile needs to expose its list operations so that the Map can send the text according to some algorithm which is found within the for loop. If we gave this algorithm to the Tile, the Tile would have to have intimate knowledge of the object types in its list, which may be undesirable. So let's try out the visitor pattern here.

[code] class ObjectVisitor {
public;
virtual ~ObjectVisitor () { }
virtual void visit (Object *) = 0;
};

class ObjectSendTextVisitor; public ObjectVisitor {
public;
ObjectSendTextVisitor (const string &new_str); str(new_str) { }

void visit (Object *object) {
if (!object->isDeaf())
object->send(str);
}

private;
const string &str;
};

void Map;;sendText (Tile &tile, const string &str) {
ObjectSendTextVisitor visitor(str);
tile.visitObjects(&visitor);
}

// Tile;;visitObjects; Calls the visit function for every object
// contained in this tile.
void Tile;;visitObjects (ObjectVisitor *visitor) {
for (ObjectList;;iterator current = mObjectList.begin(); \
current != mObjectList.end(); ++current) {
visitor->visit(*current);
}
}[/quote]

Much better. Now that might look like a lot of work, but don't let your eyes deceive you. You only have to write a visitor for every operation -- with a macro to declare the visitor type, you'll find yourself writing less code in total, because now you don't have to bother with a loop every time you want to visit something.

This code also has other advantages. If Tile were a virtual interface object, we can now include multiple lists for subclassed tiles. For example, if you had a "HiddenCacheTile" that sported a hidden cache of objects, you could easily adapt your code so that the visitor works on all of them -- all without having to modify the code that actually employs the visitor:

[code] void HiddenCacheTile;;visit (ObjectVisitor *visitor) {
ObjectList;;iterator current;
for (current = mObjectList.begin(); \
current != mObjectList.end(); ++current) {
visitor->visit(*current);
}

for (current = mHiddenCache.begin(); \
current != mHiddenCache.end(); ++current) {
visitor->visit(*current);
}
}[/quote]

Using this technique will save you from writing countless accessors to achieve flexibility within your code.

aforsman 05-10-2002 12:56 AM

I'm in the process of writing an OO MUD using C++. I've found it to be a challenge to keep it as "OO proper" as possible, there are a lot of things in a MUD just aren't OO friendly. Ironically, a MUD seems to be the perfect OO setting. Like you said, having getters and setters for every variable in a class just isn't a viable option in most cases. If you do use the visitor method to get around this, you're still going to have to write a method for each thing you want to do with the class.

Let's say a player types in "look". The character class has all of the commands in it, and there is another class that holds the room. So now we're going to have to get the info from the room to the character. We could have a method that has the room create a look string and pass it back to the character class, and that would work ok for that instance. But what about other room manipulation commands? We'll need seperate methods to do all that stuff too.

I've heard that friend classes are bad, but I've spent a long time trying to figure out a way to set up my MUD to keep good OO practice and all that other good stuff. In most other programs I've written, it's been easy and worked well, but in my code this time I gave in and am using the friend keyword in a few places. For instance, I've made the character class a friend in the item and room classes, because the character class uses those all the time. I do not however, change any member variables of room or item in the character class directly.

I'm really interested in a new way to do this, but from what I've seen, many examples of OO in muds have made it way too complicated, messy, or too much work. Sure, encapsulation may make things better if used properly, but I think sometimes you can go too far and start throwing in OO techniques that make it worse.

my 2 cents.

Yui Unifex 05-10-2002 07:52 AM

I must agree that there are alot of things that -- somewhat incomprehensibly -- aren't OO friendly when they really should be.  But I think that this stems from a lack of knowledge of just how certain things can be made friendly.  It's definately a different paradigm from the rather haphazard designs (Dikurivatives) that most of us learned from.

Correct.  Just like you'd have to write a function if you want to do something with a "public" class.  I use a number of macros (I'll post about them later) to define the types I need.  Most of them have one-line declarations, just like static methods.  The only thing you can't do is embed the algorithm in the parent function.  While the usefulness of this is debatable, I generally find that encapsulating the algorithm results in nicer code.

Assuming that you have two seperate classes, Room and Character, this is another pattern called the mediator.  This was actually what my next article was going to be about =).  Basically, the mediator executes an operation using those two objects.  To avoid getters and setters, the mediator is often made a friend of both objects.  So in your example, the "CharacterRoomMediator" would perform the operation on the Room class, then format and send output to the Character class.

Before I knew much about mediators, I used the exact method you described -- the look operation returned a string, which was then sent to the character class =).

Friends aren't necessarily bad -- just like accessors and public datamembers aren't necessarily bad.  Some of them do have their place.  Friends are actually more desirable in alot of cases, because then you give explicit permissions, whereas an accessor will give permission to everybody.

I wholeheartedly agree.  Unfortunately, C++ (and Java even moreso) leaves much to be desired in the methods used to declare classes and types that can rival a C programmer's function library.  Nobody wants to write a class definition for every operation, but that's often what one has to do.  I use macros to get around this, to make my C++ declarations as simple to perform as a C declaration.

Encapsulation is only bad if used improperly.  If done poorly, encapsulation can greatly increase the amount of work one has to do.  Because one has to write many of the encapsulating functions (accessors), the programmer receives very few, if any, of the benefits by tightly coupling classes together.

Your input is appreciated =).


All times are GMT -4. The time now is 07:11 AM.

Powered by vBulletin® Version 3.6.7
Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
Copyright Top Mud Sites.com 2022