Your Own PerlNET Component

Most of the components that we will create throughout this book will expose one class. However, there is no limitation on the number of classes that reside inside one component (library). Hence, when discussing classes' definition, we assume that they reside in different components unless we specify otherwise.

Class Interface

Whenever designing a new class, we first think about the way it should be used and we try to make our class users' lives as easy and comfortable as possible. In other words, we think about a convenient interface[4] through which the users will access our class. Interface is a compound notion, and here we will try to divide it into simpler parts and explain each separately. We hope that in this case, a “divide and conquer” technique will turn out to be a fruitful one.

[4] In this chapter we are using the term interface in its general sense of an external specification. In Chapters 12 and 13 we will see that .NET has a special notion of interface.

Class interface usually is composed of the following elements:[5]

[5] Fields are not applicable to Pure Perl types that we describe in this chapter. Hence, we omit the discussion of fields.

  • Properties

  • Fields[5]

  • Methods

  • Constructors

  • Attributes

Properties and methods may belong to two major parts of class interface: visible (public) and invisible (private). In general, a visible part contains properties and methods that may be accessed by class users. An invisible part consists of properties, fields, and methods that are private to our class; that is, access to them is permitted only to other class members. Modifiers, which we will discuss later in this chapter, control the visibility.

As we have stated, .NET is a strongly typed environment. Therefore, when we define methods, we should explicitly specify argument types as well as return types of our methods and types of properties and fields that belong to the class interface.

Typing

There are several often used .NET value and class types for which aliases were created in PerlNET. We can use these aliases in for interface POD (Plain Old Documentation) blocks for defining types of properties, fields, return values of methods, and arguments types. Table 11-1 shows the .NET types and the corresponding aliases in PerlNET.

Table 11-1. PerlNET Aliases for .NET Types
.NET TypePerlNET Alias
Booleanbool
Charchar
Sbytesbyte
Int16short
Int32int
Int64long
Bytebyte
UInt16ushort
UInt32uint
UInt64ulong
Singlefloat
Doubledouble or num
Decimaldecimal
Objectany
Stringstr

As you can see, this table is almost identical to the cast table (Table 10-1) in Chapter 10. However, there are some amendments: The System.Double class has two aliases, and there are two new entries for System.Object and System.String. The value types are the same as in C#.

If you want to use a type that is not listed in the table above, you can always write its fully qualified name. However, if the use namespace pragma was turned on for the namespace where the type of your interest resides, then the unqualified name may be used. For example, if we want to define that our method returns an integer value, we can do it in three different ways:

=for interface
   # using alias
   int SomeMethod();
   # using full-qualified name
   System.Int32 AnotherMethod();
=cut
. . .
# Using unqualified name
use namespace "System";
=for interface
   Int32 YetAnotherMethod();
= cut
. . .

We use the same aliases and types in specifying the type of arguments.

int SomeMethod(str s, System.UInt32 i);

We will see more detailed examples of typing when we discuss methods and properties definitions later in this chapter.

To specify that a return value of an argument is an array, we use the square brackets:

str[] Words(str sentence);

The Words method returns an array of strings.

You may encounter a situation in which your method returns a Perl list and not a .NET array to the .NET client application. In this case, it is useful to convert the Perl list into a .NET array of a correct type, as not all our client applications are able to work with Perl-specific structures. To force the conversion into a .NET array before returning a value, we use wantarray! method modifier.

=for interface
   static wantarray! str[] Words();
=cut
. . .
sub Words
{
   return qw(Car Bicycle Ship);
}

The Words method returns the .NET string array. We will see the use of the wantarray! method modifier when we discuss wrapping existing Perl modules.

Attributes

The first attribute we are going to use is

[interface: pure]

With help of this attribute, we specify that our PerlNET component will be written in the Core Perl manner (Pure Perl type). This chapter is dedicated to Pure Perl components only.

We present additional attributes when we discuss inheritance issues in the next chapter. We use attributes to define interfaces and/or methods with special features. Attributes are placed in square brackets inside for interface POD blocks. When the attribute is relevant to an interface as whole, it is preferable to put such attribute in the first line of the first for interface block for better readability. Attributes that affect a specific method should be placed above that method definition. We will see method attributes usage when we will learn about graphical user interfaces and Windows Forms in Chapter 14.

