Modules and Object Classes Some Background and Case Study Examples Bill Cowan Metagenix, Inc. www.metagenix.com 919 / 490-0034 bcowan@metagenix.com For any comments on my approach to Perl O-O style, please object politely… Table of Contents Table of Contents .......................................................................................... 2 Background: References .............................................................................. 3 Scalar References ...................................................................................... 3 Array References ....................................................................................... 3 Anonymous Array References: ................................................................... 3 Hash References ....................................................................................... 5 Anonymous Hash References: ................................................................... 5 Background: Perl Modules ............................................................................ 6 Example of Simple Module ......................................................................... 6 Calling Program ......................................................................................... 7 Comments about Sample Module .................................................................. 9 Example of Module with POD ...................................................................... 10 Perl and Classes/Objects ............................................................................. 13 The Basics … ........................................................................................... 13 Example of Simple Class from perltoot ........................................................ 14 How to Represent an Object ........................................................................ 17 Categories for Methods................................................................................ 18 Expanded Example with Inherited Utility Class ............................................ 19 Sample Methods from Utility Class .............................................................. 23 AUTOLOADed Data Assessors ................................................................... 27 Candidates to Consider for Utility Class ....................................................... 29 Productivity Thoughts to Code Classes ....................................................... 30 Background: References Scalar References DB<1> $Scalar = 'abcd' DB<2> $ScalarRef = \$Scalar DB<3> print $ScalarRef SCALAR(0xdef3bc) DB<6> print $Scalar, ' abcd ', ${$ScalarRef} abcd Array References DB<8> @Array = qw( A B C D); DB<9> $ArrayRef = \@Array; DB<10> print $ArrayRef, ' ', join('|', @{$ArrayRef}) ARRAY(0x9b6940) A|B|C|D DB<11> print $ArrayRef, ' ARRAY(0x9b6940) ', join('|', @{$ArrayRef}) A|B|C|D DB<13> print $ArrayRef->[1], ' B ', $ArrayRef->[3] D Anonymous Array References: DB<16> $ArrayRef = ['a', 'b', 'c', 'd'] DB<18> print @{$ArrayRef}, ' abcd ', $ArrayRef->[1] b Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 3 of 30 919 / 490-0090 Doc Reference: perlref Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 4 of 30 919 / 490-0090 Hash References DB<19> %Hash = (A => '1', B => '2') DB<20> $HashRef = \%Hash DB<24> print $HashRef->{A}, ' 1 AB DB<25> print $HashRef->{A}, ' 1 ', keys( %{$HashRef} ) AB DB<26> print $HashRef->{A}, ' 1 ', keys(%$HashRef) ', keys( %$HashRef ) AB Anonymous Hash References: DB<27> $HashRef = {A => '1', B => '2'} DB<29> print $HashRef, ' ', join('|', %{$HashRef}) HASH(0xdeff38) A|1|B|2 DB<30> print $HashRef->{A}, ' 1 ', keys( %{$HashRef} ) AB Note different ways to do de-referencing on the reference variables: %{ $HashRef }, $HashRef->{'A'} Caution: Until you are comfortable with references and de-referencing process, do not start writing classes and objects! Doc Reference: perlref Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 5 of 30 919 / 490-0090 Background: Perl Modules Example of Simple Module # -- MODULE: HandyDandyLib.pm package HandyDandyLib; use strict; use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); use Exporter(); @ISA = qw(Exporter); @EXPORT = qw(Trim Ltrim); @EXPORT_OK = qw(TrimHashValues); functions to export. #-- optional %EXPORT_TAGS = (VARS => [ @EXPORT_OK ]); $VERSION = ''; sub Trim { #-- Parameters: my( $StringParm #-- string to process ) = @_; $StringParm =~ s/^\s+//; $StringParm =~ s/\s+$//; return( $StringParm ); } #-- end of Trim function Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 6 of 30 919 / 490-0090 Example Module Continued. sub TrimHashValues { #-- Parameters: my( $HashRef, #-- in/out: hash ref to trim. )=@_; .... return(1); } #-- end of TrimHashValues function. sub GetPgmRevNbr { #-- Function parameters: my ($RevString, #-- Input revision string. ) = @_; .... return($1); } 1; #-- End of GetPgmRevNbr. #-- end of module Calling Program # Default imported entry points. use HandyDandyLib; # &Trim was in @EXPORT $String = &Trim($Input) . subtr($Name, 1, 5); -----------------# Explicit request to import specific entry point. use HandyDandyLib qw(TrimHashValues); # &TrimHashValues was in @EXPORT_OK. Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 7 of 30 919 / 490-0090 %NewHash = &TrimHashValue(\%InHash); Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 8 of 30 919 / 490-0090 Comments about Sample Module Points about Module: "Package HandyDandyLib" to create separate namespace. "use vars …" as way to declare package standard package globals. "use Exporter;" to handle export entry points into caller's namespace. @EXPORT, @EXPORT_OK to control which entry points to export by default or by request. "sub Trim…" to define each function. Some functions can be exported and other can remain local to this module. "1; #-- end of module" to signal end of this module. Points about Caller's Code Note difference in "use" statement and how that relates to which function names are listed in @EXPORT and @EXPORT_OK. You know that a function is defined in a module (e.g. a .pm file), because you visually verified that function existed. But Perl tells you "undefined" function. Check the @EXPORT and @EXPORT_OK arrays. Also remember that undefined functions are runtime error (not found at compile time). You have forgotten the "use" command. If Perl can not locate the .pm file for the module, then do "perl -V" to check your list of directories being searched for .pm files. Another good trick in the debugger is to do "x \%INC" to display module name and what directory it came from off list of directories. Reference: perlmod, perl, perlsub, perlmodlib Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 9 of 30 919 / 490-0090 Example of Module with POD See Handout Available with Generated HTML Documentation #---------------------------------------------------# MODULE: HandyDandyLib.pm #---------------------------------------------------#---- START of Plain Old Documentation (POD) -------=head2 NAME B<HandyDandyLib> - A function library defined in the HandyDandyLib.pm module. =head2 SYNOPSIS use HandyDandyLib; B<Functions:> Trim -- Trim leading and trailing whitespace from scalar. TrimHashValues -- Trim leading/trailing whitespace from a hash. GetPgmRevNbr -- Return the program revision number. B<Prerequisites:> Perl 5.002 =head2 DESCRIPTION B<HandyDandyLib> provides a library of simple, common utility functions. These functions are application-independent. =head2 SOURCE CONTROL INFORMATION Source file: $Archive: /Perl/Misc/HandyDandyLib.pm $ Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 10 of 30 919 / 490-0090 Last changed by: $Author: Billc $ Date: $Date: 5/13/99 12:02a $ Version: $Revision: 4 $ =head2 DIAGNOSTICS and KNOWN BUGS None. =head2 SEE ALSO None. =head2 AUTHOR Bill Cowan. =cut #-------------- END of Plain Old Documentation (POD) ---------package HandyDandyLib; use strict; <<< SNIP >>> #-------------------------------------------------------------=head1 =head2 C<TrimHashValues> -- Trim leading/trailing whitespace from a hash. $Result = &TrimHashValues(\%HashToTrim); The C<TrimHashValues> function removes leading and trailing spaces from all values in a hash. Returns true (one) on success, and false (zero) on error. Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 11 of 30 919 / 490-0090 Parameters: \%HashToTrim - A reference to the hash to trim. =cut sub TrimHashValues { . . . . Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 12 of 30 919 / 490-0090 Perl and Classes/Objects (Material Heavily Stolen from Perl's Documentation Set) The Basics … 1. An object is simply a reference that happens to know which class it belongs to. 2. A class is simply a Perl package that happens to provide methods to deal with object references. 3. A method is simply a subroutine that expects an object reference as the first argument. For class methods, the package name (e.g. class name) is expected as the first argument. Method invocations for Employee class: Class method using Employee class name, e.g. constructor: $EmpObj = Employee->new($Name, $SSN, $Salary); Object method using object reference $EmpObj: $NewSalary = $EmpObj->SalaryIncrease(0.10); Opinion: There is "indirect syntax" for method calls that I decided to forget several months ago, e.g. "$db = new Win32::ODBC $DSN;". My preferred or standard is "$db = Win32::ODBC->new($DSN;". Reference: perlobj and perltoot have excellent discussions. Suggestions: If you understand references and modules, you can probably spend your time with perltoot and not purchase a book for Perl O-O. My observation is that books usually provide one chapter on how to write classes. I had to pull different things from different books. Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 13 of 30 919 / 490-0090 Example of Simple Class from perltoot What caller's code may look like: use Person; # class name. $him = Person->new(); # constructor as class method $him->name("Jason"); # data assessor to set value $age = $him->age(); # data assessor to get value $him->peers( "Norbert", "Rhys", "Phineas" ); What the module would look like to implement this class: package Person; use strict; sub new { my $Class = shift; # Get class name. my $Obj = {}; # Set object as hash reference. $Obj->{NAME} = undef; $Obj->{AGE} = undef; $Obj->{PEERS} = []; bless $Obj, $Class; # Bless hash ref into class. return $Obj; } sub name { my $Obj = shift; # Get object reference. if (@_) { $Obj->{NAME} = shift } return $Obj->{NAME}; Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 14 of 30 919 / 490-0090 } sub age { my $Obj = shift; if (@_) { $Obj->{AGE} = shift } return $Obj->{AGE}; } sub peers { my $Obj = shift; if (@_) { @{ $Obj->{PEERS} } = @_ } return @{ $Obj->{PEERS} }; } 1; # so the require or use succeeds A faster, but less readable implementation of data assessor method is: sub age { my $Obj = shift; return $Obj->{age} = $_[0] if (@_ > 0); return $Obj->{age}; #-- set. #-- get attribute value. } Still faster, but even less readable implementation is: sub age { my $Obj = shift; return (@_ > 0 ? $Obj->{age} # parm to set value. : $Obj->{age} # no parm; get value. ); } Suggestions: Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 15 of 30 919 / 490-0090 I prefer $Obj as standard convention, instead of $self as is often used. Use $Obj->{age} for public members, $Obj->{ _age} for private members by using the underscore convention. Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 16 of 30 919 / 490-0090 How to Represent an Object Hashes are often used to implement the object, i.e. object reference is a blessed hash reference. Hashes are easy to use for storing public and private members of the object (also called instance variables). This has worked well in all of my development. Nothing requires objects to be implemented as hash references. An object can be any sort of reference so long as its reference has been suitably blessed. That means scalar, array, and code references are also fair game. A scalar would work if the object has only one datum to hold. An array would work for most cases, but makes inheritance a bit dodgy because you have to invent new indices for the derived classes. A code reference may be too exotic; I have not tried that. Suggestion: Good discussion of this in perltoot. Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 17 of 30 919 / 490-0090 Categories for Methods Constructors such as $EmpObj = Employee->new($Name); Often new() method creates object and initializes member values. Other times the constructor just creates an empty object. And uses 2nd method to initialize/allocate info for the object. Depends on your application… Data assessors to get/set public members of the object such as: $Salary = $EmpObj->Salary(); # to get value $NewSalary = $EmpObj->Salary($Salary * 1.1); # 10% increase Sometime data assessor methods are implemented as: $Salary = $EmpObj->get('Salary'); # to get value $NewSalary = $EmpObj->set('Salary', $Salary * 1.1); # to set Opinion: I prefer the first style as being more compact. But main thing is to choose one consistent style to use. Workhorse methods (probably object methods) to perform most of the functions needed by the application. Probably where you do most of the new coding. Support methods such as ErrorMsg(), Debug(), PrintObj() that you tend to find in many classes. This is where you can inherit common support or utility methods from a common base class. That is, utility library class. Destructor such as $EmpObj->DESTROY(); to remove an employee. Note DESTROY method is automatically called by Perl. Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 18 of 30 919 / 490-0090 Expanded Example with Inherited Utility Class #----------------------------------------------------# MODULE: Employee.pm # #----------------------------------------------------..... <<Similar to Module POD>> ..... #----------- END of Plain Old Documentation (POD) ---package Employee; use strict; #-- Note no use of Exporter.pm module. use UtilMethods; # Utility methods to inherit. #-- Must be package globals (not file-scoped 'my'). use vars qw(@ISA $VERSION); @ISA = qw(UtilMethods); #-- Classes to inherit. #----------------------------------------------------# Class Variables # Some as package global variables for access by #INHERITED class methods: use vars qw($DEBUG @_LastErrorListG); $DEBUG = 0; #-- Control debug display. @_LastErrorListG = (); #-- Save last set of msg. Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 19 of 30 919 / 490-0090 #----------------------------------------------------# Declare 'my' Variables with File Scope Here #-- Legal list of user-visible attributes #-- and default values to build part #-- of "instance variables" for each object. my %ExternalAttributesG = ( Attribute1 => 'value', Attribute2 => 'value', ); #-- Internal attributes (not changed by user) #-- and default values to build #-- part of the "instance varibles" for each object. my %InternalAttributesG = (_Attribute3 => 'value'); =head1 METHODS =head2 C<new> -- Constructor Method my $Obj = Employee->new( Attribute1 => 'aaaaa', #-- named parameters. Attribute2 => 'zzzzz', #-- named parameters. ); if (not defined $Obj) { #-- Class method to call if "new" fails. print STDERR Employee->ErrorMsg(), "\n"; } Edit here to describe method. Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 20 of 30 919 / 490-0090 C<Attribute1> -- Explanation of this named parameter. =cut sub new { my $Class = shift; #-- class or method call? $Class = (ref($Class) or $Class or 'Employee'); #-- Create hash-based object w/attr & defaults. #-- More attributes added by_AllocateObject. my $Obj = $Class>_AllocateObject( \%ExternalAttributesG, \%InternalAttributesG); bless $Obj, $Class; $Obj->_PushErrorMsg(undef); Clear old messages. #-- Validate all named parameters. Then replace #-- any object's attr values with NAMED parms. if (not $Obj->_ReplaceAttributes(\@_, 1)) { return undef; } #-- Update other attributes for this object. return $Obj; } #-- end of 'new' method. Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 21 of 30 919 / 490-0090 =head2 C<age> -- Data accessor method $CurrentValue = $Obj->age(); #-- get attribute. $UpdatedValue = $Obj->age($NewValue); #-- set. For either get or set operation, the value of the attribute is returned. =cut sub age { my $Obj = shift; #-- If another parm present, write attr value. #-- Either way, attr value returned. return $Obj->{age} = $_[0] if (@_ > 0); #-- write. return $Obj->{age}; #-- read attribute. } #-- end of get/set age method. 1; #-- end of module Employee.pm; modules must return true. Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 22 of 30 919 / 490-0090 Sample Methods from Utility Class =head2 C<ErrorMsg> -- Retrieve list of error messages previously stored. @Messages = $Obj->ErrorMsg(); #-- list context. $LastMessage = $Obj->ErrorMsg(); # scalar context. @Messages = $Class->ErrorMsg(); #-- Class methods $LastMessage = $Class->ErrorMsg(); .... =cut sub ErrorMsg { my $Obj = shift; #-- Parameters: my( $ConcatenateMsgs #-- Optional boolean. ) = @_; if (ref($Obj)) { #-- Being used as an object method. $ArrayRef = $Obj->{__ErrorMsgList}; } else { #-- No object reference; class method. my $Class = $Obj; #-- Really class! #-- Get last set of errors for this class. no strict 'refs'; $ArrayRef = \@{"${Class}::_LastErrorListG"} ; } Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 23 of 30 919 / 490-0090 ..... } #-- end of ErrorMsg method. =head2 C<_ReplaceAttributes> -- Use named parameters to update object variables for object. $OK = $Obj->_ReplaceAttributes( \@_ ); Validate any named parameter is legal for this class. If all parameters are valid, replace the value for the corresponding object variable (attribute). Return boolean to show valid or invalid parameters. Method will do error return for any invalid parameters found. =cut sub _ReplaceAttributes { my $Obj = shift; .... return($OK); } #-- end of _ReplaceAttributes method. =head2 C<SelfDisplay> -- Display contents of object. $Obj->SelfDisplay($Title); $Obj->SelfDisplay(); #-- suppress title line. $Obj->SelfDisplay($Title, 1); #-- compact display. .... =cut sub SelfDisplay { my $Obj = shift; .... Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 24 of 30 919 / 490-0090 return; } #-- end of SelfDisplay method. Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 25 of 30 919 / 490-0090 =head2 C<Debug> -- Read or write the "debug" setting. $OldValue = $Obj->Debug(); $NewValue = $Obj->Debug($Level); $OldValue = $Class->Debug(); $NewValue = $Class->Debug($Level); =cut sub Debug { my $Obj = shift; .... } #-- end of Debug method. This method will be automatically called when object goes out of scope or is undef-ed. sub DESTROY { my $Obj = shift; $Obj->DebugMsg("Destroying " . ref($Obj) . " class for object $Obj..."); undef $Obj; return; } #-- end of DESTROY method Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 26 of 30 919 / 490-0090 AUTOLOADed Data Assessors =head2 C<Get/Set Attribute>-- Data accessor methods. $CurrentValue = $Obj->Attribute(); $UpdatedValue = $Obj->Attribute($NewValue); =cut sub AUTOLOAD { my $Obj = shift; my $Class = ref($Obj) or confess "DIE: \$Obj '$Obj' not object\n\t"; #-- $AUTOLOAD has "Package::Method" to invoke, #-- Package may also include '::' separators also. my $MethodName = $AUTOLOAD; return if $MethodName =~ m/::DESTROY$/; $MethodName =~ s/^.+:://; Remove package name. #-- Autoload for get/set attribute method. if (exists $Obj->{__Permitted}->{$MethodName} ) { #-- Autoload get/set for object attribute. #-- If another parm, then write attribute. return $Obj->{$MethodName} = $_[0] if (@_ > 0); #-- write attribute. return $Obj->{$MethodName}; # read attribute. } else { #-- Use confess to "die" since this is #--"undefined function" also. confess "DIE: '$MethodName' method undefined"; Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 27 of 30 919 / 490-0090 } } #-- end of AUTOLOAD method. Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 28 of 30 919 / 490-0090 Candidates to Consider for Utility Class 1. Error message methods -- Class or object methods. For caller and internal. And handle list of error messages. 2. Validate access for public members of object to get/set values. Also default values for public and internal members of cless. 3. Diagnostic methods such as setting flags to enable/disable debugging code. Also to print out contents of object on requests (See Data::Dumper.) 4. Display of debug data conditionally based on debug settings. 5. Support easy use of named parameters with default values. 6. Standard initialization of common values to each object, if needed. Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 29 of 30 919 / 490-0090 Productivity Thoughts to Code Classes 1. Use a utility class as a base class in order to inherit common methods such as error handling or debugging. 2. Evaluate CPAN tools to quickly develop classes -- especially data assessors -- such as Class::Struct in perltoot. Class::Struct is useful if you want to use for object as a data structure. See Class::xxxxx on CPAN. 3. Develop template or skeleton files for classes and/or methods to save keystrokes and encourage consistency. Also evaluate editor macros and templates. I am currently playing with Perl template module as tool to generate and fill-in values for methods. 4. Add __END__ to end of .pm file for the module. You can then place test cases there and later enable by removing __END__. Metagenix, Inc. www.metagenix.com Leading Way in Data Migration Page 30 of 30 919 / 490-0090