Guru: Abstract Data Types and RPG
November 8, 2021 Ted Holt
An abstract data type (ADT) is a type of data and a set of operations defined over that type of data. Using ADTs allows a programmer to work with data in terms of functionality rather than physical representation. The ADT is the basis of object-oriented programming. Does that mean that abstract data types don’t apply to procedural languages like RPG? Not at all. Quite the contrary.
Before I show you how you can use abstract data types in RPG-based applications, let me further illustrate abstract data types with another, non-OO object — the user profile. The user profile is a type of data. It has attributes — the profile name, a password, creation date, status, current library, etc. There are a limited set of operations that you can perform on a user profile. You can create one, delete one, change one, dump one, and more. For a full list of what you can do, run GO CMDUSRPRF from an IBM i command line.
Do you know how user profiles are stored in the system? I don’t, and what’s more, I don’t care. It doesn’t matter.
Are user profiles stored exactly the way they were when the first AS/400 was shipped? Probably not, but that doesn’t matter either, because the interface still works as it did back then.
Since IBM i is an object-based system, what I’ve just said applies to other types of objects — programs, files, data queues, data areas, message queues, etc., etc., etc. Every object type has defined attributes and operations.
Now consider application programming. Whatever your organization does, it has objects. For example, every organization has people who use their services. What those people are called varies. Customers, clients, patients, consumers, citizens, taxpayers, whatever.
There is a limited set of operations that can be carried out with customer data. Create a customer. Remove a customer from the database. Change the customer’s attributes (fields). Suspend a customer. Therefore, a customer can be represented in a computer by means of an abstract data type.
How do we do that? Easily! I’m going to use inventory as an example. For purposes of this illustration, I define inventory as goods that are stored in a warehouse.
What can we do with inventory? Lots of things. Here are just a few.
- Receive: Someone ships something to us and we store it in our warehouse.
- Return: We can ship something back to the person or organization we bought it from.
- Transfer: We can move inventory from one place in a warehouse to another place in the same warehouse or to a different warehouse.
- Ship: We can send something to a customer.
These “lots of things” are called transactions, and there are a limited number of transactions that we can carry out with inventory. In other words, we have a type of data and a set of operations. We have an abstract data type.
The first thing to do is to describe the data and the operations defined over that data. That’s easily done in a copybook. Here’s member INVENTORY in source physical file PROTOTYPES.
**free dcl-s InventoryStatus_t char(2) template; dcl-c INV_ALL_OK '00'; dcl-c INV_ITEM_NOT_FOUND '01'; dcl-c INV_INVALID_LOCATION '02'; dcl-c INV_INVALID_ITEM '03'; dcl-c INV_MISC_ERROR '99'; dcl-s ItemNumber_t packed(5) template; dcl-s Quantity_t packed(5) template; dcl-s Warehouse_t packed(3) template; dcl-s Aisle_t packed(3) template; dcl-s Bay_t packed(3) template; dcl-s Level_t packed(1) template; dcl-ds InventoryRecord_t qualified template; ItemNumber like(ItemNumber_t); Warehouse like(Warehouse_t); Aisle like(Aisle_t); Bay like(Bay_t); Level like(Level_t); Quantity like(Quantity_t); end-ds InventoryRecord_t; dcl-pr AdjustItem; ouStatus like(InventoryStatus_t); inItemNumber like(ItemNumber_t) const; inWarehouse like(Warehouse_t) const; inAisle like(Aisle_t) const; inBay like(Bay_t) const; inLevel like(Level_t) const; inQuantity like(Quantity_t) const; end-pr AdjustItem; dcl-pr MoveItem; ouStatus like(InventoryStatus_t); inItemNumber like(ItemNumber_t) const; inFromWarehouse like(Warehouse_t) const; inFromAisle like(Aisle_t) const; inFromBay like(Bay_t) const; inFromLevel like(Level_t) const; inToWarehouse like(Warehouse_t) const; inToAisle like(Aisle_t) const; inToBay like(Bay_t) const; inToLevel like(Level_t) const; inQuantity like(Quantity_t) const; end-pr MoveItem;
The copybook begins with the definition of a status code. This is an enumerated data type that the routines use to report the success or failure of a transaction. This is not an attribute of the inventory data type.
After that come the descriptions of the attributes, of which there are three: the item identifier, the location of the item in the warehouse, and how many of that item are stored at that location. Be aware that the definitions of these attributes do not necessarily match the definitions of the columns in the tables (fields in the physical files).
Everything to this point is defined as a template, which means that you cannot store data in these variables. Instead, you must use the LIKE keyword to define working variables. I have ended the name of each variable with _t, to indicate that they are template code.
I defined a record type, Inventory_t, even though I didn’t use it in the two subprocedures. This type of structure is useful for passing a lot of attributes without defining a lot of parameters in the procedure interface.
After the descriptions come the procedure prototypes for the service program. For this short example, I only defined two inventory transactions — adjustment and transfer. An inventory adjustment just plugs a value into the on-hand quantity. For example, the computer might say that we have a dozen of something, but when we walk into the warehouse and count them, we find we only have 10. We can use the adjustment transaction to correct the database. The other transaction is the transfer. I prefer the simpler term move.
We need one other source member in which to put the calculations that carry out the operations. This is member INVENTORY in source physical file QRPGLESRC.
**free ctl-opt nomain option(*srcstmt: *nodebugio); /include prototypes,inventory dcl-s DB_ItemNumber_t char(15) template; dcl-s DB_Location_t char(16) template; dcl-s DB_Quantity_t packed(11: 3) template; dcl-proc AdjustItem export; dcl-pi *n; ouStatus like(InventoryStatus_t); inItemNumber like(ItemNumber_t) const; inWarehouse like(Warehouse_t) const; inAisle like(Aisle_t) const; inBay like(Bay_t) const; inLevel like(Level_t) const; inQuantity like(Quantity_t) const; end-pi; dcl-s Quantity like(DB_Quantity_t); dcl-s ItemNumber like(DB_ItemNumber_t); dcl-s Warehouse like(Warehouse_t); dcl-s Aisle like(Aisle_t); dcl-s Bay like(Bay_t); dcl-s Level like(Level_t); dcl-s Location like(DB_Location_t); ouStatus = INV_ALL_OK; EncodeLocation (Location: inWarehouse: inAisle: inBay: inLevel); Quantity = inQuantity; ItemNumber = %editc(inItemNumber: 'X'); exec sql update inventory set Qty = :Quantity where Item = :ItemNumber and Loc = :Location; select; when SqlState = '02000'; ouStatus = INV_ITEM_NOT_FOUND; when SqlState > '02000'; ouStatus = INV_MISC_ERROR; endsl; end-proc AdjustItem; dcl-proc MoveItem export; dcl-pi *n; ouStatus like(InventoryStatus_t); inItemNumber like(ItemNumber_t) const; inFromWarehouse like(Warehouse_t) const; inFromAisle like(Aisle_t) const; inFromBay like(Bay_t) const; inFromLevel like(Level_t) const; inToWarehouse like(Warehouse_t) const; inToAisle like(Aisle_t) const; inToBay like(Bay_t) const; inToLevel like(Level_t) const; inQuantity like(Quantity_t) const; end-pi; dcl-s Quantity like(DB_Quantity_t); dcl-s ItemNumber like(DB_ItemNumber_t); dcl-s FromLocation like(DB_Location_t); dcl-s ToLocation like(DB_Location_t); ouStatus = INV_ALL_OK; Quantity = inQuantity; ItemNumber = %editc(inItemNumber: 'X'); EncodeLocation (FromLocation: inFromWarehouse: inFromAisle: inFromBay: inFromLevel); EncodeLocation (ToLocation: inToWarehouse: inToAisle: inToBay: inToLevel); exec sql update inventory set Qty = Qty - :Quantity where Item = :ItemNumber and Loc = :FromLocation; // fixme -- check SQLSTATE, set ouStatus if error exec sql update inventory set Qty = Qty + :Quantity where Item = :ItemNumber and Loc = :ToLocation; // fixme -- check SQLSTATE, set ouStatus if error end-proc MoveItem; dcl-proc EncodeLocation; dcl-pi *n; ouLocation like(DB_Location_t); inWarehouse like(Warehouse_t) const; inAisle like(Aisle_t) const; inBay like(Bay_t) const; inLevel like(Level_t) const; end-pi; ouLocation = %editc(inWarehouse : 'X') + %editc(inAisle : 'X') + %editc(inBay : 'X') + %editc(inLevel : 'X'); end-proc EncodeLocation;
This is not robust code. I threw together just enough to make my example work. A real application would also have to have appropriate error checking and commitment control at the very least.
This module is where all the inventory transactions would be defined. If this were a real production application, this would be compiled into a module, from which you would create a service program, to which all programs that need to carry out inventory transactions would bind. That is, if a dozen programs need to move inventory, all dozen would call the MoveItem routine. None of them would directly interact with the INVENTORY table.
Should I repeat that? If a dozen programs need to move inventory, all of them would call the MoveItem routine. None of them would directly interact with the INVENTORY table.
Here’s an example call to the adjustment routine.
/include prototypes,inventory dcl-s Status like(InventoryStatus_t); AdjustItem (Status: Item: Whs: Aisle: Bay: Lvl: NewQty);
I want to point out something else that is important. I said above that the data types of the attributes do not have to match the data definitions in the database. When building this contrived example, I went to extra trouble to emphasize that point. Here’s a comparison of the data definitions.
This is where the abstract part comes in. How we perceive the data matters. How the data is stored in the database is irrelevant for transaction processing. Ideally, we could change the service program to use a different table, and the programs that bound to these subprocedures would never know the difference.
What about non-transaction processing, such as querying the database? That is a separate process and doesn’t fit into the abstract data type. When querying the tables, you would have to work with the raw data, rather than the data as defined in the service program.
Or would you?
Not necessarily. You can define SQL views that let you perceive the data any way you like. That’s beyond the scope of this article, and Paul Tuohy has already done a good job explaining how that works.
RPG gets plenty of bad press because various forms of it have been around a long time, and I’m not opposed to newer and better things, including programming languages. But I find that RPG can do more than it’s given credit for. Using abstract data types can make a huge difference in building software applications, and RPG can handle them just as well as any other programming language can.