by Unknown user
In the previous installment I wrote a successful unit test that saved and loaded checkbox data. By way of refresher, here's that code:
Code Block |
---|
SaveAndLoadData PROCEDURE (*long addr) ! Declare Procedure
ManytoMany CML_Data_ManyToManyLinks
itemize(),pre()
RightRecordX equate
RightRecordY equate
RightRecordZ equate
end
Persister CML_Data_ManyToManyLinksPersisterForTPS
Filename cstring(500)
CODE
addr = address(UnitTestResult)
BeginUnitTest('SaveAndLoadData')
Persister.SetFilename(Filename)
ManytoMany.Persister &= Persister
ManyToMany.LeftRecordID = 1
ManyToMany.SetLinkTo(RightRecordX)
ManyToMany.SetLinkTo(RightRecordZ)
AssertThat(ManyToMany.IsLinkedTo(RightRecordX),IsEqualTo(true), 'before save, record X should be linked')
AssertThat(ManyToMany.IsLinkedTo(RightRecordY),IsEqualTo(false),'before save, record Y should not be linked')
AssertThat(ManyToMany.IsLinkedTo(RightRecordZ),IsEqualTo(true), 'before save, record Z should be linked')
ManyToMany.Save()
ManyToMany.Reset()
AssertThat(ManyToMany.IsLinkedTo(RightRecordX),IsEqualTo(false),'after reset, record X should not be linked')
AssertThat(ManyToMany.IsLinkedTo(RightRecordY),IsEqualTo(false),'after reset, record Y should not be linked')
AssertThat(ManyToMany.IsLinkedTo(RightRecordZ),IsEqualTo(false),'after reset, record Z should not be linked')
ManytoMany.Load()
AssertThat(ManyToMany.IsLinkedTo(RightRecordX),IsEqualTo(true), 'after load, record X should be linked')
AssertThat(ManyToMany.IsLinkedTo(RightRecordY),IsEqualTo(false),'after load, record Y should not be linked')
AssertThat(ManyToMany.IsLinkedTo(RightRecordZ),IsEqualTo(true), 'after load, record Z should be linked') |
You can ignore the first two lines of code as they're standard setup for the unit test procedure, and required by ClarionTest.
First I assign the Persister object to the ManyToMany object.
Then I set some links to the right-side records and verify that the ManyToMany object correctly maintains those links.
I call ManyToMany.Save(), which as you'll recall from last time checks to see if there's a Persister object, and if there is one it calls that object's Save method.
I then call ManyToMany.Reset() and verify that all the previous associations have been wiped out, so I can be sure that anything I test after a Load() is there because of Load().
And finally I Load() the saved values and verify that ManyToMany now has the expected state.
Now let's take a closer look at this line of code:
ManytoMany.Persister &= Persister
In the ManyToMany class, the Persister property is declared like this:
Persister &CML_Data_ManyToManyLinksPersister
But in my code above, the Persister object isn't of type CML_Data_ManyToManyLinksPersister, it's of type CML_Data_ManyToManyLinksPersisterForTPS. How is it possible to declare a reference variable of one type and then pass it an object of another type?
The answer is in the declaration for CML_Data_ManyToManyLinksPersisterForTPS:
Code Block |
---|
CML_Data_ManyToManyLinksPersisterForTPS Class(CML_Data_ManyToManyLinksPersister),Type,Module('CML_Data_ManyToManyLinksPersisterForTPS.CLW'),Link('CML_Data_ManyToManyLinksPersisterForTPS.CLW',_CML_Classes_LinkMode_),Dll(_CML_Classes_DllMode_)
Filename cstring(500),protected
Construct Procedure()
Destruct Procedure()
Load procedure(long leftRecordID,CML_Data_ManyToManyLinksData linksData),derived
Save procedure(long leftRecordID,CML_Data_ManyToManyLinksData linksData),derived
SetFilename procedure(string filename)
End |
The Class declaration
Class(CML_Data_ManyToManyLinksPersister)
means that CML_Data_ManyToManyLinksPersisterForTPS is derived from CML_Data_ManyToManyLinksPersister, so for all intents and purposes the ManyToMany instance sees the Persister object as an instance of CML_Data_ManyToManyLinksPersister. In fact, when the ManyToMany instance calls the Save() method, it "thinks" (if you'll forgive the anthropomorphism) that it's calling Save() on an instance of CML_Data_ManyToManyLinksPersister.
But in CML_Data_ManyToManyLinksPersister the Save method is just a stub:
Code Block |
---|
CML_Data_ManyToManyLinksPersister.Save procedure(long leftRecordID,CML_Data_ManyToManyLinksData linksData)
code |
The code I really want to execute is in the CML_Data_ManyToManyLinksPersisterForTPS class:
Code Block |
---|
CML_Data_ManyToManyLinksPersisterForTPS.Save procedure(long leftRecordID,CML_Data_ManyToManyLinksData linksData)
x long
code
dbg.write('CML_Data_ManyToManyLinksPersisterForTPS.Save')
! Warning - the following code, although workable, is highly inefficient and will be rewritten
if self.OpenDataFile()
! Clear out any existing
clear(Links:Record)
Links:LeftRecordID = LeftRecordID
set(Links:kLeftRight,Links:kLeftRight)
loop
next(LinksDataFile)
if errorcode() or Links:LeftRecordID <> LeftRecordID then break.
dbg.write('deleting existing LinksDataFile record with Links:LeftRecordID ' & Links:LeftRecordID & ', Links:RightRecordID ' & Links:RightRecordID & ': ' & error())
delete(LinksDataFile)
end
dbg.write('records(LinksData.LinksQ) ' & records(LinksData.LinksQ))
loop x = 1 to records(LinksData.LinksQ)
get(LinksData.LinksQ,x)
clear(Links:Record)
Links:LeftRecordID = LinksData.LinksQ.LeftRecordID
Links:RightRecordID = LinksData.LinksQ.rightRecordID
dbg.write('adding LinksDataFile record with Links:LeftRecordID ' & Links:LeftRecordID & ', Links:RightRecordID ' & Links:RightRecordID & ': ' & error())
add(LinksDataFile)
if errorcode()
dbg.write('Error adding record: ' & error())
else
dbg.write('Success adding record')
end
end
self.CloseDataFile()
end |
Ordinarily, the only way I could get this code to execute would be to change the reference in CML_Data_ManyToManyLinks from
Persister &CML_Data_ManyToManyLinksPersister
to
Persister &CML_Data_ManyToManyLinksPersisterForTPS
That would work but it would have the very undesirable side effect of hard wiring in my TPS persister. If I decided that in one part of my application I wanted a TPS persister and in another I wanted, say, an XML persister, I'd have to make a full copy of my CML_Data_ManyToManyLinks class and adapt it for the XML persister.
Wouldn't it be nice if there was a way to keep the generic reference to CML_Data_ManyToManyLinksPersister yet be able to call the code in any derived class?
Happily there is exactly such a mechanism, and it is called the virtual method.
Virtual methods
The Save and Load methods in CML_Data_ManyToManyLinksPersister both have a ,virtual attribute:
Code Block |
---|
Load procedure(long leftRecordID,CML_Data_ManyToManyLinksData linksData),virtual
Save procedure(long leftRecordID,CML_Data_ManyToManyLinksData linksData),virtual |
When the virtual attribute is present on any method in the class the compiler creates something called a virtual method table (VMT) for that class.
If a derived class has a method with the same name and prototype as a virtual method in the parent class, and that method has the virtual attribute, then any time the parent method is called the runtime library consults the VMT to see if there is a derived method that should be called instead.
Virtual methods are one of the most powerful and compelling features of object-oriented programming. They allow you to selectively replace code in a parent class with your own code. Among other things, they are the mechanism by which most embed code is inserted into ABC applications.
Derived virtual methods
To guarantee that your virtual methods are called in place of the parent class methods they must have the same name and prototype as the parent class methods. It's easy to make a mistake in a derived class and have a subtly different method signature. This could be the result of a typo, or because you made a change to the parent method and forgot to change the derived class method. What you end up with then is a derived virtual method that isn't derived at all; instead it's a new "top level" method that just happens to have a virtual attribute, but which will never get called in place of the method in the parent class.
To avoid this problem always use
,derived
rather than
,virtual
when declaring virtual methods in the derived classes. That tells the compiler that it must find a matching virtual method in the parent class. If there's a mismatch you'll get a "DERIVED procedure does not match parent prototype" error and you'll know something needs fixing.
Virtual methods are so incredibly useful that some developers argue that all methods should be virtual by default. In Java, for instance, this is true of all non-static methods. In C# (as in Clarion) you have to explicitly declare methods as virtual.
A blunt instrument
The Save method as written is something of a blunt instrument; it simply wipes out any existing data then adds in the new data. It will come in for some refinement a little later in the process.
The Load method is a simple bit of code that loads values from the file into the data queue.
Wiring the unit-testable class into the UI example program
Unit testing can be incredibly useful, but sometimes wiring unit-tested code into a working program reveals new issues that need to be addressed. I'll cover those next time.