v_melnik has asked for the wisdom of the Perl Monks concerning the following question:

Dear Monks,

I would like you to share your opinions on the matter of passing the reference to a common object between a library components.

Let's imagine that we have a complex library that consists of a bunch of classes. There is the Library::Logger class that is being used by other components (e.g. the initialization method of Library initializes an object of the Library::Logger class and then the Library-based object uses the Library::Logger-based object to call the logging methods.

There are many other sub-classes that needs to use the same object too: Library::Foo, Library::Bar, Library::Baz. Moreover, there are Library::Foo::Schmoo, Library::Bar::Schmar and Library::Baz::Schmaz sub-classes, there are dozens of classes. The question is: how to let them all have the reference to the logger that had been initialized once by the Library-based object?

I see the following ways to acheive it:

  1. When a Library-based object initializes the Library::Foo-based object, it passes the link to the logger to the newly-created object: $obj = Library::Foo->new(logger => $self->_get_logger);. So the Library::Foo-based object could get the logger as $logger = $self->_get_logger.
  2. When a Library-based object initializes the Library::Foo-based object, it passes the link to itself to the newly-created object: $obj = Library::Foo->new(papa => $self);. So the Library::Foo-based object could get the logger as $logger = $self->_get_papa->_get_logger. It breaks encapsulation, but it lets the Library::Foo-based object to get many other things from its "papa".
  3. Use a singleton (as MooseX::Singleton) or a global variable (as $Library::_logger) which seems to be even worse.

What do you think, which way seems to be better? What way would you propose? How do you solve this problem for yourself?

Many thanks in advance!

V.Melnik