Constructors

Every .NET class should expose at least one version of a constructor—a special method for initializing class instances (methods are discussed later in this chapter). Inside the for interface blocks, the constructor name should be identical to the name of the class. The new subroutine defines the code for the constructors. We can define more than one constructor. Constructors should differ by their signatures—types and/or quantity of parameters they expect. This technique is called constructor overloading and is widely used in object-oriented languages, such as C++ and Java. Even if we defined several constructors in POD blocks, there remains only one new subroutine. You should examine the parameters inside the body of new and determine which version of constructor was called. In addition, a static modifier should be specified for every constructor, since these are class methods that we call before the new class instance exists. Constructors of Pure Perl classes return class objects, but we may omit the return type for them.

Following are examples of constructors' definitions. First, we show the Car class with a single constructor. The sample is stored in the CarStep1SingleConstructor directory.


#
# Car.pm
#
package OI::Samples::Car;
=for interface
   [interface: pure]
   # Return type specified
   static Car Car(str Model);
=cut
sub new
{
   # Constructor body
   my ($self, $Model) = @_;
   my $s = bless {}, $self;

   print "Car1: Model = ", $Model, "
";
   # Do something with $Model and $s
   return $s;
}

Additionally, we demonstrate constructor overloading with the Car2 class. The code is in the CarStep1TwoConstructors directory.


#
# Car2.pm
#
package OI::Samples::Car2;
=for interface
   [interface: pure]
   # Return types omitted
   static Car2(str Model);
   static Car2(str Model, int Year);
=cut
sub new
{
   # We determine which version of constructor was
   # called
   my ($self, $Model, $Year) = @_;
   my $s = bless {}, $self;

   if (!defined $Year)
   {
      # First version was called
      print "Car2: Model only was passed
";
   }
   else
   {
      # Second version was called
      print "Car2: Model and Year were passed
";
   }
   return $s;
}

Our two classes (Car and Car2) are “dummy” for now, as they do not provide any functionality. However, as we build on these classes, they will grow and may become even “Rolls Royce” classes. We wrote a small client program to illustrate the use of these classes and to show their constructors in action. However, before we can work with our classes, we should compile them and build the components. plc.exe will do the job for us. The command line will differ from those we used to compile into executables, as now our targets are libraries (DLLs) where our classes will reside. In order to compile the Car package, run the command shown below in the command prompt when you are in the same directory where Car.pm is located.

plc Car.pm -target="library"

In the same manner, compile the Car2.pm package.

plc Car2.pm -target="library"

As a result, you built two DLLs, which we will reference in the client program.

You may find the code for our client in the CarCliStep1 directory.


#
# CarCli.pl
#
use namespace "OI.Samples";

# Car class has single constructor
# We pass it Car Model as string
my $c = Car->new("SuperCar");

# Car2 class has two constructors
# We create two objects using two constructors
my $diesel = Car2->new("Diesel");
my $turbo  = Car2->new("Turbo", int(2001));

This simple client program should reference two components (car.dll and car2.dll). Therefore, after we copy these two DLLs into the same directory where our program resides, the command line for building an executable should look as follows:

plc CarCli.pl -reference="car.dll" –reference="car2.dll"

Running CarCli.exe will display the following information:

Car1: Model = SuperCar
Car2: Model only was passed
Car2: Model and Year were passed

OVERLOADING BY TYPE

You should pay special attention to the cases when you wish to overload the constructor only by type of parameters and not by their quantity, as the defined function will not be helpful. To distinguish the types, the PerlNET module offers a typeof function that you may import. This function returns a reference to the Type class object, which is initialized with the type that we passed as a parameter to the function. The Type class resides in the System namespace of .NET Framework and represents the declarations of classes. We may use it to query a class for information about our objects at runtime. To check whether a certain object is an instance of a class, we use the IsInstanceOfType method of the Type class and pass it the reference to the object. For demonstration purposes, let us take the above Car class and change it so it has two constructors, as follows:

static Car(int Model);
static Car(str Model);

We may now construct our Car objects, passing to them the Model parameter as an integer or as a string. We should handle the different versions of the constructor accordingly.

								use PerlNET qw(typeof);
								use namespace "System";
