LT Project: Addition Discriminators

 A couple of posts back we saw a number of examples of implementations of IAdditionDiscriminator, a body of classes which bridge between having a criterion defined and applying it to the body of data to be retrieved.

One of the component interfaces making up the ILibraryBookRecord interface is the IFilterable record, described some posts back.  The addition discriminators interact with this interface for filtering, but work with the more general interface for related functionality.

class IAdditionDiscriminator

{

public:

  virtual ~IAdditionDiscriminator ();

  virtual bool reject (const IFilterableRecord &inRecord) const = 0;

  virtual ILibraryBookRecord *returnRecord (LibraryBookRecord &&inRec) const;

  virtual ILibraryBookRecord * returnRecordCopy (const ILibraryBookRecord &inRec) const;

  virtual std::string getType() const = 0;

  virtual void updateStats (LtStats &outStats,

               const LtLibrary::BaseLibraryBookRecord &inRec) const

  { }

};

updateStats() is only very rarely implemented as anything but a no-op, so it has that as a default behaviour rather than being a pure abstract function. There are also default implementations of the functions which return a working record:

namespace {

NullLibraryBookRecord Nullrval;

}

ILibraryBookRecord *

IAdditionDiscriminator::returnRecord (LibraryBookRecord &&inRec) const

{

  return &Nullrval;

}

ILibraryBookRecord *

IAdditionDiscriminator::returnRecordCopy (

    const ILibraryBookRecord &inRec) const

{

  return &Nullrval;

}

These return no-op implementations of the ILibraryBookRecord interface. We'll look in a moment at how these default implementations interact with the rest of the interface.

One typical example of the family is the AuthorAdditionDiscriminator:

class AuthorAdditionDiscriminator : public IAdditionDiscriminator

{

public:

  AuthorAdditionDiscriminator(std::string_view inStr): m_author(inStr) {}

  ~AuthorAdditionDiscriminator() override;

  bool reject(const IFilterableRecord &inRecord) const override;

  virtual std::string getType() const { return "AuthorAdditionDiscriminator"; }

private:

  std::string_view m_author;

};

getType() is used in logging and related contexts. reject() is the key function:

bool AuthorAdditionDiscriminator::reject(

    const IFilterableRecord &inRecord) const

{

  return !inRecord.matchesOnAuthor(m_author);

}

These classes return true when a record does not match the required criteria.  The default implementations of the other functions reflect this; they return values which will end up getting excluded by dead-ending in a no-op callback in the null object version.

The one and only exception to the above rule is the AlwaysAdditionDiscriminator:

class AlwaysAdditionDiscriminator : public IAdditionDiscriminator

{

public:

  ~AlwaysAdditionDiscriminator() override {}

  bool reject(const IFilterableRecord &inRecord) const override

  {

    return true;

  }

  ILibraryBookRecord *

  returnRecord(LibraryBookRecord &&inRec) const override

  {

    LibraryBookRecord *rval

        = new LibraryBookRecord(std::move(inRec));

    rval->setIsOnHeap(true);

    return rval;

  }

  ILibraryBookRecord *

  returnRecordCopy(const ILibraryBookRecord &inRec) const override

  {

    return inRec.clone();

  }

  std::string getType() const override

  {

    return "AlwaysAdditionDiscriminator";

  }

  void updateStats(LtStats &outStats,

              const BaseLibraryBookRecord &inRec) const override

  {

    outStats.add(inRec);

  }

};

This never rejects a record.  When passed a record in one of the relevant functions, it returns a copy (two different ways to reflect two different contexts) or it does a stats update call, of the soirt we saw in the last post.

This allows the processing to be set up using the AlwaysAdditionDiscriminator as a sentinel.

The discriminators are kept in an implementation of IAdditionDiscriminatorSet:

class IAdditionDiscriminatorSet

{

public:

  virtual ~IAdditionDiscriminatorSet();

  virtual const IAdditionDiscriminator &

  findMatch(const IFilterableRecord &inNewRec) const = 0;

  virtual void

  addDiscriminator(std::unique_ptr<IAdditionDiscriminator> &&inVal)

      = 0;

};

In the implementation they are stored in a vector:

class AdditionDiscriminatorSet : public IAdditionDiscriminatorSet

{

  using discriminator_storage_type

      = std::vector<std::unique_ptr<IAdditionDiscriminator>>;

public:

  AdditionDiscriminatorSet(const IRecordSetCriteria &inCriteria,

                           const bool inBreakoutCollections);

  ~AdditionDiscriminatorSet() override;

  const LtLibrary::IAdditionDiscriminator &

  findMatch(const IFilterableRecord &inNewRec) const override;

  void

  addDiscriminator(std::unique_ptr<IAdditionDiscriminator> &&inVal) override

  {

    m_additionDiscriminators.push_back(std::move(inVal));

  }

private:

  discriminator_storage_type m_additionDiscriminators;

};

The constructor puts in place a screen against deaccessioned books, adds whatever criteria come in from the user's search criteria, and then always adds an instance of the AlwaysAdditionDiscriminator last of all.