Replies are listed 'Best First'.
Re: Passing the logger's object
by stevieb (Canon) on Nov 13, 2016 at 16:13 UTC

    Here's a very quick and dirty example that I think uses parts of each technique you've shown. When new() is called on Library, we store a log as a class variable. It has a log() method that will return this log. In the case below, with Library::Foo, Library passes the log in. We could also have accessed it as $Library::log, or much more preferably $log = Library::log()->child('name');.

    This way, each class has a copy of the top-level log which has had it's class name appended to the log name. Then, each sub inside of each package makes a copy of this class log object, which then in turn appends its name, so the log messages each reflect exactly where the log was used.

    I've used numerous ways of doing things like this. This is but one of them.

    use warnings; use strict; package Library::Foo; { my $log; sub new { my ($class, $logger) = @_; my $self = bless {}, $class; # set the class level log $log = $logger->child('Library::Foo'); # generate a local log per each sub my $log = $log->child('new'); $log->_0("creating new foo obj"); return $self; } } package Library; { use Logging::Simple; # set the class log my $log = Logging::Simple->new(name => 'Library'); sub new { my $self = bless {}, shift; # generate a child log per sub my $log = $log->child('new'); $log->_0('creating Library obj'); return $self; } sub log { # return the class level log obj return $log; } sub foo { my ($self) = @_; my $log = $log->child('foo'); $log->_0('about to crate lib::foo obj'); $self->{foo} = Library::Foo->new($self->log); } } package main; my $lib = Library->new; $lib->foo;

    Output:

    [2016-11-13 09:12:13.179][lvl 0][Library.new] creating Library obj [2016-11-13 09:12:13.180][lvl 0][Library.foo] about to crate lib::foo +obj [2016-11-13 09:12:13.180][lvl 0][Library.Library::Foo.new] creating ne +w foo obj

      Thank you for the comment!

      So, you would suggest to pass the logger's reference to the class' initializer, right?

      V.Melnik

        Not always, but mostly I do. Whether using named params:

        my $obj = Module->new(log => $obj, arg1 => $arg1);

        or, positional (in this case, I typically have the log obj as the first parameter):

        my $obj = Module->new($log, $arg1, $arg2);

        ...and sometimes, I'll have the first arg as $self, then the log obj, then named params after that:

        my $obj = Module->new($log, %args);

        The new() method would look something like this internally in that case:

        sub new { my $self = shift; my $log_obj = shift; my %args = @_; # do stuff }

        You can also call the class method within your new() method, and stash it inside the object:

        sub new { my $self = bless {}, shift; $self->{log} = Library->log()->child('This::Module::Name'); my $log = $self->_log()->child('new'); } sub foo { my $self = shift; my $log = $self->_log()->child('foo'); # do fooish type stuff } sub _log { return shift->{log}; }
Re: Passing the logger's object (updated)
by haukex (Archbishop) on Nov 13, 2016 at 17:30 UTC

    Hi v_melnik,

    Globals have a bad reputation because they are so commonly misused. But that doesn't make all globally accessible values bad: Will you ever have a need for more than one logger object in your entire application? And you can still use an OO approach and encapsulate the retrieval of the globally accessible value in a method, allowing for you to later change how loggers are retrieved (admittedly, if you make it a class method, that limits it a bit, but luckily in Perl class and instance methods are very similar). Here are just a few examples of the many possibilities:

    use warnings; use strict; use feature 'state'; package Library; sub new { bless {}, shift } sub logger { # Just an example; # could be class or instance method / property, # could hand out different logger objects. state $logger = Library::Logger->new; return $logger; } package Library::Logger; sub new { bless {}, shift } sub log { shift; print "[".gmtime." UTC] @_\n" } package Library::Foo; sub new { bless {}, shift } sub foo { Library->logger->log("access via class method works") } package Library::Bar; use parent -norequire, 'Library'; sub bar { shift->logger->log("inheritance works") } package Library::Quz; sub new { my $class = shift; bless {@_}, $class } sub library { shift->{library} } sub quz { shift->library->logger->log("access via property works") } package main; Library::Foo->new->foo; Library::Bar->new->bar; my $lib = Library->new; $lib->logger->log("access via instance method works"); Library::Quz->new(library=>$lib)->quz;

    Update: Added Library::Quz class and updated example code accordingly; added some comments.

    Hope this helps,
    -- Hauke D

      Thank you for these examples!

      In other words, it's okay to use some methods of the parental class, as it's not a crime, right?

      V.Melnik

        Absolutely having class methods/data is ok.

        This does not break encapsulation. You're simply calling the method on the class, instead of an instance (object) of the class.

        This in fact is quite common. Imagine you have a class that counts its objects. The count variable would be a class var, and you could just write a class method that returns that value. Note here that when writing class methods, you have to realize that the first param will be the module name, not a blessed instance, so you won't have $self to operate on.

        Class methods don't break encapsulation, and if you ever changed $count, you'd refactor the class method to return the right thing, instead of changing scripts that reference $Module::count directly.

        Class methods also provide you the ability to do things with a module without having to instantiate a new object:

        sub new { return bless {}, shift; } sub add { shift; # throw away object/class; not needed my ($x, $y) = @_; return $x + $y; }

        Now you can add() in either of these ways:

        my $res = Module->add(1, 2); # or my $obj = Module->new; my $res = $obj->add(1, 2); # or even my $mod = 'Module'; my $res = $mod->add(1, 2);

        I use the latter form of calling class methods in my unit tests, especially when creating numerous objects within the same test file:

        my $mod = 'My::Long::Module::Name'; { # test 1 my $obj = $mod->new; } { # test 2 my $obj = $mod->new; } # etc

        Hi v_melnik,

        If you're referring to my inheritance example Library::Bar, then no, it's not a crime and absolutely normal to call a method inherited from the parent class this way. A subclass is just a mixture of methods inherited from the parent, parent methods overridden by the subclass, and methods defined only in the subclass. Except for the case of a method in the parent class being intended to be completely private to the parent, the subclass's methods can call each other all they want.

        If you're referring to my Library::Foo example of calling a class method, then again no, any class is free to call any other class's (public) class methods - the most common example of a class method is new! The disadvantage of class methods is that they don't operate on individual objects, which in your example would mean that the class method that hands out the logger object could not hand out different Library::Logger objects based on different Library instances. You may not need to do so at the moment, but perhaps you might decide in the future you'd like to do so, hence an instance method would be more extensible.

        Just as an aside, in Perl, it's easy to write a method that can be used both as a class and an instance method, since the only difference between the two is that a class method gets the name of the class it was called on as its first argument, while an instance method gets the object as its first argument. Also note how inheritance works in the following example:

        package Foo; sub new { bless {}, shift } sub foo { my $self = shift; if (ref $self) { print "Instance method called on $self\n"; } else { print "Class method called on class $self\n"; } } package Bar; use parent -norequire, 'Foo'; package main; Foo->foo; # "Class method called on class Foo" Foo->new->foo; # "Instance method called on Foo=HASH(0x...)" Bar->foo; # "Class method called on class Bar" Bar->new->foo; # "Instance method called on Bar=HASH(0x...)"

        Hope this helps,
        -- Hauke D

Re: Passing the logger's object
by stevieb (Canon) on Nov 13, 2016 at 21:21 UTC

    Here's a script which covers everything I've commented about on this thread in the first 31 lines.

    I use class methods at the beginning so that I don't go through the expense of creating an object (which is a bit intensive) until I know that the script will execute past the part that I absolutely need it.

    Lines 29-31 is where I instantiate the object, get a log object from it, then localize it.

    I love it when I reference my own code. I read it thoroughly, and then spot bugs I didn't know existed. This is one of them cases, and surprisingly, I'm kind of weirded out that things work anyways ;)