Starting OO design
From oorexx
Main_Page contents Week 2 Week 3
Designing an OO application
This is a simple tutorial on starting an OO design. It doesn't deal with formal design methods, it just shows how I would go about designing a simple report class (object design) and then using it to print the two times multiplication table.
If it gets a good response in the rexxLa mailing list I will follow it with Articles refining and expanding the example. Feel free to join in, whether you have questions or you know better than me how to do this.
Please note that the code examples require ooRexx 3.2 (the latest as I write) or above.
Jon
What does a report consist of?
- A title
- A header
- Rows
- A footer
- A suffix perhaps
A class is a factory for creating objects that defines what the object looks like. We could start to create a report class:
::class report ::attribute title ::attribute header ::attribute rows ::attribute footer ::attribute suffix
This gives us a start at containing our data. All of our attributes are string objects, except for the rows, which are going to be a collection of some objects, … lets use an array to contain the rows. As such we need to set that up when the object instance is created.
When you create an object instance the init method is run.
::method init self~rows= .array~new
Because we have declared rows as an attribute of our class, we can set values to it like this. We can also get (query) it’s value from any method within our class like this:
Say self~rows
We can also set or get it’s value from code outside the class like this:
MyReport = .report~new /* make a report instance */ Say MyReport~rows
This would make more sense if rows were a string object. As it is an array object, the string value of the array class will be returned, ie “an Array”
Now we can ask our self what our rows are going to look like. I guess that they are going to consist of a number of fields. These fields are also going to affect the header. So I am going to add a fields attribute to hold the fields definitions and make that an array as well.
::class report ::attribute Title ::attribute header ::attribute rows ::attribute footer ::attribute suffix ::attribute fields ::method init self~rows = .array~new self~fields = .array~new
as you can see, I have separated the ‘=’ from the attribute messages in the init method so that they line up nicely. OoRexx allows this.
Each of our fields is going to be an object in itself. What characteristics will they have?
- A title (for the header row)
- A width
- An alignment (left, right, center)
- A data type (string, integer, decimal)
- A precision (if they are decimal numbers)
- A default value (hunch tells me this will come in handy)
So this sounds like we need another class to cope with this:
::class reportFieldDefinition ::attribute title ::attribute width ::attribute alignment ::attribute type ::attribute precision ::attribute defaultValue
As the only actions we are going to do with object instances of this class are define them and query the attributes it should be fairly straightforward. To save us code setting the attributes individually we can do that in the init method from parameters passed:
::method init use arg title, width, alignment, type, precision, defaultvalue self~title = title self~width = width self~alignment = alignment self~type = type self~precision = precision self~defaultValue = defaultValue
Let’s say we were going to create a report to show the two times table, we would have the following fields
- Multiplier
- ‘*’
- Multiplicand
- ‘=’
- Result
And we might set it up as follows:
MyReport = .report~new
/* set up the field definitions */
f = .reportFieldDefinition~new('Multiplier',10,'right','integer',0)
MyReport~fields~append(f)
f = .reportFieldDefinition~new("",1,'left',string,,'*')
MyReport~fields~append(f)
f = .reportFieldDefinition~new('Multiplicand',12,'right','integer',0)
MyReport~fields~append(f)
f = .reportFieldDefinition~new("",1,'left',string,,'=')
MyReport~fields~append(f)
f = .reportFieldDefinition~new('Result',7,'right','integer',0)
MyReport~fields~append(f)
As MyReport~fields is an array it provides the append method so I don’t need to write that myself – just use it.
In the above example I’ve assigned the reportFieldDefinition objects to variables and then appended them, so that the lines don’t wrap. You can of course do it all in one statement.
Now we have to attend to the actual data in the report. We have defined an array to contain the rows, but what will a row look like?
- Fields
Now lets create a class for a row:
::class reportRow ::attribute fields ::method init self~fields = .array~new
Fields is an array of string objects.
Now we could add the data for the row 2 * 3 = 6 like this:
R = .reportRow~new R~fields[1] = 2 /* field 2 has the default value of ‘*’ */ R~fields[3] = 3 /* field 4 has the default value of ‘=’ */ R~fields[5] = 6 MyReport~Rows~append(r)
Lets see where we are at now. We have a way to define our report and a way to get data in. All that is missing is a way to get the data out.
Outputting the report
This is an action, so it is going to be a method of the Report Object. You might want to direct this to a file or a printer, but we are just going to use say as our output method.
::method Output
say self~title
hString = "" /* construct the header string */
do field over self~fields
parse upper value field~alignment~strip with align 2 .
select
when align = 'L' then hstring = hstring field~title~left(field~width)
when align = 'R' then hstring = hstring field~title~right(field~width)
otherwise
hstring = hstring field~title~center(field~width)
end /* select */
end
self~header = hstring~subStr(2) /* remove extraneous leading blank */
say self~header
do row over self~rows
rowStr = '' /* contruct the row string */
do fieldNo = 1 to row~fields~last /* for all the fields */
/* get the field value or the defaul if not defined */
if row~fields[fieldNo] = .nil
then fieldText = self~fields[fieldNo]~defaultValue
else fieldText = row~fields[fieldNo]
if self~fields[fieldNo]~type~left(1)~translate = 'D' /* decimal */
then fieldText = fieldText~format(,self~fields[fieldNo]~precision)
parse upper value self~fields[fieldNo]~alignment~strip with align 2 .
fieldWidth = self~fields[fieldNo]~width
select
when align = 'L' then rowStr = rowStr fieldText~left(fieldWidth)
when align = 'R' then rowStr = rowStr fieldText~right(fieldWidth)
otherwise
rowStr = rowStr fieldText~center(fieldWidth)
end /* select */
end /* DO */
say rowStr~substr(2) /* remove extraneous leading blank */
end /* DO */
say self~footer
say self~suffix
Purists would say quite correctly that there is too much code in this method and it should be broken down into smaller methods or routines, which call each other, and I agree, but I leave it as it is for demonstrations sake.
Using our classes
We have now written our classes & it only remains to use them:
Here, tidied up a bit is all the code for our two times table report:
MyReport = .report~new
MyReport~title = 'Two times Table'
MyReport~footer = 'Report produced on' date()
MyReport~suffix = ''
/* set up the field definitions */
MyReport~fields~append(.reportFieldDefinition~new('Multiplier',10,'right','integer',0))
MyReport~fields~append(.reportFieldDefinition~new("",1,'left',string,,'*'))
MyReport~fields~append(.reportFieldDefinition~new('Multiplicand',12,'right','integer',0))
MyReport~fields~append(.reportFieldDefinition~new("",1,'left',string,,'='))
MyReport~fields~append(.reportFieldDefinition~new('Result',7,'right','integer',0))
do i = 1 to 10
r = .reportRow~new
r~fields[1] = 2
r~fields[3] = i
r~fields[5] = 2*i
MyReport~Rows~append(r)
end
MyReport~outPut
/* ===================================================================== */
::class report
/* ===================================================================== */
::attribute title
::attribute header
::attribute rows
::attribute footer
::attribute suffix
::attribute fields
/* --------------------------------------------------------------------- */
::method init
/* --------------------------------------------------------------------- */
self~rows = .array~new
self~fields = .array~new
/* --------------------------------------------------------------------- */
::method Output
/* --------------------------------------------------------------------- */
say self~title
hString = "" /* construct the header string */
do field over self~fields
parse upper value field~alignment~strip with align 2 .
select
when align = 'L' then hstring = hstring field~title~left(field~width)
when align = 'R' then hstring = hstring field~title~right(field~width)
otherwise
hstring = hstring field~title~center(field~width)
end /* select */
end
self~header = hstring~subStr(2) /* remove extraneous leading blank */
say self~header
do row over self~rows
rowStr = '' /* contruct the row string */
do fieldNo = 1 to row~fields~last /* for all the fields */
/* get the field value or the defaul if not defined */
if row~fields[fieldNo] = .nil
then fieldText = self~fields[fieldNo]~defaultValue
else fieldText = row~fields[fieldNo]
if self~fields[fieldNo]~type~left(1)~translate = 'D' /* decimal */
then fieldText = fieldText~format(,self~fields[fieldNo]~precision)
parse upper value self~fields[fieldNo]~alignment~strip with align 2 .
fieldWidth = self~fields[fieldNo]~width
select
when align = 'L' then rowStr = rowStr fieldText~left(fieldWidth)
when align = 'L' then rowStr = rowStr fieldText~right(fieldWidth)
otherwise
rowStr = rowStr fieldText~center(fieldWidth)
end /* select */
end /* DO */
say rowStr~substr(2) /* remove extraneous leading blank */
end /* DO */
say self~footer
say self~suffix
/* ===================================================================== */
::class reportFieldDefinition
/* ===================================================================== */
::attribute title
::attribute width
::attribute alignment
::attribute type
::attribute precision
::attribute defaultValue
/* --------------------------------------------------------------------- */
::method init
/* --------------------------------------------------------------------- */
use arg title, width, alignment, type, precision, defaultvalue
self~title = title
self~width = width
self~alignment = alignment
self~type = type
self~precision = precision
self~defaultValue = defaultValue
/* ===================================================================== */
::class reportRow
/* ===================================================================== */
::attribute fields
/* --------------------------------------------------------------------- */
::method init
/* --------------------------------------------------------------------- */
self~fields = .array~new
And here is what the output looks like
Two times Table
Multiplier Multiplicand Result
2 * 1 = 2
2 * 2 = 4
2 * 3 = 6
2 * 4 = 8
2 * 5 = 10
2 * 6 = 12
2 * 7 = 14
2 * 8 = 16
2 * 9 = 18
2 * 10 = 20
Report produced on 6 Jan 2008
What next?
Next time we'll look at strong interfaces, documentation and storing your classes in a library
Week 2