. . .
sub new
{
   my ($self, $Model) = @_;
   my $s = bless {}, $self;

   # We have to find out the type of $Model
   if (typeof("Int32")->IsInstanceOfType($Model))
   {
      print "Integer version
";
   }
   elsif (typeof("String")->IsInstanceOfType($Model))
   {
      print "String version
";
   }
   return $s;
}

We would like to emphasize that for typeof, you cannot use PerlNET aliases of .NET types and you have to supply the exact fully qualified name of the .NET type (or omit namespace if you used the use namespace pragma, as we did in our sample above).

Defining Properties

Properties for .NET classes play the same role as properties for Core Perl classes. Using properties, we may define different states for our class objects. The general syntax for property definition is shown next.

=for interface
   # Optionally other methods/properties
   . . .
   [Access Modifier] <Property Type> PropertyName;
=cut
. . .
# Code for getting/setting property
sub PropertyName
{
   # Handle getting or setting
}
. . .

The access modifier of a property defines its visibility. By default, properties are accessible by all class users. We will explain about different kinds of access modifiers later in this chapter. For now, no access modifier will be applied in our samples. A property type should be one of the .NET-supported types or the PerlNET alias to such a type from Table 11-1. The property name is the name the class user should use to access our property. In addition, a subroutine with the property name should be defined. This subroutine will handle getting and setting operations on our property, as we will explain later.

In the .NET environment, properties are divided into three kinds:

  • Read-only

  • Write-only

  • Read-write

Read-only properties, as their name suggests, may be read and may not be mutated. Write-only may be mutated only. Finally, read-write properties may be read and mutated. Unfortunately, PerlNET has no special syntax for defining the kind of property. However, we can handle this by other means.

When a class interface exposes some property, there has to be a defined subroutine with the same name as property in the body of the Perl module (exactly as in Core Perl modules). After the module is built into a component (DLL), our interface exposes two methods for each defined property, get_XXX and set_XXX, where XXX is the property name. Whenever the property is accessed or mutated, the PerlNET runtime module readdresses the call to the same subroutine with the name of the property in our Perl module. If we read the property, then no arguments besides the package are passed. If we set the property, the new value is passed as an argument in addition to the package. Hence, we can determine by the arguments list whether our property is being read or mutated and act accordingly. In addition, read-only or write-only behavior may be simulated.

Let us enrich the Car class with some properties:

=for interface
   str Model;
   int Year;
   int Age;
=cut

As you can see, our Car class exposes three properties. Model and Year are read-write and Age, which calculates car age (based on Year property), is read-only. Until now, there is no difference in definition of read-write and read-only properties. This difference will be reflected in the corresponding subroutines code. Model and Year are much alike; therefore, we present only the Year subroutine:

sub Year
{
   my($self, $value) = @_;
   # decide if Year is being read or mutated
   if (defined $value)
   {
      # Property is being mutated
      $self->{Year} = $value;
   }
   else
   {
      # Property is being read
      return $self->{Year};
   }
}

The code for the Age property is slightly different, as we want to simulate the read-only behavior:

sub Age
{
   my($self, $value) = @_;
   # decide if Age is being read or mutated
   if (defined $value)
							{
							# Property is being mutated
							# Do nothing as Age is Read-only
							return;
							}
   else
   {
      # Property is being read
      # Calculate number of years from
      # Year (of production) to current year.
      my @cur_time = localtime(time);
      my $cur_year = $cur_time(5) + 1900;

      return $cur_year - $self->{Year};
   }
}

As you notice, when there is an attempt to set the Age property, the call to set_Age is readdressed to sub Age with an additional value parameter, and the subroutine immediately returns and no error is reported at runtime. Therefore, you may add your own error message to indicate to the developer that he or she cannot set the read-only property. The code below will not mutate the Car object.

my $c = "Car"->new("Turbo", 1999);
$c->{Age} = 5; # No effect - we simulate read-only behavior

In the same manner, we may imitate write-only behavior, which is rarely used when defining properties.

Initialization of properties is most likely to happen during the stage of construction. Here is the code for the altered version of the Car class constructors. All the calls to constructors are readdressed to the same subroutine, new.

