7.4 Recap of ConventionsWhile 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:
The following example puts all these techniques and conventions into practice. 7.4.1 ExampleConsider 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. Figure 7.1: Object model for computer store exampleAll 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. Example 7.1: Sample Object Implementationpackage 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. |
|