Many-to-many checkboxes revisited, part 7: The persister class
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:
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:
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:
Â
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:
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:
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:
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:
Load procedure(CML_Data_ManyToManyLinksData linksData) Save procedure(CML_Data_ManyToManyLinksData linksData)
using these calls:
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.
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.