sub new
{
   # We determine which version of constructor was
   # called
   my ($self, $Model, $Year) = @_;
   my $s = bless {}, $self;

   if (!defined $Year)
   {
      # First version was called
      # Call subroutine Model to set the property
      $s->Model($Model);
   }
   else
   {
      # Second version was called
      # Set both properties
      $s->Model($Model);
							$s->Year($Year);
   }
   return $s;
}

The Age property is not initialized, as we calculate it based on the Year property.

Defining Methods

The ability to define states for class objects by setting certain properties is not enough, since very often our classes should provide some functionality and actually do something. For this purpose, we use methods—special member functions—that implement class functionality based on current values of properties and/or parameters.

Generally, you should use the following syntax to define a method properly:

=for interface
   # Optionally other methods/properties
   . . .
   # Method definition
   [modifiers] <return type> MethodName([args]);
=cut
. . .
# Code for the method
sub MethodName
{
   # Do something useful
}
. . .

We may optionally start our method's definition with modifiers, which tell the compiler and/or .NET environment about special characteristics, such as visibility (access modifiers). If a method is independent of the properties' values, then it is preferable to declare such method as a static or class method (it is done by putting a static modifier in the method definition). Hence, it may be invoked without instantiating a class object. If the method has some dependency on the properties, then it will be nonstatic (without a static modifier). In addition, a static method does not get a package as its first argument, unless it is a constructor. We will meet more modifiers when we discuss the inheritance in .NET in the next chapter.

A return type follows modifiers or starts the line of the definition if no modifiers are applied. It is mandatory to write the return type for each method we define.[6] If the method does not return a value, its return type should be void. After the return type, we write the method name. In brackets we define for each parameter of the method a pair—<arg type>, <arg name>—delimited with commas. Optionally, the method can get no arguments. If this is the case, we leave the brackets empty. The code for the method is defined inside the subroutine with the same name as our method. There may be several versions of the method (method overloading). These versions must differ by their signature.

[6] The only exceptions for this rule are class constructors. We may omit the return type in their definitions.

We handle overloaded versions of methods exactly the same way as we do for constructors, using defined or typeof functions inside the corresponding method subroutine.

For better understanding, we illustrate defining methods on our Car class. We define two nonstatic methods: OutputCarInfo prints formatted information about the car. CalculateCarAge returns the age of the car. This method has two versions: without parameters and with one integer Year parameter. The former version returns the current age of the car—the Age property. The latter version returns the age of the car in the year specified as parameter Year. Calls to these two methods are readdressed to the same CalculateCarAge subroutine; inside that we should decide which version was called by examining the arguments list and act accordingly. In addition, there is one static method, ClassInfo, which outputs help information about the Car class interface.

# Interface definition
=for interface
   void OutputCarInfo();
   int CalculateCarAge();
   int CalculateCarAge(int Year);
   static void ClassInfo(str delim);
=cut
# Output information about the car
sub OutputCarInfo
{
   my $self = shift;
   print "Car model: ", $self->Model(),"
";
   print "Year of production: ";
   print $self->Year(),"
";
   print "End of report
";
}
# Calculate car age – two version in one subroutine
sub CalculateCarAge
{
   my ($self, $year) = @_;
							if (defined $year)
   {
      return $year-$self->Year();
   }
   else
   {
      return $self->Age();
   }
}
# static method – no package as first parameter
sub ClassInfo
{
   my $delim = shift;
   print "Read-Write Properties: ";
   print "Model", $delim, "Year
";
   print "Read-Only Properties: Age
";
   print "Non-static Methods: ";
   print "OutputCarInfo", $delim, "CalculateCarAge
";
   print "Static Methods: ";
   print "ClassInfo
";
   print "End of Report
";
}

Access Modifiers

The object-oriented paradigm offers a concept of information hiding. This means that the information, which is used inside a class only, should not be accessible by the class users. In Core Perl we assumed that class users “play by the rules” and do not access private object parts (through hash reference). In PerlNET this concept is fully implemented, and other .NET programs that are clients of our .NET component will not be able to gather access to the private parts of our class. This became possible by introducing access modifiers for methods definitions:

  • public

  • private

  • protected

If no access modifier is applied for a method or a property, the corresponding class member is assumed to be public—in other words, accessible for everybody.

