[Long] Static classes, inheritance and base class funkery - or overriding module subroutines

T

Tim S

Hi

I'd be grateful of some feedback or different ways of solving this problem:

Scenario:

To write a suite of libraries for use by system maintenance scripts, eg
common logging, file-write handling (open tmp file, write to that, atomic
rename at end), database access etc. No problem fundamentally - done it
before. Want to improve and make this cut open source (last was at a
previous place of work).

Requirements:

a) Static classes (or modules) - each class is initialised once globally,
first by built in defaults then possibly reconfigured (typically once) at
run time from script arguments.

b) Make it easy for a 3rd party to override or add methods.

c) More complex classes may use the services of simpler classes, eg
everything uses the Log class.

d) Method overrides need to become visible to all users of the original
class as we do not want to edit the "use" line in every utiliser class.

e) No derived class can directly access the data in its SUPER class - public
methods only allowed, so this simplifies things.

eg:

Say we have classes:

Sys::Admin::Log
Sys::Admin::DB

and a script:

useraccounts which regenerates the passwd, group and shadow files on *nix.

But another site wants to override the message format in Sys::Admin::Log, so
derives Sys::Admin::Log::MySite, replacing one method, formatmsg($baremsg)

---------------
My solution so far (which works in test code):

Sys::Admin is a true base class to all others and provides a way for each
derived class to declare itself the "implementor" for its "root class" (my
terminology for the base class provider of that functionality). Sys::Admin
merely contains common helper methods.

When Sys::Admin::Log loads, it notes in a package hash in Sys::Admin that it
is the implementor class for "Sys::Admin::Log" (ie the base of the class
hierarchy that is responsible for logging functionality) and ditto for
Sys::Admin::DB (provider of DB services).

To use the class in another class or script we do:

##########
use Sys::Admin::Log;
sub L { return Sys::Admin::Log->load() }

L->log(L->INFO, 'Initialising ' . __PACKAGE__);

#########

Now our script might do:
#########
use Sys::Admin::Log::MySite;
use Sys::Admin::DB;

sub L { return Sys::Admin::Log::MySite->load() }
sub D { return Sys::Admin::DB->load() }


D->dosql("update table foo='bar' where wibble=1") or
L->log(L->FATAL, 'Bang');

#########

load() initialises the class the first time and everytime it returns the
implementor class name.

Thus L() just returns a class name - eg the literal 'Sys::Admin::Log'
OO style method calls work, eg L->log(..) which is fairly neat.

1) Sys::Admin::Log::MySite loads and notes (via Sys::Admin) that it is the
implementor for the root class "Sys::Admin::Log". It mostly relies on
Sys::Admin::Log's methods but overrides formatmsg()

2) Sys::Admin::DB loads and initialises Sys::Admin::Log. If it needs to log,
it's call to sub L { return Sys::Admin::Log->load() } actually returns the
literal 'Sys::Admin::Log::MySite' (from the hash in Sys::Admin) so it gains
the overridden methods via perl's normal @ISA search.

This is why the "object" is actually a sub - it can potentially change
depending on the order things are initialised - seems safer this way. It
was that or making the "object" a reference which makes the calling
semantics more ugly.

The test code is actually fairly elegant (but weird) in the way that derived
sub classes can be declared, but I'm wondering if I've missed a trick...

It's hard to describe further without getting even more verbose...

It seems rather complicated, and indeed, when we did a similar scheme at my
last workplace, we just made a suite of pure non OO modules. The difference
is, if we needed to change the functionality, we edited the base modules.
I'm looking for a way to give a basic core away, but allow other users to
customise the suite without having to hack around with the base
modules/classes - and thus make integrating updates more difficult.

In effect, I really want neat subroutine overriding on modules, but perl's
OO methodology seemed the only sensible way to achieve this.

Thoughts would be welcome. Sorry if this is not clear - it's not very easy
to describe in a couple of screen of text!

Cheers

Tim
 
A

anno4000

[...]

