Reputation: 341
I am writing a perl class, which for the purpose of this question I will call Student. The Student class will have a lot of methods, and for the sake of tidiness and efficiency, I don't want to put them all in one source file.
Let's say that Student.pm is package Student
and contains sub new
, the constructor, and that Student/Enroll.pm is package Student::Enroll
and contains sub enroll
, a method. I am writing sub enroll
like this:
sub enroll
{
my ($student) = @_;
# do something;
return;
}
*Student::enroll = \&enroll;
In my main program, I can write my $student = new Student()
and $student->enroll()
. This works but it's opaque. Does it have any disadvantages that I am overlooking?
I haven't tried exporting enroll
from the package Student::Enroll into the package Student. It's not what I want because then I would need to write use Student::Enroll
in package Student, and I don't want to do that. There will be programs that need the Student package but don't need Student::Enroll.
Is there a better, cleaner way to write sub enroll
in Student/Enroll.pm so that it becomes a Student method?
Upvotes: 1
Views: 106
Reputation: 3272
It easily can be done with help of AUTOLOAD method.
package Student::Enroll;
use strict;
use warnings;
use v5.14;
sub enroll { say "Good luck $_[1]"; }
1;
and
NEW VERSION:
package Student;
use strict;
use warnings;
use v5.14;
use Carp qw/croak/;
{
my $_additional_methods = {
enroll => undef
};
sub _can_access {
exists $_additional_methods->{$_[0]}
}
}
sub new { bless {}, shift }
# Subroutine AUTOLOAD will be called always
# when someone calls a method (a subroutine) which
# isn't present in this module.
# In our case "enroll" will be called from AUTOLOAD
# only one and next time of calling it will be called
# directly as a normal method of the class since
# we create a typeglob ref for it.
sub AUTOLOAD {
no strict 'refs';
our $AUTOLOAD; # in this global variables are kept
# the full name of a called method
my $package_method = $AUTOLOAD;
# making up the full method name
$AUTOLOAD =~ s/(.*)::(\w+)$/Student::Enroll::$2/;
my $package = $1;
my $method = $2;
if ( _can_access($method) ) {
eval 'use Student::Enroll;'; # loading of the necessary class
&$AUTOLOAD(@_); # calling the method from Student::Enroll class
no warnings 'redefine';
*{$package_method} = \&AUTOLOAD;
} else {
croak "Can't locate object method '$method' via package '$package'";
}
}
package main;
use strict;
use warnings;
my $student = Student->new;
$student->enroll('David');
$student->enroll('David');
$student->enoll('David');
OLD VERSION:
package Student;
use strict;
use warnings;
use v5.14;
sub new { bless {}, shift }
# Subroutine AUTOLOAD will be called always
# when someone calls a method (a subroutine) which
# isn't present in this module (enroll in our case)
sub AUTOLOAD {
no strict 'refs';
our $AUTOLOAD; # in this global variables are kept
# the full name of a called method
eval 'use Student::Enroll;'; # loading of the necessary class
# making up the full method name
$AUTOLOAD =~ s/.*::(\w+)$/Student::Enroll::$1/;
&$AUTOLOAD; # calling the method from Student::Enroll class
# Note: array @_ of args is passed automatically
}
package main;
use strict;
use warnings;
my $student = Student->new;
$student->enroll('David');
Upvotes: 1
Reputation: 954
First of all, I would suggest to start programming in the 'Modern Perl' way and move away from old fashioned Perl5 blessed hashes for object...
use Moose;
Which is a proper OO system for Perl, borrowed from Perl6.
The moment your classes are becoming 'big', it's time to reconsider what actually went into the class and what is not supposed to be there. Some attributes and methods might actually be very general and could be inherited from a super class. But Moose (and Moo) also have 'Roles', behaviour that can be added on top of another class.
Let me show how my 'Student' class looks like:
package College::Student;
use Moose;
extends 'Person';
has 'student_registration_number' => (
is => 'ro',
isa => 'Int',
required => 1,
);
with 'College::Enrolment';
1;
What is happening:
line 01: declare the name of the package, College::Student
, a separate namespace for College related stuff.
line 03: use Moose;
Naturally!
line 05: We are using another class, Person
and use that as a base class. Now, in this case I did create a College::Student
class, a more specific class then a general purpose Person
class (not in the College
namespace either).
line 07: specific for this College::Student
class, only here we use a student_registration_number
, which is a read-only attribute of an integer type and is required.
line 12: with the role College::Enrolment
we want a student to have additional behaviour - so that it probably can enroll to a course and what ever methods and attributes are needed in order to do that, which are of no importance here, all we want to take care of here, is that it can do that.
Now have a brief look inside that 'College::Enrolment' class:
package College::Enrolment;
use Moose::Role;
sub enroll {
my $self = shift; # a person
my $args = {@_};
print
$self->name,
" is being enrolled into: ",
$args->{'course'}->course_title,
"\n",
;
};
sub un_enroll {
};
1;
line 01: a nice namespace, again something to do with Colleges.
line 03: this is a Moose::Role that can be applied to other objects... you can't instantiate these!
line 05: Ah... obviously, there is an enroll
instance method,
line 16: and an un_enroll instance method as well.
For completeness, the base class Person
which looks very basic and no specific things for students.
package Person;
use Moose;
has name => (
is => 'ro',
isa => 'Str',
required => 1,
);
has date_of_birth => (
is => 'ro',
isa => 'DateTime',
required => 1,
);
1;
One could extend this class also for a College::Professor
like:
package College::Professor;
use Moose;
extends 'Person';
has 'employee_number' => (
is => 'ro',
isa => 'Int',
required => 1,
);
with 'College::Enrolment'
1;
Now, a College::Professor
is a Person
however, it does not have the College::Enrolment
role.
If you would like,it's also very easy to have a class for College::GuestStudent
which might not have a registration number, but do need to be able to enroll --- with that same role.
Here kicks in the power of Moose Roles...
instead of creating huge classes, try to split of as many roles you can think of that are logical combinations of methods and their attributes. This makes it way more maintainable - and testable. And soon you'll see that roles are a way more logical way to build classes, instead of trying to do inheritance - or even worse, multiple inheritance.
Oh.. something missing:
use strict;
use warnings;
use College::Student;
use College::Course;
use DateTime::Format::ISO8601;
my $study = College::Course->new(
course_title => "French for beginners"
);
my $pupil = College::Student->new(
name => "John Doe",
date_of_birth => DateTime::Format::ISO8601->parse_datetime("2001-07-10"),
student_registration_number
=> '123456',
);
$pupil->enroll( course => $study );
__END__
and don't forget to read up on Moose at
http://modernperlbooks.com/books/modern_perl_2014/07-object-oriented-perl.html
http://www.theperlreview.com/articles/moose.html
Upvotes: 1
Reputation: 118605
You can use a package qualifier on a sub
declaration
package Student::Enroll;
...
sub foo { ... } # Student::Enroll::foo
sub Student::enroll { ... } # Student::enroll, not Student::Enroll::enroll
...
Of course if the only definition of Student::enroll
in Student/Enroll.pm
, and some program needs the Student::enroll
function, that program will have to load Student/Enroll.pm
and all of the Student::Enroll
package.
Upvotes: 2