[Sorry for the delayed response. I knew this would take some time to type
down and I was unable to find any until tonight.]
At Sat, 8 May, 1999, Niklas Elmqvist wrote:
>
>On Thu, 6 May 1999, Emil Eifrem wrote:
>> Lately I've beginning to doubt this modular approach. It sounds very good
>> in concept and on an abstract level, but when you get down to some real
>> code it's partly really messy and troublesome.
>
>Hehe, you don't see me *implement* this stuff, do you?
*grin* No, no I don't. I have no doubts that you could tho.
And I do see the DevMUD team implement it, although I'm not sure how far
they've come (I'm subscribed to the list but it's very quiet, I guess most
of the discussions are on Jon's slimy or frosty or whatever it's called :).
>
>> That's probably a result of flawed design decisions on my end, though,
>> it's the first time I build a system of this magnitude.
>
>I guess the difference between good and bad design decisions is very slim
>in this area -- do it just right, and you've got a real winner, but if
>you're a little off, the results might be less-than-perfect. I'm not
>critizing you in any way (I haven't seen your systems architecture), but
>rather speaking from my own experience. Nowadays, I think I've outgrown
>the start-coding-immediately syndrome and find design and analysis
>extremely rewarding (almost TOO rewarding, in fact -- implementation does
>not hold that certain thrill anymore).
Yes, I think I'm currently in the less-than-perfect area. I get the feeling
that many of my design decisions were forced by language-level quirks and
specifics, particularly in the area of inter-module communication [IMC].
And I think designing a flexible enough system for IMC is the key issue
with this modular architecture.
>
>> I've been toying with the idea of writing down my overall design
>> decisions and post it to the list for comments but never gotten around
>> to it.
>
>Please do! I for one would be very interested.
Oh, not after I've talked for a while. But okey, you asked for it. This
turned out to be way longer than I thought and I apologize in advance for that.
The basic concept (shamelessly stolen from the initial devmud thread
('pdmud'? 'virtual communities'? anyway, it was horrendously misnamed but
very interesting, interesting enough, in fact, that I printed out the
entire 200 pages thread... but I'm digressing already!)) is that the server
consists of a number of separate code entities called modules that are
dynamically loaded and unloaded from a static kernel.
The kernel's sole responsibility is module loading and unloading -- all
other functionality is provided by modules. One of the main advantages with
this design is that, if handled properly, one would seldom if ever have to
reboot the server. If there's a bug in the economy code -- well, fix the
bug off line, recompile the economy module then tell the kernel to ditch
the old economy module and load the new one in its place. Beautiful.
So far so good. This is all basic DevMUD stuff. Or basic plugin stuff.
Whatever.
This is familiar to anyone who paid attention to the devmud threads and/or
-list and basically to anyone familiar with for example the Linux OS's
module loading/unloading features. I don't have a problem with the concepts
but I do have some problems with implementing them. In order to elaborate
on my problems, I will have to go pretty low level and language centric,
sorry about that.
So, in my attempt to concretize in Java these very elegant concepts, I
(rightly or wrongfully) identified two critical sections: First off, the
kernel implementation: If there is a need to modify the kernel, a reboot
*is* required. So it's very important to assign as little functionality as
possible to the kernel. Secondly, the inter-module communication. IMC is of
course the way the different modules talk to each other and it's important
in order to get the modules to, once loaded and initialized, behave not as
separate pieces but as a coherent whole.
My server has two ways of handling IMC. The first, most obvious and most
elegant way is event based. I have a standard GoF Observer+Mediator
implementation of it where a module registers itself with the kernel for
every specific event it's interested in and only then will the kernel relay
events of that type to the module.
All modules inherit from the abstract superclass kernel.Module, which
provides the interface to which all kernel<->module communication is
handled -- as well as a number of utility methods for the modules. One such
utility method is generateEvent(e, d), which tells the kernel to broadcast
an event of type e with data d to all modules subscribed to that type of event.
Here's what it may look like in code:
---
/* in some random module */
// initialize() is defined in kernel.Module and is called on module bootstrap
public void initialize()
{
// subscribe to the event of type NETWORK_NEW_INPUT
registerEvent(IMCEvent.NETWORK_NEW_INPUT);
}
public void eventReceived(IMCEvent event, IMCEventData data)
{
if (event == IMCEvent.NETWORK_NEW_INPUT)
{
parseInput(data);
}
// ... and all other events we're interested in
}
private void someRandomMethod()
{
generateEvent(IMCEvent.MY_EVENT, myData);
}
---
I like this way. It's decoupled and it's clean and simple. I wish it were
enough but, alas, it turns out that we need something more.
The second way of of module communication is way uglier. As a short
motivation for it, consider a case where I have two modules. One is a
Command module that handles command requests from players and the other is
a Database module that interacts with a database. Let's say the Command
module wants to find out the amount of swords available in the DB. A
"traditional" way to solve this may be something like this:
---
String query = "SELECT COUNT(*) FROM WEAPONTABLE WHERE TYPE = 'Sword';";
int amountOfSwords = DataBaseBroker.getBroker().issueQuery(query);
player.println("There are " + amountOfSwords + " swords in the db.");
---
All good. In a server that uses the modular approach (or at least my
interpretation of it!) the asynchronousity of the event mechanism is
causing some troubles. Consider this snippet:
---
String query = "SELECT COUNT(*) FROM WEAPONTABLE WHERE TYPE = 'Sword';";
generateEvent(IMCEvent.DB_ISSUE_QUERY, query);
plr.println("uhh... what? We haven't gotten anything back from the db yet.");
// ... now what?
---
The problem here is that generateEvent() just adds the DB_ISSUE_QUERY event
to the event queue and then immediately returns. All the actual processing
of the event is handled asynchronously, ie in one or more separate threads
in the kernel. Sticking to my example above, this is a big problem for the
Command module who just wanted to get an int representing the amount of
swords in the DB. What should it do? Sleep until the DB answers with a new
event? Continue processing other commands until the DB module answers --
then, continue executing the sword-counting command?
None of the above is viable. My solution was to introduce a second way of
IMC, tentatively called "exported functions." (Horrible name for something
in a Java server, I know.) The idea is to make it possible for a module to
execute special, 'exported' methods in other modules. Probably the best way
to explain this is to show an example:
---
DBModuleInterface = getModuleInterface("database");
int amountOfSwords = DBModuleInterface.issueQuery(query);
player.println("There are " + amountOfSwords + " swords in the db.");
---
getModuleInterface() is a utility method defined in the module superclass.
It takes a string representing a module as argument and returns a Java
interface that contains the methods that the target module has chosen to
export. This second way of IMC is nice cause it makes stuff like the
example above possible -- but it's less nice in that it couples modules
together and makes module unloading unsafer.
Ok, so in summary, I have two ways of handling communication between
modules. One uses a standard event broadcasting
(/Observer/Publisher-Subscriber/etc) mechanism. The other uses a way for a
module to expose selected "extremely public" methods to the other modules.
I guess that was a not-so-brief but yet very-incomplete description of my
approach to this modular architecture. I think this all started with me
stating that I lately have had doubts as to the (or rather, my) actual
implementation of a modular MUD server so I guess I should finish up with
elaborating some on that. [Reeaally sorry for the length. :( Is anyone
still reading?]
Some problems with my approach to the modular server (some of the points
are probably applyable to other's as well):
- Source code visibility. The way class unloading in Java works, you
have to write a custom class loader that load all classes you want to be
able to unload. This means that all modules exist in separate namespaces.
They can see classes in their own module and classes in the kernel but
nothing in the other modules.
This means that if I create a 'public class MyClass' in the kernel, then
module X can instantiate an object of type MyClass. But if I define a
'public class MyOtherClass' in module Y, module X has no clue about it. So
basically, the only thing that is "portable" between modules are classes
defined in the standard Java libraries (eg String, int, Hashtable, Socket)
and what I have defined in the kernel.
This may not sound like a big problem. But it is, believe you me. Let's say
that I want to have a Player class that several modules are dealing with.
For example, I used to have a "Creation" module, that was responsible for
creating new players and characters (player = a rl-world person, character
= a fictional person run by a player). I thought this was a neat idea, but
in order to have the Creation module pass the newly created players and
characters to other modules (say, the 'World' module and the 'Playermode'
module) I would have to move the Player and Character classes into the
kernel. Remember, classes created in the Creation module are only visible
to the Creation module.
This whole thing was a big disappointment to me. I originally had planned
for about 15 modules. If I can't work out a good solution to this problem,
I will probably have to go down to two or three. And that's when I ask
myself, why did I make this modular again?
- I want my events to be typesafe. I would hate a situation where I'm
looking all over the place for a specific bug -- and then realize that I've
just mistyped "NETWORK_NEW_INPUT" as "NETWORK_NWE_INPUT". I want the
compiler to catch that.
Java doesn't have 'enum' but a similar effect is easily fixed by making a
singleton class (ie final and with a private constructor) that has static
instances of itself as public member variables. I chose to call mine
IMCEvent and the code would look something like this: 'IMCEvent e IMCEvent.MY_EVENT;'.
In order for all modules to see and use this class it has to be put into
the kernel. Oh. Fun. What was the number one point that was critical to
this modular approach again? Oh yea, minimal functionality in the kernel.
Why? Cause everytime we change the kernel we have to reboot the entire
server. IMCEvents are bound to change almost every time you make some
serious changes. Can we put that in the kernel? No. Can we put it in a
module? No. Houston?
Hmmm. I guess those are the two main probs. I'm gonna quit talking now. If
anyone's still reading, congratulations to you. Now tell me what I'm doing
wrong. I want to believe in a modular, object-oriented MUD server again!
[ - - - -
Emil Eifrem [emil@prophecy.lu || www.prophecy.lu/~emil]
Implementor of Prophecy [
telnet://mud.prophecy.lu:4000]
Coordinator of the Jamu effort [
http://www.javamud.org]
- - - - ]