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.