Sorry for the big snip, I only want to comment on one thing.
In effect, I really want neat subroutine overriding on modules, but perl's
OO methodology seemed the only sensible way to achieve this.

Have you tried the direct approach? If you want subroutine overriding
in modules, use subroutine overriding in modules, it may be powerful
enough. What I mean is that you *can* redefine a sub that is called
elsewhere, *after* "elsewhere" has been compiled and have "elsewhere"
use the new definition.

Suppose you have routine logger(), based on a central_logging_routine()
that defaults to an English format. Then, in some environment, someone
wants to use logger() in a German format, but continue to call logger()
for the purpose. This can be realized as follows:

sub logger {
central_logging_routine( shift);
}

sub central_logging_routine {
my $msg = shift;
print "Log Message: $msg\n";
}

logger( "Message one");

{
no warnings 'redefine';
*central_logging_routine = sub {
my $msg = shift;
print "Log-Meldung: $msg\n";
};
}

logger( "Meldung zwei");

That prints

Log Message: Message one
Log-Meldung: Meldung zwei

The effect is program-wide. After the change, everyone using logger()
will use the new format.

The example shows everything in one file, with the switch in behavior
at run-time. The switch might as well be performed in a loadable
file through "use" or "require" without first exercising the default
version.

I'm not sure if that is the behavior you want, but you may want to
consider it.

Anno
 
T

Tim S

[...]

Sorry for the big snip, I only want to comment on one thing.
In effect, I really want neat subroutine overriding on modules, but
perl's OO methodology seemed the only sensible way to achieve this.

Have you tried the direct approach? If you want subroutine overriding
in modules, use subroutine overriding in modules, it may be powerful
enough. What I mean is that you *can* redefine a sub that is called
elsewhere, *after* "elsewhere" has been compiled and have "elsewhere"
use the new definition.

Suppose you have routine logger(), based on a central_logging_routine()
that defaults to an English format. Then, in some environment, someone
wants to use logger() in a German format, but continue to call logger()
for the purpose. This can be realized as follows:

sub logger {
central_logging_routine( shift);
}

sub central_logging_routine {
my $msg = shift;
print "Log Message: $msg\n";
}

logger( "Message one");

{
no warnings 'redefine';
*central_logging_routine = sub {
my $msg = shift;
print "Log-Meldung: $msg\n";
};
}

logger( "Meldung zwei");

That prints

Log Message: Message one
Log-Meldung: Meldung zwei

The effect is program-wide. After the change, everyone using logger()
will use the new format.

The example shows everything in one file, with the switch in behavior
at run-time. The switch might as well be performed in a loadable
file through "use" or "require" without first exercising the default
version.

I'm not sure if that is the behavior you want, but you may want to
consider it.

Anno

Ah yes - that is a pretty simple way to achieve overrides. I'd initially
discounted direct redefines as I'd erroneously considered
redefining "logger()" which I worried wouldn't affect the already exported
verions (I tend to EXPORT_OK if doing modules). But what you are doing is,
in effect, using logger() as a dispatcher to central_logging_routine()
which is never exported, so there is no problem.

Neat and simple and will work with OO and non OO modules.

Does mean that potential hooks need to be pre-declared (perhaps every public
method simply wraps an internal sub??), but may be beneficial because it is
clearer.

May have a problem with sub-classing a sub-class - but I'm not sure that is
ever going to be a practical problem...

Thanks for your ideas Anno. I going to think further on this.

Cheers

Tim
 
B

Brian McCauley

Ah yes - that is a pretty simple way to achieve overrides. I'd initially
discounted direct redefines as I'd erroneously considered
redefining "logger()" which I worried wouldn't affect the already exported
verions (I tend to EXPORT_OK if doing modules). But what you are doing is,
in effect, using logger() as a dispatcher to central_logging_routine()
which is never exported, so there is no problem.

Hey, that's neat. I'd never thought of that in those terms. I've used
this technique in the past but I've never thought to describe it in
such succinct terms.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,763
Messages
2,569,563
Members
45,039
Latest member
CasimiraVa

Latest Threads

Top