AdditionDiscriminatorSet::AdditionDiscriminatorSet(

    const IRecordSetCriteria &inCriteria, const bool inBreakoutCollections)

{

  addDiscriminator(std::make_unique<DeaccessionedAdditionDiscriminator>());

  inCriteria.addMatchingCriteria(*this, inBreakoutCollections);

  addDiscriminator(std::make_unique<AlwaysAdditionDiscriminator>());

}

This means that a find_if implementation will never return an end iterator, as the Always version always matches.

const IAdditionDiscriminator &

AdditionDiscriminatorSet::findMatch(const IFilterableRecord &inNewRec) const

{

  return *(*(std::ranges::find_if(

               m_additionDiscriminators,

               [&](const auto &inVal) { return inVal->reject(inNewRec); })))

              .get();

}

In most cases one of the early tests will cut off the find_if processing, so the number of actual tests applied per test tends to being rather low.  No branch needs to be made after the return.

RecordSetPopulator

At one level up, making use of the discriminator sets, is the record set populator, which is the key interface between the source collections and the retrieved data.

Here's the interface:

class IRecordSetPopulator

{

public:

  virtual ~IRecordSetPopulator();

  virtual ILibraryBookRecord *

  createRecord(const std::string &inString) const = 0;

  virtual ILibraryBookRecord *

  filterRecord(const BaseLibraryBookRecord &inRec) const = 0;

  virtual void updateStats(LtStats &outStats,

                           const BaseLibraryBookRecord &inRec) const = 0;

};

Here's the implementation:

class RecordSetPopulator : public IRecordSetPopulator

{

public:

  RecordSetPopulator (const IFieldAdderSet &inAdder,

                      const IRecordSetCriteria &inCriteria);

  ~RecordSetPopulator () override;

  ILibraryBookRecord *

  createRecord (const std::string &inString) const override;

  ILibraryBookRecord *

  filterRecord (const BaseLibraryBookRecord &inRec) const override;

  void updateStats (LtStats &outStats,

                    const BaseLibraryBookRecord &inRec) const override;

private:

  const IFieldAdderSet &m_adder;

  bool m_breakOutCollections;

  bool m_breakOutTags;

  AdditionDiscriminatorSet m_additionDiscriminators;

};


with the constructor simply capturing the information passed in:

RecordSetPopulator::RecordSetPopulator(const IFieldAdderSet &inAdder,

                                       const IRecordSetCriteria &inCriteria):

    m_adder(inAdder),

    m_breakOutCollections(inCriteria.hasCollections()),

    m_breakOutTags(inCriteria.hasTags()),

    m_additionDiscriminators(inCriteria, m_breakOutCollections)

{ }

All three of the filtering functions have the same shape: create a record for testing, and pass it to the appropriate discriminator function.  This is guaranteed always to return a value, but in the case of unwanted records that value will itself return a NullObject in returnRecord() and the calls to returnRecord() will return pointers to a single static object with low cost.  Only for wanted records will there be a real object passed out.

ILibraryBookRecord *

RecordSetPopulator::createRecord(const std::string &inString) const

{

  LibraryBookRecord rec(inString, m_adder, m_breakOutCollections,

                        m_breakOutTags);

  return m_additionDiscriminators.findMatch(rec).returnRecord(std::move(rec));

}


ILibraryBookRecord *

RecordSetPopulator::filterRecord(const BaseLibraryBookRecord &inRec) const

{

  LibraryBookRecord rec(inRec, m_adder, m_breakOutCollections, m_breakOutTags);

  return m_additionDiscriminators.findMatch(rec).returnRecord(std::move(rec));

}


void RecordSetPopulator::updateStats(LtStats &outStats,

                                     const BaseLibraryBookRecord &inRec) const

{

  std::unique_ptr<ILibraryBookRecord> rec(

      inRec.createRecord(m_adder, true, true));

  m_additionDiscriminators.findMatch(*rec).updateStats(outStats, inRec);

}

We've already seen how the call to createRecord() is used in the LibraryRecordSet:

      inPopulator.createRecord(inStr)->addToCollection(*this);

The NullObject implementation does nothing for addToCollection(); onlt the real records will insert themselves into the record set.  With the exception of the unique_ptr used in the stats collection variant, the records used for testing are generated on the stack. (The records in the LtStats call are RefLibraryBookRecord instances which have a low cost for their data storage).  Only when they are selected are they turned into a heap version with a longer life.

This is the largest block of processing in a typical application, as all records need to be tested.  The code as shown reflects the experienced need to do a reasonable degree of optimization by reducing branching, and reducing heap creation of objects and copying of data; it reached this stage when responsiveness became good.

The other change which might have been reasonable would have been to reverse the tests from reject() to accept() and then replace the algorithm with find_if_not().  The effecct would have been identical.  I considered it but left it as is as I considered the logic generally about as clear either way.

Comments

Popular posts from this blog

Boundaries

State Machines

Considerations on an Optimization