When we want to hide the class members, we use either private or protected modifiers. If the private modifier appears in a method or a property definition, then the corresponding member is accessible by other class members only. The protected modifier hides the class members, but allows access to the methods in the inherited classes from the base class where the protected method or property was defined. We will see a sample of using protected in Chapter 12 when we discuss inheritance.

We decided to hide the Age property from the users, as it is already accessible indirectly when passing no parameters to the CalculateCarAge method. Therefore, we apply the private modifier to the Age property inside the interface definition. Other code remains intact.


=for interface
   private int Age;
=cut

The full version of the Car class that we developed throughout the chapter is located in CarStep2 under the chapter samples directory.

Car Client Program

The client program, which exploits the functionality of the Car class, resides in CarCliStep2. Here is the client code for your convenience.


#
# CarCli.pl
#
use strict;
use namespace "OI.Samples";
use PerlNET qw(AUTOCALL);

# Use static method and output Car class info
# Delimiter string is passed as parameter
"Car"->ClassInfo(", ");

# Construct Car object BMW
my $bmw = "Car"->new("BMW 327i", 1997);

# Output BMW car information
print "-------------------------------
";
print "Car Info
";
print "--------
";
$bmw->OutputCarInfo();

my ($age_2000, $cur_age);
$age_2000 = $bmw->CalculateCarAge(2000);
$cur_age  = $bmw->CalculateCarAge();
print "Car age in 2000 was: ", $age_2000, "
";
print "Car age now is: ", $cur_age, "
";

# Upgrade car to the newer of the same model
print "Upgrading to 2002...
";
$bmw->{Year} = 2002;
$cur_age  = $bmw->CalculateCarAge();
print "Age of new car is: ", $cur_age, "
";

# Change model of BMW
print "Replacing by 518...
";
$bmw->{Model} = "BMW 518";

# Print upgraded car information
print "-------------------------------
";
print "New Car Info
";
print "------------
";
$bmw->OutputCarInfo();
print "-------------------------------
";

Here is the output:

Read-Write Properties: Model, Year
Read-Only Properties: Age
Non-static Methods: OutputCarInfo, CalculateCarAge
Static Methods: ClassInfo
End of Report
-------------------------------
Car Info
--------
Car model: BMW 327i
Year of production: 1997
End of report
Car age in 2000 was: 3
Car age now is: 5
Upgrading to 2002...
Age of new car is: 0
Replacing by 518...
-------------------------------
New Car Info
------------
Car model: BMW 518
Year of production: 2002
End of report
-------------------------------

StockItem Example

For the sake of variety, we offer in this section another sample of a PerlNET component with full code. This sample is the translation of the C# sample that we presented at the beginning of this chapter. The PerlNET component has the same interface as its C# analogue, so its usage is identical to the C# component.

EXAMPLE CODE

Here we present the code, which is followed by short explanations. After reading and understanding the previous sections of this chapter, you should find this code very easy to deal with. The code is located in the StockPerlNET StockItem.pm file under the chapter samples directory.


#
# StockItem.pm
#
package OI::Samples::StockItem;

use strict;
use PerlNET qw(bool);

# Constructor
=for interface
   [interface: pure]
   static StockItem(int id, str name);
=cut

# Properties
=for interface
   int ID;
   str Name;
=cut

# Method
=for interface
   void OutputItem(bool lines);
=cut

# Constructor definition
sub new
{
   my $self = shift;
   my ($id, $name) = @_;
   my $s = {ID => $id,
            Name => $name};
   bless $s, $self;
   return $s;
}

# ID read-only Property
sub ID
{
   my($self, $value) = @_;
   if (defined $value)
   {
      # We want make ID read-only, so we just return
      return;
   }
   else
   {
      $self->{ID};
   }
}

# Name read-write Property
sub Name
{
   my($self, $value) = @_;
   if (defined $value)
   {
      $self->{Name} = $value;
      return;
   }
   else
   {
      $self->{Name};
   }
}

# OutputItem method
sub OutputItem
{
   my($self, $lines) = @_;
   if (bool($lines))
   {
      print "---------------------------
";
   }
   print $self->{ID}, " ", $self->{Name}, "
";
   if (bool($lines))
   {
      print "---------------------------
";
   }
}

