While Perl allows us infinite flexibility in how we organize our modules, we choose to stick to the particular set of conventions introduced in this chapter so that everyone deals with modules in a consistent fashion. Let us quickly summarize these conventions:
A module must be present in its own file called < module>.pm . (Remember that the last executing global statement must return 1 to signify successful loading.)
All subroutines in a module should be designed as methods. That is, they should expect either the name of a class or an object reference as their first parameter. For added convenience, they should be able to deal with either.
Package names should never be hardcoded. You must always use the package name obtained as the first argument to supply to bless . This enables a constructor to be inherited.
Always provide accessor methods for class and instance attributes.
The following example puts all these techniques and conventions into practice.
Consider a store that sells computers and individual components. Each component has a model number, a price, and a rebate. A customer can buy individual components, but can also put together a custom computer with specific components. The store adds a sales tax to the final price. The objective of this example is to provide the net price on any item you can buy from the store.
We need to account for the facts that a part may consist of other parts, that the sales tax may depend on the type of part and the customer's location, and that we may have to charge for labor to assemble a computer
One useful technique for jump-starting a design is to use case analysis , as propounded by Ivar Jacobson [ 19 ]. You look at the interface from the point of view of the user, without worrying about specific objects' attributes. That way, we can understand the objects' interface without worrying about implementation details. Let's say this is how we want to use the system:
$cdrom = new CDROM ("Toshiba 5602"); $monitor = new Monitor ("Viewsonic 15GS"); print $monitor->net_price(); $computer = new Computer($monitor, $cdrom); print $computer->net_price();
Figure 7.1 shows one way of designing the object model. I have used Rumbaugh's OMT (Object Modeling Technique) notation to depict classes, inheritance hierarchies, and associations between classes. The triangle indicates an is-a relationship, and the line with the 1+ indicates a one-to-many relationship. The computer is-a store item and contains other components (has-a relationship). A CD-ROM or monitor is a component, which in turn is a store item.
All attributes common to all store items are captured in the StoreItem class. To compute the net price of any component, we have to take rebate and sales tax into account. But when assembling components to build a computer, we have to add sales tax only at the end; we can't simply add up all the components' net prices. For this reason, we split the calculation into two: price , which subtracts the rebate from the price, and net_price , which adds on the sales tax. At present, the component classes are empty classes, because their entire functionality is captured by StoreItem . Clearly, if the problem stopped here, this design would be unnecessarily complex; we could have simply had one lookup table for prices and rebates and one function to calculate the prices. But we are designing for change here. We expect it to get fleshed out when we start accounting for taxes by location, dealing with components containing other components, and charging for labor. It is best to adopt a generalized mentality from the very beginning.
The Computer class does not use its price attribute; instead, it adds up the prices of its constituent components. It doesn't need to override the net_price functionality, because that function simply adds the sales tax onto an object's price, regardless of the type of the object.
Example 7.1 gives a translation of the object model into code.
package StoreItem ; my $_sales_tax = 8.5; # 8.5% added to all components's post rebate price sub new { my ($pkg, $name, $price, $rebate) = @_; bless { # Attributes are marked with a leading underscore, to signify that # they are private (just a convention) _name => $name, _price => $price, _rebate => $rebate }, $pkg; } # Accessor methods sub sales_tax {shift; @_ ? $_sales_tax = shift : $_sales_tax} sub name {my $obj = shift; @_ ? $obj->{_name} = shift : $obj->{_name}} sub rebate {my $obj = shift; @_ ? $obj->{_rebate} = shift : $obj->{_rebate}} sub price {my $obj = shift; @_ ? $obj->{_price} = shift : $obj->{_price} - $obj->_rebate} } sub net_price { my $obj = shift; return $obj->price * (1 + $obj->sales_tax / 100); } #-------------------------------------------------------------------------- package Component; @ISA = qw(StoreItem); #-------------------------------------------------------------------------- package Monitor; @ISA = qw(Component); # Hard-code prices and rebates for now sub new { $pkg = shift; $pkg->SUPER::new("Monitor", 400, 15)} #-------------------------------------------------------------------------- package CDROM; @ISA = qw(Component); sub new { $pkg = shift; $pkg->SUPER::new("CDROM", 200, 5)} #-------------------------------------------------------------------------- package Computer; @ISA = qw(StoreItem); sub new { my $pkg = shift; my $obj = $pkg->SUPER::new("Computer", 0, 0); # Dummy value for price $obj->{_components} = []; # list of components $obj->components(@_); $obj; } # Accessors sub components { my $obj = shift; @_ ? push (@{$obj->{_components}}, @_) : @{$obj->{_components}}; } sub price { my $obj = shift; my $price = 0; my $component; foreach $component ($obj->components()) { $price += $component->price(); } $price; }
The design for change philosophy is in evidence here. All instance variables get accessor methods, which makes it possible for us to override price() in the Computer class. The Computer::components accessor method can now be changed at a later date to check for compatibility of different components. Even the package global variable $sales_tax is retrieved through an accessor method, because we expect that different components may later on get different sales taxes, so we ask the object for the sales tax.
Notice also that the constructors use SUPER to access their super classes' new routines. This way, if you create a Component::new tomorrow, none of the other packages need to be changed. StoreItem::new blesses the object into a package given to it; it does not hardcode its own package name.
If you put these packages into different files, recall from Chapter 6, Modules , that the files should have the <package name>.pm naming convention. In addition, they should have a 1; or return 1; as the last executing statement.
Copyright © 2001 O'Reilly & Associates. All rights reserved.