In Part 6 I began working on the data side of the checkbox code and created a class to manage the links between "left" and "right" records. I also loaded up some data into an instance of the class.
To make that data available to the checkbox class I use two method calls:
Code Block |
---|
ListCheckbox.Initialize(CheckboxQueue,CheckboxQueue.CheckedIcon,?List,,links)
ListCheckbox.LoadCheckboxData() |
The first is an overloaded version of the original Initialize method that assigns the passed CML_Data_ManyToManyLinks instance to the CML_UI_ListCheckbox.ManyToManyLinks property, for future use.
That future use happens in LoadCheckboxData:
Code Block |
---|
if not self.ManyToManyLinks &= null
loop x = 1 to records(self.ListQ)
get(self.ListQ,x)
if self.ManyToManyLinks.IsLinkedTo(x)
self.ListQIconField = CML_UI_ListCheckbox_TrueValue
dbg.write(x & ' true')
else
self.ListQIconField = CML_UI_ListCheckbox_FalseValue
dbg.write(x & ' false')
end
put(self.ListQ)
end
end |
So far so good. But in my UITest program I'm manually adding data to the links object, and I'm not saving that data anywhere so it can be reloaded.
I could just add some more methods to the CML_Data_ManyToManyLinks class to load and save data, but by now you know that would be an extremely bad idea. CML_Data_ManyToManyLinks has but one job, and that's two manage the links between left and right records. Loading and saving data, commonly called persisting data, is a different job.
A data persister
I'll call my persister class CML_Data_ManyToManyLinksPersister, and I'll give it some preliminary methods:
Code Block |
---|
CML_Data_ManyToManyLinksPersister Class,Type,Module('CML_Data_ManyToManyLinksPersister.CLW'),Link('CML_Data_ManyToManyLinksPersister.CLW',_CML_Classes_LinkMode_),Dll(_CML_Classes_DllMode_)
Construct Procedure()
Destruct Procedure()
Load procedure()
Save procedure()
End |
Aside from the gnawing uncertainty over whether the word is persister or persistor, the most pressing issue with a data persister class is how to call it.
It could be that the user initiates some action to load or save data, in which case it seems appropriate to have Load and Save methods in the CML_UI_ListCheckbox class. But CML_Data_ManyToManyLinks can live independently of any UI code, as the unit tests demonstrate. So even though the UI class may at some point grow these methods, I've decided that if that happens they'll simply be wrappers for calls to the CML_Data_ManyToManyLinks.Load and CML_Data_ManyToManyLinks.Save methods, both of which will look to CML_Data_ManyToManyLinksPersister to do the actual work of loading and saving.
Here's my updated CML_Data_ManyToManyLinks definition; note the new Include, the Persister property and the Load and Save methods:
Code Block |
---|
include('CML_IncludeInAllClassHeaderFiles.inc'),once
Include('CML_Data_ManyToManyLinksPersister.inc'),Once
CML_Data_ManyToManyLinks_DataQ queue,TYPE
LeftRecordID long
RightRecordID long
end
CML_Data_ManyToManyLinks Class,Type,Module('CML_Data_ManyToManyLinks.CLW'),Link('CML_Data_ManyToManyLinks.CLW',_CML_Classes_LinkMode_),Dll(_CML_Classes_DllMode_)
LinksQ &CML_Data_ManyToManyLinks_DataQ
LeftRecordID long
Persister &CML_Data_ManyToManyLinksPersister
Construct Procedure()
Destruct Procedure()
IsLinkBetween procedure(long leftRecordID,long rightRecordID),bool
IsLinkedTo procedure(long rightRecordID),bool
Load procedure
Save procedure
SetLinkBetween procedure(long leftRecordID,long rightRecordID)
SetLinkTo procedure(long rightRecordID)
End |
The Load and Save methods look like this:
Code Block |
---|
CML_Data_ManyToManyLinks.Load procedure
code
if not self.Persister &= null
self.Persister.Load()
end
CML_Data_ManyToManyLinks.Save procedure
code
if not self.Persister &= null
self.Persister.Save()
end |
It's important that I only call the Persister property's Load and Save methods if that object actually exists (leaving aside for the moment of where it will be created and assigned).
But as you can see I already have one major problem. The persister object needs some knowledge of the data in CML_Data_ManyToManyLinks.
I could pass the instance of CML_Data_ManyToManyLinks to the persister, but that would mean the links class and the persister class have references to each other, and that's a kind of carnal knowledge I really don't like my classes to have. I never want to have to decide which class I should use when calling a certain method, and I don't want to have to look in two classes to see the flow of logic.
There's an easy solution to this, which is to split the queue that contains the data out into its own class. This may seem like unnecessary proliferation of classes, but it will help to keep the code clean, and instantiating classes is a very inexpensive operation especially as compared to database access.
Here's my new class:
Code Block |
---|
CML_Data_ManyToManyLinksDataQ queue,TYPE
LeftRecordID long
RightRecordID long
end
CML_Data_ManyToManyLinksData Class,Type,Module('CML_Data_ManyToManyLinksData.CLW'),Link('CML_Data_ManyToManyLinksData.CLW',_CML_Classes_LinkMode_),Dll(_CML_Classes_DllMode_)
LinksQ &CML_Data_ManyToManyLinksDataQ
Construct Procedure()
Destruct Procedure()
End |
Of course I have to modify my CML_Data_ManyToManyLinks class to use this class instead of its own queue. That means replacing the queue declaration with the class declaration, and changing the self.LinksQ statements in the class code to self.LinksData.LinksQ.
How do I know my changes are good? By running the unit tests of course!
Have I mentioned how much I like having unit tests to fall back on? Refactoring is so much less stressful this way.
Now I can pass just the link data to the persister class methods:
Code Block |
---|
Load procedure(CML_Data_ManyToManyLinksData linksData)
Save procedure(CML_Data_ManyToManyLinksData linksData) |
using these calls:
Code Block |
---|
CML_Data_ManyToManyLinks.Load procedure
code
if not self.Persister &= null
self.Persister.Load(self.LinksData)
end
CML_Data_ManyToManyLinks.Save procedure
code
if not self.Persister &= null
self.Persister.Save(self.LinksData)
end |
This is getting much closer to what I need. However I still have to decide what kind of storage I want to use for my data.
For my tests a simple TPS file will do, but maybe down the road I'll want to be able to plug in any ABC-compliant file, or perhaps even an XML file.
Ideally I should be able to drop in any kind of persister I want without making code changes. And in fact I have written one such persister for TPS files. I'll explain how it works next time; for now, here's the unit test code to make sure that saving and loading works as expected.
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') |
I begin by setting up the ManyToMany object as usual, except that I assign an instance of CML_Data_ManyToManyLinksPersisterForTPS to the ManyToMany.Persister reference.
Then it's the usual code for setting and testing links.
I then save the link data to the file with a Save() call, followed by a Reset(). To make sure the links have been cleared I test for each link.
After that I load the data back up from the TPS file, and re-run the original set of tests to verify that the data has been restored. Does it work? Yes!
Next time: how the code works, and the changes required to the existing classes.