To compile this PerlNET component, run plc in the following form:

plc StockItem.pm –target="library"

StockItem.dll will be created, and we may reference it when compiling the StockItemCli program, which was created to test the C# component at the beginning of this chapter. The output will be identical:

-------------------------
1     item 1
-------------------------
-------------------------
0     item 0
1     item 1
2     item 2
3     item 3
4     item 4
-------------------------
ID of Second Item: 1
Name of Second Item: item 1
New Name of Second Item: Pencil

EXAMPLE DISCUSSION

The code we have just presented for the PerlNET StockItem component may be used by Core Perl programs as easily as any other Perl module. There are several blocks in the code that are necessary for integrating into the .NET environment or that have another meaning when used in .NET. Here we present their .NET interpretation.

package OI::Samples::StockItem;

This line defines StockItem inside the OI.Samples namespace. This means that the fully qualified name for our class will be OI.Samples.StockItem.

You should additionally notice several =for interface POD blocks. To remind you, they are used to expose methods and properties definitions to the .NET environment (class interface definition), and they have no effect when the package is used with Core Perl. These blocks do not have to be divided into several parts—you can write all the definitions in one POD block. We did it for the sake of accuracy and readability of our code.

# Constructor
=for interface
   [interface: pure]
   static StockItem(int id, str name);
=cut

The constructor for the class should have the same name as the class. In the same manner, we defined properties and methods.

# Properties
=for interface
   int ID;
   str Name;
=cut

# Method
=for interface
   void OutputItem(bool lines);
=cut

We define the code for methods and properties getters and setters with the sub keyword, exactly as with Core Perl subroutines.

Using PerlNET Components in C#

We stated before that our PerlNET components can be easily integrated in other .NET programs. These programs may be written in any .NET-compliant language. In this section, we back this statement with a C# client program that works with the Car class, which we developed throughout the chapter. The C# code is simple and quite intuitive.

//
// CarCli.cs
//

// Standard System namespace
using System;
// Our Car class namespace
using OI.Samples;

class CarCli
{
   public static void Main(string[] args)
   {
      // Use static method and output Car class info
      // Delimiter string is passed as parameter
      Car.ClassInfo(", ");

      // Construct Car object BMW
      Car BMW = new Car("BMW 327i", 1997);

      // Output BMW car information
      Console.WriteLine("---------------------------");
      Console.WriteLine("Car Info");
      Console.WriteLine("--------");
      BMW.OutputCarInfo();
      int age_2000, cur_age;
      age_2000 = BMW.CalculateCarAge(2000);
      cur_age  = BMW.CalculateCarAge();
      Console.Write("Car age in 2000 was: {0}
",
                                        age_2000);
      Console.WriteLine("Car age now is: {0}
",
                                        cur_age);

      // Upgrade car to the newer of the same model
      Console.WriteLine("Upgrading to 2002...");
      BMW.Year = 2002;
      cur_age  = BMW.CalculateCarAge();
      Console.Write("Age of new car is: {0}
",
                                        cur_age);
      // Change model of BMW
      Console.WriteLine("Replacing by 518...");
      BMW.Model = "BMW 518";

      // Print upgraded car information
      Console.WriteLine("New Car Info");
      Console.WriteLine("------------");
      BMW.OutputCarInfo();

      Console.WriteLine("---------------------------");
   }
}

To compile this program, use the C# compiler and run the following commands (first to build car.dll and then to build CarCli.exe):

plc Car.pm –target="library"
csc /reference:car.dll CarCli.cs

The output for the C# Car class client is


Read-Write Properties: Model, Year
Read-Only Properties: Age
Non-static Methods: OutputCarInfo, CalculateCarAge
Static Methods: ClassInfo
End of Report
-------------------------------
Car Info
--------
Car model: BMW 327i
Year of production: 1997
End of report
Car age in 2000 was: 3
Car age now is: 5
Upgrading to 2002...
Age of new car is: 0
Replacing by 518...
New Car Info
------------
Car model: BMW 518
Year of production: 2002
End of report
-------------------------------

The code for the C# sample is located in the CarCliCs folder under the chapter samples directory.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.220.140.5