We've noticed in tech support that customer interest seems to move in waves. One week it seems that everyone wants to know about data grids, then the next week everyone wants to know about open query files. It's almost like our customers have a secret meeting on Friday afternoon and then spring the new topic on us all at once the following Monday.
Lately, I've noticed the Friday meeting must have been about using AVR for .NET's OO capabilities to add power and sophistication to their programs. I think one of the reasons so many customers are riding this wave is the number of AVR Classic customers moving to AVR for .NET. The advent of Windows 10, and the realization that COM won't last forever, has probably provided some motivation for this.
We have a several-years-old AVR class workbook that has two important chapters on using classes with AVR. Chapters 2 and 5 are required reading for anyone moving to AVR for .NET from any procedural language. That's the good news. The bad news is that while the material isn't really technically dated, it needs to be on the Web and it needs a bit of a refresher (it has some typos and its original format doesn't lend itself well to conversion to HTML). So, that's what we're starting here. This is the first in a new series of articles about using classes and some OO concepts with AVR. Some of these articles will be featured in the ASNA newsletter, but they'll also be posted as regular content in our new knowledge base. Keep an eye on that section of ASNA.com for more articles in this series.
The video version
For better control and resolution view the video directly at YouTube
The text version
Unlike AVR Classic or green-screen RPG, AVR for .NET is built on an object oriented foundation. In AVR for .NET (for the rest of this article "AVR" refers to AVR for .NET), classes define a "program boundary". That is, variable scope (which includes disk files and their record formats) is contained within a class--unless you specifically declare a variable public.
In .NET, some classes present themselves implicitly. For example, in Win or Web forms, each form gets its own class (often called the "code-behind" or "code-beside" class). Think of these classes as structural. You can create a pretty good AVR app using nothing but these implicit structural classes and procedural code. However, for non-trivial applications, putting all of your code into these structural classes makes a challenging, hard-to-test and hard-to-maintain application. You end up with a boar's nest of subroutines, event handlers, and virtually no distinction between business logic, file IO, and UI management.
One of the things many AVR coders could do to improve their applications is to use more classes to separate, or partition, distinct parts of the app from each other. We'll dig more into the motivations for partitioning in a moment, but first, let's have a class refresher course. In academic parlance, a class is a template (or blueprint) for an instance of an object. The class provides data and, usually, actions to perform on that data. It "encapsulates" that data and those actions from the rest of the application. This separation of concerns improves maintenance, reuse, and testing by imposing a strict boundary between the class and the rest of the app (its other classes). The only data and/or actions that a class makes available to other classes are those members that you explicitly expose. Let's look at a small example.
A simple class
Figure 1a below shows a very simple class with two public properties (
Returns) and one private field (
Returns values are available to the outside world; the
Rank value is only visible inside an instance of the class. The
Access keyword controls member visibility.
*Public members are available outside the class instance,
*Private members are not. You'll often see using private members referred to as data hiding. While that might sound like a bad thing, it's a good thing. Private members do work inside the class and their values don't need, and shouldn't be, visible or changeable outside the class.
BegClass SalesData Access(*Public) DclProp Sales Type(*Packed) Len(12,2) Access(*Public) DclProp Returns Type(*Packed) Len(12,2) Access(*Public) DclFld Rank Type(*Integer4) Access(*Private) EndClass
Figure 1a. A small class with data members only.
In AVR Classic or green-screen RPG, you would have probably used a multi-occurrence data structure or a data structure array to store values. As you'll see, using a class, rather than these old-school storage techniques, adds considerable possibilities.
The code below in Figure 1b shows how to use an instance of the SalesData class. Note the word "instance." That's important. Your code doesn't work with the SalesData class, rather it works with an instance of it. The SalesData class is an ethereal definition of data (and, optionally actions) and it doesn't contain data. An instance of it does.
DclFld sd Type(SalesData) New() sd.Sales = 45 sd.Returns = -17
Figure 1b. A small class with data members only.
In the code in Figure 1b, the instance of
SalesData is named
New after its declaration causes an instance of
SalesData to be created in memory. That instance is assigned to
sd. Access to a class instance's public members is available through a fully qualified reference; ie,
sd.Sales allows access to this instance's
Sales member. For all intents and purposes, what we've created here is a special case data structure. As written it doesn't seem to offer much of a benefit over a traditional data structure.
Passing class instances
A benefit of a class instance over a traditional RPG data structure is that class instances can be passed as arguments to subroutines and functions. This allows you to dispatch the global nature of a traditional data structure. Before we dig into passing a class instance as an argument, let's review passing scalar numeric types.
By default AVR for .NET passes arguments to functions and subroutines by value. This ensures that when you pass an AVR intrinsic scalar value type (*Integer4, *Zoned, *Packed, etc) changes made to passed arguments are not seen by the caller. For example:
DclFld y Type(*Integer4) ... y = 55 MySubr(y) // y is still 55 after the call. ... BegSr MySubr DclSrParm x Type(*Integer4) x = 5 EndSr
Value types (which, with one exception are all numeric) directly contain a value. Instances of classes you create with AVR are reference types, where the instance variable contains a reference to the class instance. For example, in Figure 1c below, the
sd variable contains a reference to an instance of
SalesData. Surprisingly, when you pass the
sd instance by value, changes made in the called routine are reflected in the caller. Notice how
sd.Sales is 450 after the call to
TestByVal. What you're passing in this case is a reference to the class, not a direct value or values.
DclFld sd Type(SalesData) New() sd.Sales = 45 sd.Returns = -17 TestByVal(sd) // sd.sales is now 450. BegSr TestByVal DclSrParm sd Type(SalesData) sd.Sales = 450 EndSr
Further, if you pass
sd by reference, changes made in the called routine
are also reflected in the caller. For example, in the code below,
sd.Sales is 450 after the call to
DclFld sd Type(SalesData) New() sd.Sales = 45 sd.Returns = -17 TestByVal(*ByRef sd) // sd.sales is now 450. BegSr TestByRef DclSrParm sd Type(SalesData) By(*Reference) sd.Sales = 450 EndSr
The full reason for this behavior is beyond the scope of this article, but is related to how value types and reference types are stored in the stack and the heap. Read this article for more info. But don't agonize over it very much, just understand the implications.
The notion that members in a class are exposed as you pass a class instance around is both a good thing and a bad thing. It's a good thing because you can pass around a class instance with lots of data in it effectively because you are simply passing a reference to the class instance around. However, as your programming antennae probably tells you, it's also a potentially bad thing because any routine to which you pass the class instance has the right to change public values in that class instance.
The upshot? Code your class member visibility carefully, making sure to only make public those members that truly need to be public. Remember, too, that AVR arrays are reference types, so this warning applies to them, too.
Note also that while strings in .NET are reference types, they are exempt from the exposure just discussed. AVR's
*Char is implemented as a System.String. System.String is a reference type, but it is a special-case reference type. Most reference types have to be explicitly instanced and most reference types can't be passed by value;
*Char and its kissing cousin
System.String are reference types that obey value type semantics. That is, they don't need to be explicitly instanced and they can be passed by value.
Methods give classes actions
SalesData class is shown in Figure 1c with an added member,
GetNetSales(). This is a function that returns the sum of
BegClass SalesData Access(*Public) DclProp Sales Type(*Packed) Len(12,2) Access(*Public) DclProp Returns Type(*Packed) Len(12,2) Access(*Public) DclFld Rank Type(*Integer4) Access(*Private) BegFunc GetNetSales Type(*Packed) Len(12,2) Access(*Public) LeaveSr *This.Sales + *This.Returns EndFunc EndClass
Figure 1d below shows
GetNetSales() being called to return the net sales. While this calculation is very simple, in the real world it represents a complex calculation. With this computation power added to the
SalesData class, that class is now much more powerful than a traditional data structure. In this case, it is a data structure smart enough to total itself. Not a ton of smarts, for sure, but 100% more than any old-school data structure has!
DclFld sd Type(SalesData) New() DclFld NetSales Type(*Packed) Len(12,2) sd.Sales = 45 sd.Returns = -17 NetSales = sd.GetNetSales()
Enough for now
That's plenty to absorb right now. As we move forward, the article in this series will focus on delivering something tangible with AVR and classes. File IO is one place where a great payoff is available by the effective partitioning of your code into classes. Check back soon for part 2.