The Psalter Interface and Implementation
The psalter representation is fairly simple, with one tiny quirk.
The Psalter class -- which is the receptacle for the psalms derives from a one-function interface:
class IPsalter
{
public:
virtual ~IPsalter();
virtual const Psalm &getPsalm(uint16_t inIndex) const = 0;
};
However, the class itself has two functions:
class Psalter : public IPsalter
{
public:
~Psalter() override;
const Psalm &getPsalm(uint16_t inIndex) const override
{
if ((inIndex < 1) || (inIndex > 150) || (m_psalms[inIndex-1].get() == nullptr))
{
return m_nullPsalm;
}
return *(m_psalms[inIndex-1]);
}
void addPsalm(std::unique_ptr<Psalm> &inPsalm);
private:
std::array<std::unique_ptr<Psalm>, 150> m_psalms;
Psalm m_nullPsalm;
};
Using a limited interface rather than a concrete class meant not only that the (many) classes referencing the interface are insulated from the implementation but, more importantly, that a mock version could be created for the unit tests. This mock version could reflect calls without cluttering up output with the full texts of the psalms.
It also meant that the interface could be narrowed to only the const subset of the Psalter calls. Psalter is not in itself an immutable class, but referencing it through the interface makes it behave as such. (The small price that is paid for this is that the PsalmParser class is tightly coupled to the concrete Psalter class: that's a price I'm prepared to pay, although it could be avoided by providing a mutable-only interface class and having Psalter inherit from both the mutable and immutable interfaces. There is no need to mock the Psalter to rest the PsalmParser, as the ordinary concrete class works very well for checking what has been out into it once the parse is done, and it has no extended dependencies.) The only class referencing the Psalter via a concrete handle is the parser which builds it.
Note that psalms are accessed starting from 1, not starting from 0; the domain numbering takes precedence over the physical index.
The addPsalm() function, like the Psalm functions for adding verses and sections, has fairly extensive error checking:
void Psalter::addPsalm(std::unique_ptr<Psalm> &inPsalm)
{
if (inPsalm.get() == nullptr)
throw std::runtime_error("Null pointer passed on psalm addition");
const uint16_t num = inPsalm->getNumber();
if ((num == 0) || (num > 150))
throw std::runtime_error("Psalm with illegal number added");
// Shortest Psalm is 117, two verses.
if (inPsalm->getLength() < 2)
throw std::runtime_error("Psalm " + boost::lexical_cast<std::string>(num)
+ " should have length > 1");
if (m_psalms[num - 1].get() == nullptr)
m_psalms[num - 1].swap(inPsalm);
else
throw std::runtime_error("Attempt to add psalm "
+ boost::lexical_cast<std::string>(num)
+ " twice");
}
This is, again, because we want to be validating the data read in from the XML file.
Note one slight finesse: we pass in a normal reference (not Rvalue reference) to the new psalm, and we use swap() to move the contents of the new psalm into the value in the (fixed-size) array. The effect of this is that we know the value of the original unique_ptr after the swap (a nullptr value); if we had passed in an Rvalue reference and used std::move, the state of the target value would be identical to the existing logic, but there would be no guarantee about exactly what was in the original object: its only guarantee is "valid, but unknown". This allows the original reference to be reused always starting from a known base state.
Aside from the use of std::array (which could be replaced in C++03 by boost::array), std::unique_ptr (which could be replaced, here, by boost::scoped_ptr as we are not relying on its move semantics anywhere) and nullptr instead of NULL or 0 there aren't many "recent" language features deployed here.
Comments
Post a Comment