Hours' Psalm Specifications
Hour-level Psalm Specs is where the psalm world (Psalm, IPsalter, PsalmSpec) and the Antiphon world come together. Each office has a corresponding PsalmSpec class (e.g. PrimePsalmSpec) which incorporates a model of the content of the psalms portion of an office but can manage the formatting of those songs in terms of the relation between psalms and antiphon.
There is a general interface defining the two ways in which psalms can be output: first, using a set of antiphons proper to the office; second, by using a set of antiphons for Lauds on that day. These will, naturally, have the same implementation for Lauds.
class IOfficePsalmSpecSet
{
public:
virtual ~IOfficePsalmSpecSet();
virtual void outputPsalms(const IEncapsulatedOfficeFormatter &inFormatter,
const IAntiphons &inAntiphons,
const IPsalter &inPsalter) const = 0;
virtual void outputPsalmsWithLaudsAntiphons(
const IEncapsulatedOfficeFormatter &inFormatter,
const IPsalter &inPsalter, const ILaudsAntiphons &inAntiphons) const = 0;
};
Compline
Perhaps the simplest implementation is for Compline:
class ComplinePsalmSpec : public IOfficePsalmSpecSet
{
public:
~ComplinePsalmSpec() override;
void outputPsalms(const IEncapsulatedOfficeFormatter &inFormatter,
const IAntiphons &inAntiphons,
const IPsalter &inPsalter) const override;
// Does not happen for Compline
void outputPsalmsWithLaudsAntiphons(
const IEncapsulatedOfficeFormatter &inFormatter,
const IPsalter &inPsalter,
const ILaudsAntiphons &inAntiphons) const override
{
}
void outputPsalmsWithoutAntiphon(const IEncapsulatedOfficeFormatter &inFormatter,
const IPsalter &inPsalter,
const bool inIsTriduum) const;
private:
std::array<PsalmSpec, 4> m_psalms { PsalmSpec("4"), PsalmSpec("31:1-6"), PsalmSpec("91"), PsalmSpec("134") };
};
The two formatting functions are very simple:
void ComplinePsalmSpec::outputPsalms(const IEncapsulatedOfficeFormatter &inFormatter,
const IAntiphons &inAntiphons,
const IPsalter &inPsalter) const
{
inAntiphons.formatAntiphon(inFormatter, 0);
outputPsalmsWithoutAntiphon(inFormatter, inPsalter, false);
inAntiphons.formatAntiphon(inFormatter, 0);
}
void ComplinePsalmSpec::outputPsalmsWithoutAntiphon(
const IEncapsulatedOfficeFormatter &inFormatter,
const IPsalter &inPsalter,
const bool inIsTriduum) const
{
std::ranges::for_each(m_psalms, [&](const auto& r)
{
r.formattedPrint(inFormatter, inPsalter, inIsTriduum);
});
}
There is only ever one antiphon (except for a special case in Holy Week which is why that call to format without antiphon is public), it never has anything to do with Lauds. The only complexity is the selection of six verses from Psalm 31, and that's handled invisibly at this level.
Lesser Hours
For the lesser hours, there's a shared implementation:
class LesserHoursPsalmSpec : public IOfficePsalmSpecSet
{
public:
~LesserHoursPsalmSpec() override;
protected:
LesserHoursPsalmSpec(std::span<std::string> inSections,
const IPsalter &inPsalter, const bool inIsTriduum);
void outputPsalmsUnderOneAntiphon(const IEncapsulatedOfficeFormatter &inFormatter,
const IAntiphons &inAntiphons,
const IPsalter &inPsalter,
const int inIndex) const;
void outputPsalmsWithLaudsAntiphons(
const IEncapsulatedOfficeFormatter &inFormatter,
const IPsalter &inPsalter, const ILaudsAntiphons &inAntiphons,
const OfficeNames inOffice) const;
private:
std::vector<Psalm119PsalmSpec> m_psalms;
bool m_isTriduum;
void formatPsalms(const IEncapsulatedOfficeFormatter &inFormatter,
const IPsalter &inPsalter) const;
};
Note that this is heavily specialized in a couple of ways:
1) The only psalms used in the three lesser hours (Terce, Sext and None) are parts of psalm 119; this is one of the two places where the specialized Psalm119PsalmSpec comes into use.
2) It does not implement the interface function
virtual void outputPsalmsWithLaudsAntiphons(
const IEncapsulatedOfficeFormatter &inFormatter,
const IPsalter &inPsalter, const ILaudsAntiphons &inAntiphons) const = 0;
but it provides a helper function with a different signature. (It also has a protected constructor, so even aside from being an abstract class it can't be instantiated except in a child class).
The constructor puts a constraint on the span passed in:
LesserHoursPsalmSpec::LesserHoursPsalmSpec(std::span<std::string> inSections,
const IPsalter &inPsalter,
const bool inIsTriduum)
: m_isTriduum(inIsTriduum)
{
const Psalm &p = inPsalter.getPsalm(119);
if (inSections.size() == 6)
{
m_psalms.emplace_back(&p, inSections[0], inSections[1]);
m_psalms.emplace_back(&p, inSections[2], inSections[3]);
m_psalms.emplace_back(&p, inSections[4], inSections[5]);
}
else
throw std::runtime_error(
"Must have six sections in creating Lesser Hours Psalm Spec");
}
(The class uses a vector rather than a std::array<std::string, 3> to avoid having to have the elements default-allocated before assignment; the other work done means that we can't easily get an array initialized in an initializer list, The use of emplace_back means better efficiency than with a create-default-and-assign model. (This also means that Psalm119PsalmSpec can have only one constructor, the default one (other than the automatically-generated copy and move constructors).))
The two formatting functions display in petto the antiphon pattern for sets of psalms:
void LesserHoursPsalmSpec::outputPsalmsUnderOneAntiphon(
const IEncapsulatedOfficeFormatter &inFormatter,
const IAntiphons &inAntiphons, const IPsalter &inPsalter,
const int inIndex) const
{
inAntiphons.formatAntiphon(inFormatter, inIndex);
formatPsalms(inFormatter);
inAntiphons.formatAntiphon(inFormatter, inIndex);
}
void LesserHoursPsalmSpec::outputPsalmsWithLaudsAntiphons(
const IEncapsulatedOfficeFormatter &inFormatter, const IPsalter &inPsalter,
const ILaudsAntiphons &inAntiphons,
const OfficeNames inOffice) const
{
if (inAntiphons.useAsAntiphonsForLesserHours())
{
inAntiphons.formatAntiphonForHour(inFormatter, inOffice);
formatPsalms(inFormatter);
inAntiphons.formatAntiphonForHour(inFormatter, inOffice);
}
}
where formatPsalms is a simple call to ranges for_each:
void LesserHoursPsalmSpec::formatPsalms(
const IEncapsulatedOfficeFormatter &inFormatter) const
{
std::ranges::for_each(m_psalms, [&](const auto r) {
r.formatWithoutPsalter(inFormatter, m_isTriduum);
});
}
The child classes essentially simply provide concrete specifiers:
class TercePsalmSpec : public LesserHoursPsalmSpec
{
public:
explicit TercePsalmSpec(const IPsalter &inPsalter, const bool inIsTriduum = false)
: LesserHoursPsalmSpec(s_Sections, inPsalter, inIsTriduum)
{ }
~TercePsalmSpec() override;
void outputPsalms(const IEncapsulatedOfficeFormatter &inFormatter,
const IAntiphons &inAntiphons,
const IPsalter &inPsalter) const override
{
outputPsalmsUnderOneAntiphon(inFormatter, inAntiphons, 1);
}
void outputPsalmsWithLaudsAntiphons(
const IEncapsulatedOfficeFormatter &inFormatter,
const IPsalter &inPsalter,
const ILaudsAntiphons &inAntiphons) const override
{
LesserHoursPsalmSpec::outputPsalmsWithLaudsAntiphons(
inFormatter, inAntiphons, OfficeNames::TERCE);
}
private:
static inline std::array<std::string, 6> s_Sections{ "5", "6", "7",
"8", "9", "10" };
};
PrimePsalmSpec
The Prime set of psalms also uses the Psalm119PsalmSpec, but it is combined with other psalms, so we use a small extra level of indirection to simplify the formatting code. In addition, Prime also uses the Quicumque Vult/Athanasian Creed, but only on Sundays, under a separate antiphon.
class PrimePsalmSpec : public IOfficePsalmSpecSet
{
public:
PrimePsalmSpec(const IPsalter &inPsalter, const SpecialDays inSpecialDays,
const Days inDays);
PrimePsalmSpec(const IPsalter &inPsalter, const Days inDays,
const HymnSeasons inSeason);
~PrimePsalmSpec() override;
void outputPsalms(const IEncapsulatedOfficeFormatter &inFormatter,
const IAntiphons &inAntiphons,
const IPsalter &inPsalter) const override;
void outputPsalmsWithLaudsAntiphons(
const IEncapsulatedOfficeFormatter &inFormatter,
const IPsalter &inPsalter,
const ILaudsAntiphons &inAntiphons) const override;
void outputCreed(const IEncapsulatedOfficeFormatter &inFormatter,
const std::string &inAntiphon) const;
private:
SpecialDays m_specialDay;
Days m_days;
std::vector<std::unique_ptr<IPsalmSpec>> m_psalms;
Canticle m_AthanasianCreed;
};
The extra level of indirection is shown by the use of the std::unique_ptr for the set of psalms.
The SpecialDays constructor, which never sets up the Athanasian Creed and doesn't vary in its psalms, is fairly simple:
PrimePsalmSpec::PrimePsalmSpec(const IPsalter &inPsalter,
const SpecialDays inSpecialDays,
const Days inDays)
: m_specialDay(inSpecialDays), m_days(inDays)
{
const Psalm &p = inPsalter.getPsalm(119);
m_psalms.push_back(std::make_unique<PsalmSpec>("54"));
m_psalms.push_back(std::make_unique<PsalmSpec>("118"));
m_psalms.push_back(std::make_unique<Psalm119PsalmSpec>(&p, "1", "2"));
m_psalms.push_back(std::make_unique<Psalm119PsalmSpec>(&p, "3", "4"));
}
The normal constructor is more finicky:
PrimePsalmSpec::PrimePsalmSpec(const IPsalter &inPsalter, const Days inDays,
const HymnSeasons inSeason)
: m_specialDay(SpecialDays::NONE), m_days(inDays)
{
m_psalms.push_back(std::make_unique<PsalmSpec>("54"));
switch (inDays)
{
using enum Days;
case SUNDAY:
if ((inSeason == HymnSeasons::SEPTUAGESIMA)
|| (inSeason == HymnSeasons::LENT)
|| (inSeason == HymnSeasons::MID_LENT)
|| (inSeason == HymnSeasons::PASSIONTIDE))
m_psalms.push_back(std::make_unique<PsalmSpec>("93"));
else
m_psalms.push_back(std::make_unique<PsalmSpec>("118"));
break;
case MONDAY:
m_psalms.push_back(std::make_unique<PsalmSpec>("24"));
break;
case TUESDAY:
m_psalms.push_back(std::make_unique<PsalmSpec>("25"));
break;
case WEDNESDAY:
m_psalms.push_back(std::make_unique<PsalmSpec>("26"));
break;
case THURSDAY:
m_psalms.push_back(std::make_unique<PsalmSpec>("23"));
break;
case FRIDAY:
m_psalms.push_back(std::make_unique<PsalmSpec>("22"));
break;
case SATURDAY:
break;
}
const Psalm &p = inPsalter.getPsalm(119);
m_psalms.push_back(std::make_unique<Psalm119PsalmSpec>(&p, "1", "2"));
m_psalms.push_back(std::make_unique<Psalm119PsalmSpec>(&p, "3", "4"));
if (inDays == Days::SUNDAY)
{
static std::vector<std::string> text{
"Whosoever will be saved : before all things it is necessary that he hold the Catholic Faith.",
/// Here omitting the bulk of the text of the Creed...
"This is the Catholic Faith : which except a man believe faithfully he cannot be saved."
};
m_AthanasianCreed
= Canticle{ "The Athanasian Creed", "Quicumque vult", text, true };
}
}
The interaction between day of week and season is a general pattern which will show up in many other locations. Prime has no variation for feast days in general, so *that* aspect of variation is not present here.
The two inherited virtual functions have similar implementations.
void PrimePsalmSpec::outputPsalms(const IEncapsulatedOfficeFormatter &inFormatter,
const IAntiphons &inAntiphons,
const IPsalter &inPsalter) const
{
inAntiphons.formatAntiphon(inFormatter, 0);
for (const auto &r : m_psalms)
r->formattedPrint(inFormatter, inPsalter,
((m_specialDay == SpecialDays::MAUNDY_THURSDAY)
|| (m_specialDay == SpecialDays::GOOD_FRIDAY)
|| (m_specialDay == SpecialDays::HOLY_SATURDAY)));
inAntiphons.formatAntiphon(inFormatter, 0);
outputCreed(inFormatter, "");
}
void
PrimePsalmSpec::outputPsalmsWithLaudsAntiphons(
const IEncapsulatedOfficeFormatter &inFormatter, const IPsalter &inPsalter,
const ILaudsAntiphons &inAntiphons) const
{
if (inAntiphons.useAsAntiphonsForLesserHours())
{
inAntiphons.formatAntiphonForHour(inFormatter, OfficeNames::PRIME);
for (const auto &r : m_psalms)
r->formattedPrint(inFormatter, inPsalter);
inAntiphons.formatAntiphonForHour(inFormatter, OfficeNames::PRIME);
outputCreed(inFormatter, "");
}
}
In this case I've chosen to use a range-based for loop (C++11) rather than an STL for_each algorithm as being slightly simpler, although converting
for (const auto &r : m_psalms) r->formattedPrint(inFormatter, inPsalter);
into
std::ranges::for_each(m_psalms, [&](const auto& r) { r->formattedPrint(inFormatter, inPsalter); }
is a trivial exercise, if slightly wordy.
Handling the Creed output uses a C++14 free lambda to hold to the DRY principle:
void
PrimePsalmSpec::outputCreed(const IEncapsulatedOfficeFormatter &inFormatter,
const std::string &inAntiphon) const
{
if ((m_specialDay != SpecialDays::NONE) && (m_days == Days::SUNDAY))
{
auto formatter = [&](){
if (inAntiphon.empty())
{
inFormatter.formatAntiphon(
"Thee, O God the Father unbegotten, Thee, O God the Son only-begotten, Thee, Holy Ghost, Paraclete, Holy and undivided Trinity : with our whole heart and our lips we confess, praise and bless ; to Thee be glory for ever.");
}
else
inFormatter.formatAntiphon(inAntiphon);
};
formatter();
m_AthanasianCreed.formattedPrint(inFormatter);
formatter();
}
}
An Aside on Locality
I once had a colleague who really disliked local classes. This was in C++03, where the above function would have had to be defined as (pretending that unique_ptr was available):
void PrimePsalmSpec::outputCreed(const IEncapsulatedOfficeFormatter &inFormatter,
const std::string &inAntiphon) const
{
if ((m_specialDay != SpecialDays::NONE) && (m_days == Days::SUNDAY))
{
class Formatter {
public:
Formatter(const IEncapsulatedOfficeFormatter &inFormatter, const std::string& inVal): m_formatter(inFormatter), m_antiphon(inVal) { }
void operator()(const std::unique_ptr<IPsalmSpec>& inSpec) {
if (m_antiphon.empty())
{
m_formatter.formatAntiphon(
"Thee, O God the Father unbegotten, Thee, O God the Son only-begotten, Thee, Holy Ghost, Paraclete, Holy and undivided Trinity : with our whole heart and our lips we confess, praise and bless ; to Thee be glory for ever.");
}
else
m_formatter.formatAntiphon(m_antiphon);
}
private:
const IEncapsulatedOfficeFormatter &m_formatter;
const std::string& m_antiphon;
};
Formatter formatter(inFormatter, inAntiphon);
formatter();
m_AthanasianCreed.formattedPrint(inFormatter);
formatter();
}
}
The above is obviously much more wordy than the lambda, but is is, logically, the same thing. He disliked the clutter of having the local class definition. My general position was, and continues to be, that the more local you can make something which is genuinely local, i.e. having no possibility for reuse in any wider scope, the better: it pushes details down to the lowest level possible. So I favour having local logic at the function level rather than the file or class level: it makes the structure of the code reflect the logical structure of the intent.
Lambdas obviously make this cleaner, by making the capture-by-reference of the local variables implicit rather than having to be spelled out, and removing the cruft of the class declaration. But I don't think that the subtraction of seven lines is what makes the local use of the lambda a good thing; I think that the locality is a plus even before the introduction of lambdas and that, for this sort of use case, lambdas simply make it cleaner.
VespersPsalmSpec
The VespersPsalmSpec is the simpler of the two major hours instances. Its construction model is elaborate, as it has various complicated factors determining content, but the rest of its implementation is fairly straightforward:
class VespersPsalmSpec : public IOfficePsalmSpecSet
{
public:
VespersPsalmSpec() {}
explicit VespersPsalmSpec(const std::string &inNamedException);
explicit VespersPsalmSpec(const SpecialDays inDay);
VespersPsalmSpec(const Days inDays, const bool inFollowRomanUse);
explicit VespersPsalmSpec(const std::vector<PsalmSpec> &inVals);
~VespersPsalmSpec() override;
std::span<PsalmSpec>
getDetails() const
{
return m_psalms;
}
void outputPsalms(const IEncapsulatedOfficeFormatter &inFormatter,
const IAntiphons &inAntiphons,
const IPsalter &inPsalter) const override;
void outputPsalmsWithLaudsAntiphons(
const IEncapsulatedOfficeFormatter &inFormatter,
const IPsalter &inPsalter,
const ILaudsAntiphons &inAntiphons) const override;
private:
mutable std::array<PsalmSpec, 5> m_psalms;
bool m_followRomanUse = true;
bool m_isEasterEve = false;
bool m_isTriduum = false;
void init(std::span<std::string> inVals);
};
Let's start by looking at initialization.
The simplest constructor just copies PsalmSpecs:
VespersPsalmSpec::VespersPsalmSpec(const std::vector<PsalmSpec> &inVals)
{
if (inVals.size() == 5)
std::ranges::copy(inVals, m_psalms.begin());
}
For everything else we translate conditions into sets of specs.
Thus, for example:
VespersPsalmSpec::VespersPsalmSpec(const Days inDays,
const bool inFollowRomanUse)
: m_followRomanUse(inFollowRomanUse)
{
switch (inDays)
{
using enum Days;
case SUNDAY:
{
static std::array<std::string, 4> initVals{ "110"s, "111"s, "112"s, "113"s };
init(initVals);
m_psalms[4] = PsalmSpec("114"s, "115"s);
}
break;
case MONDAY:
{
static std::array<std::string, 5> initVals{ "116:1"s, "116:2"s, "117"s, "120"s, "121"s };
init(initVals);
}
break;
case TUESDAY:
{
static std::array<std::string, 5> initVals{ "122"s, "123"s, "124"s, "125"s, "126"s };
init(initVals);
}
break;
case WEDNESDAY:
{
static std::array<std::string, 5> initVals{ "127"s, "128"s, "129"s, "130"s, "131"s };
init(initVals);
}
break;
case THURSDAY:
{
static std::array<std::string, 5> initVals{ "132"s, "133"s, "135"s, "136"s, "137"s };
init(initVals);
}
break;
case FRIDAY:
{
static std::array<std::string, 5> initVals{ "138"s, "139"s, "140"s, "141"s, "142"s };
init(initVals);
}
break;
case SATURDAY:
{
static std::array<std::string, 5> initVals{ "144"s, "145"s, "146"s, "147:1"s, "147:2"s };
init(initVals);
}
break;
}
}
There is an interesting note here about the intersection between language features and clean code.
The original version of this had explicit numbered initializers; instead of
static std::array<std::string, 5> initVals{ "144"s, "145"s, "146"s, "147:1"s, "147:2"s };
init(initVals);
it had
m_psalms[0] = PsalmSpec{ "144"s };
m_psalms[1] = PsalmSpec{ "145"s };
m_psalms[2] = PsalmSpec{ "146"s };
m_psalms[3] = PsalmSpec{ "147"s };
m_psalms[4] = PsalmSpec{ "148"s };
The init() function was introduced to allow for the simpler initialization syntax:
void VespersPsalmSpec::init(std::span<std::string> inVals)
{
if ((inVals.size() == 5) || (inVals.size() == 4))
std::ranges::transform(inVals, m_psalms.begin(), [](const auto & inVal) { return PsalmSpec{ inVal }; });
}
But this would only be simpler in C++11 or later! In C++03 there is no initialization-list constructor for std::vector (and std::array did not exist). For a vector (or boost::array) model each value has to be set up individually. So for an init function:
void VespersPsalmSpec::init(const std::vector<std::string> inVals)
{
class Transformer {
public:
PsalmSpec operator(const std::string& inVal) const { return PsalmSpec(inVal); }
};
Transformer t;
if ((inVals.size() == 5) || (inVals.size() == 4))
std::transform(inVals.begin(), inVals.end(), m_psalms.begin(), t);
}
you would have had to do:
std::vector<std::string initVals;
initVals.push_back("144");
initVals.push_back("145");
initVals.push_back("146");
initVals.push_back("147");
initVals.push_back("148");
init(initVals);
which is less clear and longer than the original set of five assignments.
(To be fair, you could initialize a C-style array of char pointers, each of which pointed to a static C-style string, and then iterate over it, But you'd have to pass in an extra parameter for the length check, which could be decoupled and therefore in error, and I wouldn't advise going that route.)
So this is one place where the newer language features are an enabling capability for a specific sort of refactoring.
The Vespers formatting is also more elaborate, as a number of special conditions have to be handled.
void VespersPsalmSpec::outputPsalms(const IEncapsulatedOfficeFormatter &inFormatter,
const IAntiphons &inAntiphons,
const IPsalter &inPsalter) const
{
inAntiphons.formatAntiphon(inFormatter, 0);
m_psalms[0].formattedPrint(inFormatter, inPsalter, m_isTriduum);
if (m_isEasterEve)
{
inAntiphons.formatAntiphon(inFormatter, 0);
return;
}
else if (inAntiphons.antiphons() == 1)
{
std::ranges::for_each(
std::ranges::iota_view{ 1, 5 }, [&](const int inIndex) {
m_psalms[inIndex].formattedPrint(inFormatter, inPsalter, m_isTriduum);
});
inAntiphons.formatAntiphon(inFormatter, 0);
return;
}
inAntiphons.formatAntiphon(inFormatter, 0);
std::ranges::for_each(
std::ranges::iota_view{ 1, 5 }, [&](const int inIndex) {
inAntiphons.formatAntiphon(inFormatter, inIndex);
m_psalms[inIndex].formattedPrint(inFormatter, inPsalter, m_isTriduum);
inAntiphons.formatAntiphon(inFormatter, inIndex);
});
}
The version where psalms are formatted via the Lauds antiphons varies by Sarum or Roman use:
void VespersPsalmSpec::outputPsalmsWithLaudsAntiphons(
const IEncapsulatedOfficeFormatter &inFormatter, const IPsalter &inPsalter,
const ILaudsAntiphons &inAntiphons) const
{
if (inAntiphons.useAsAntiphonsForLesserHours())
{
if (m_followRomanUse)
{
std::ranges::for_each(
std::ranges::iota_view{ 0, 5 }, [&](const int inIndex) {
inAntiphons.formatAntiphon(inFormatter, inIndex);
m_psalms[inIndex].formattedPrint(inFormatter, inPsalter, m_isTriduum);
inAntiphons.formatAntiphon(inFormatter, inIndex);
});
}
else
{
inAntiphons.formatAntiphonForHour(inFormatter, OfficeNames::VESPERS);
std::ranges::for_each(
std::ranges::iota_view{ 0, 5 }, [&](const int inIndex) {
m_psalms[inIndex].formattedPrint(inFormatter, inPsalter, m_isTriduum);
});
inAntiphons.formatAntiphonForHour(inFormatter, OfficeNames::VESPERS);
}
}
}
In Sarum use only the first antiphon is used; in Roman use all five are used.
LaudsPsalmSpec
Like the PrimePsalmSpec, the Lauds version has a canticle to handle, but in this case it is a canticle that varies by day. In addition, two of the values (the third and the fifth) never vary, but the others vary by day of week. Here is a first cut:
class LaudsPsalmSpec : public IOfficePsalmSpecSet
{
public:
LaudsPsalmSpec(const Days inDays, const Grades inGrade,
const HymnSeasons inSeason);
LaudsPsalmSpec(const SpecialDays inSpecialDay, const Days inDay);
~LaudsPsalmSpec() override;
void outputPsalms(const IEncapsulatedOfficeFormatter &inFormatter,
const IAntiphons &inAntiphons,
const IPsalter &inPsalter) const override;
void outputPsalmsWithLaudsAntiphons(
const IEncapsulatedOfficeFormatter &inFormatter,
const IPsalter &inPsalter,
const ILaudsAntiphons &inAntiphons) const override;
private:
std::array<PsalmSpec, 4> m_psalms;
static PsalmSpec s_MidPsalms;
static PsalmSpec s_EndPsalms;
Canticle m_can;
bool m_isInOctave;
bool m_isTriduum = false;
};
The two static values are initialized in the .cpp file:
namespace {
std::array<std::string, 3> EndPsalmValues{ "148", "149", "150" };
}
//...
PsalmSpec LaudsPsalmSpec::s_EndPsalms{ EndPsalmValues };
PsalmSpec LaudsPsalmSpec::s_MidPsalms{ "63", "67" };
The two constructors are a little lengthy, driven by daily variations:
LaudsPsalmSpec::LaudsPsalmSpec(const SpecialDays inSpecialDay,
const Days inDay)
: m_can(inSpecialDay, inDay),
m_isInOctave(inSpecialDay == SpecialDays::EASTER_WEEK),
m_isTriduum(inSpecialDay != SpecialDays::EASTER_WEEK)
{
if (inDay == Days::SUNDAY)
{
m_psalms[0] = PsalmSpec("93");
}
else
{
m_psalms[0] = PsalmSpec("51");
}
if (inSpecialDay == SpecialDays::EASTER_WEEK)
{
m_psalms[1] = PsalmSpec("100");
}
else
{
switch (inDay)
{
using enum Days;
case THURSDAY:
{
m_psalms[1] = PsalmSpec("90");
}
break;
case FRIDAY:
{
m_psalms[1] = PsalmSpec("143");
}
break;
case SATURDAY:
{
m_psalms[1] = PsalmSpec("92");
}
break;
default:
{
m_psalms[1] = PsalmSpec("93");
}
break;
}
}
m_psalms[2] = s_MidPsalms;
m_psalms[3] = s_EndPsalms;
}
LaudsPsalmSpec::LaudsPsalmSpec(const Days inDays, const Grades inGrade, const HymnSeasons inSeason)
: m_can(inDays), m_isInOctave((inGrade == Grades::GREATER_OCTAVE)
|| (inGrade == Grades::OCTAVE_DAY))
{
if ((inDays == Days::SUNDAY)
&& (inSeason != HymnSeasons::SEPTUAGESIMA)
&& (inSeason != HymnSeasons::LENT)
&& (inSeason != HymnSeasons::MID_LENT)
&& (inSeason != HymnSeasons::PASSIONTIDE))
{
m_psalms[0] = PsalmSpec("93");
}
else
{
m_psalms[0] = PsalmSpec("51");
}
switch (inDays)
{
using enum Days;
case SUNDAY:
{
if ((inSeason == HymnSeasons::SEPTUAGESIMA)
|| (inSeason == HymnSeasons::LENT)
|| (inSeason == HymnSeasons::MID_LENT)
|| (inSeason == HymnSeasons::PASSIONTIDE))
m_psalms[1] = PsalmSpec("118");
else
m_psalms[1] = PsalmSpec("100");
}
break;
case MONDAY:
{
m_psalms[1] = PsalmSpec("5");
}
break;
case TUESDAY:
{
m_psalms[1] = PsalmSpec("43");
}
break;
case WEDNESDAY:
{
m_psalms[1] = PsalmSpec("65");
}
break;
case THURSDAY:
{
m_psalms[1] = PsalmSpec("90");
}
break;
case FRIDAY:
{
m_psalms[1] = PsalmSpec("143");
}
break;
case SATURDAY:
{
m_psalms[1] = PsalmSpec("92");
}
break;
default:
{
m_psalms[1] = PsalmSpec("93");
}
break;
}
m_psalms[2] = s_MidPsalms;
m_psalms[3] = s_EndPsalms;
}
And the formatted output is messier than the equivalent Vespers logic because of the need to handle the canticle call specially:
void LaudsPsalmSpec::outputPsalms(const IEncapsulatedOfficeFormatter &inFormatter,
const IAntiphons &inAntiphons,
const IPsalter &inPsalter) const
{
if (m_isInOctave || (inAntiphons.antiphons() == 1))
{
inAntiphons.formatAntiphon(inFormatter, 0);
m_psalms[0].formattedPrint(inFormatter, inPsalter, m_isTriduum);
m_psalms[1].formattedPrint(inFormatter, inPsalter, m_isTriduum);
m_psalms[2].formattedPrint(inFormatter, inPsalter, m_isTriduum);
m_can.formattedPrint(inFormatter);
m_psalms[3].formattedPrint(inFormatter, inPsalter, m_isTriduum);
inAntiphons.formatAntiphon(inFormatter, 0);
}
else
{
std::ranges::for_each(
std::ranges::iota_view{ 0, 3 }, [&](const int inIndex) {
inAntiphons.formatAntiphon(inFormatter, inIndex);
m_psalms[inIndex].formattedPrint(inFormatter, inPsalter, m_isTriduum);
inAntiphons.formatAntiphon(inFormatter, inIndex);
});
inAntiphons.formatAntiphon(inFormatter, 3);
m_can.formattedPrint(inFormatter);
inAntiphons.formatAntiphon(inFormatter, 3);
inAntiphons.formatAntiphon(inFormatter, 4);
m_psalms[3].formattedPrint(inFormatter, inPsalter, m_isTriduum);
inAntiphons.formatAntiphon(inFormatter, 4);
}
}
As noted above, two specifications (the third (63 and 67) and the fifth (148, 149, 150)) are never different. It would be possible simply to represent them by static members and reduce the costs of setting up an instance in any context where there would be two or more instances created during a session. Instead, static instances are used to instantiate local members: this allows them to be handled in formatting along with the other psalms. (Because of the validation stage, copy construction, which skips validation, is faster than construction from a string.)
This is one way of approaching the question of Knuth's premature optimization by asking what we would give up. (It's never premature optimization if you give up nothing. A more efficient version which is equally clear, simple, and otherwise effective is always to be preferred.)
By initializing all the Psalm specifications as members of an array, we gain the simplicity of calling the formatting code in a simple for_each loop; or rather, we would if the fourth item were not a canticle instead of a Psalm spec. We have to break the iteration in any case. Note that the formatting call for a canticle has a different signature, as it does not have to pass a Psalter in for substantive reference, and it also internalizes the Triduum state.
We can do one of three things: leave the call as is, which is ragged and slightly unwieldy; convert the third and fifth calls to use static values directly, retaining the irregularity but squeezing a few more cycles out of the application; or provide a wrapper for the Psalm specs which holds a Psalter and allows calls to be made in a simple loop by modifying the storage of the canticle as well as the Psalm specs.
The third option would look roughly like this: create a class which holds (as a std::variant) a Canticle or a Psalter/PsalmSpec pair and has a single external formatting call. The class has two simple constructors; the calls switch on the variant type at the time of execution. This is obviously more expensive than the existing code, as it requires a test on every invocation, but it shoves the extra complexity down a level. (It also requires that the Lauds constructor take an IPsalter parameter, but that's easily accessible: the lesser hours already require that to source Psalm 119.)
Let's be honest: we don't need the extra speed. This is neither a critical inner loop nor a context where a user will notice the performance difference. Is the existing code ugly enough to warrant the proxy model?
What if we throw in better performance?
That proxy could have three constructors. In its third constructor it could intern the Psalter but store a flag indicating use of ... a static PsalmSpec for the two special cases. We have to branch on a flag in any case; jumping in a four-way switch is not much worse than switching on a boolean flag to determine which type should be referenced in a variant.
What do we gain or lose in clarity?
None of the proxy calls are particularly messy. The switch in the formatting call is at least structured through a switch. The LaudsPsalmSpec constructor gets most of the extra complexity, but that constructor is already dominated by a switch statement: having it initialize a proxy is no more messy than the existing code and it actually hides the use of the static values currently used to initialize the third and fifth values. The constructor actually becomes simpler. The proxy hides the internals: we initialize by day for a canticle, by an enum for the static values, and by strings for the first and second values.
We do add additional overall complexity by having an extra class.
What it comes down to is a particular model of clear code, associated with Robert C. Martin, as a form of the SRP: a class should be as small as possible. (This applies to functions as well.) The ideal class would have one function in its public interface, and that short. No function should be longer than a screen. (On my monitor, with emacs and a smallish font, one screen is too long.) Some developers dislike the effect this has: directories full of small files containing specialized classes: others view this as an indication that appropriate design principles are being followed.
Humans are poor at grasping more than a small amount of information together. Conversely, if you can pull apart the logic in a function so that it is expressed across several functions, as the proxy model does (e.g. by moving test logic into a constructor, separate from execution logic called in a different function; or by turning sequential logic into calls based in discriminated variants) it means that you've identified more than one responsibility in that function, it else it couldn't be divided.
The first marker of good code is clarity at each call site, testable at an immediate level. The price paid for that is an increase in overall complexity. (An application consisting of a single 1000 line function in main() has lower overall complexity than a function with a short main but 50 20-line functions spread across multiple classes; but the local complexity of each step in the latter, from main() on down, is much less.) Clarity leads to maintainability and extensibility. In the real world that's much of the cost of software development over even the medium run.
(The second marker is severability of parts (supporting both testability and extensibility), usually expressed in C++ by programming to interfaces.)
Even in real-time systems speed usually comes third, or lower, except at hot spots, and wherever there's a choice between clarity/severability and speed you should be testing with a profiler to determine the true tradeoff. In my experience most critical speed-enhancing disciplines, like reducing branching and increasing the likelihood of cache hits, can be managed without reducing clarity or severability.
So let's see what happens.
Here is our entry holder:
class EntryHolder
{
public:
enum class EntryType
{
Psalm,
Canticle,
DeusDeusMeus,
LaudateDominum
};
EntryHolder(const std::string &inPsalmNumber, const IPsalter &inPsalter, const bool inIsTriduum)
: m_data(PsalmSpec(inPsalmNumber)), m_psalter(inPsalter), m_type(EntryType::Psalm),
m_isTriduum(inIsTriduum)
{ }
EntryHolder(const Days inCanticleDay);
EntryHolder(const SpecialDays inSpecialCanticleDay, const Days inCanticleDay);
EntryHolder(const EntryType inSpecialType, const IPsalter &inPsalter, const bool inIsTriduum);
void formattedPrint(const IEncapsulatedOfficeFormatter &inFormatter) const;
private:
std::variant<PsalmSpec, Canticle> m_data;
const IPsalter &m_psalter;
static inline PsalmSpec s_MidPsalms{ "63", "67" };
static PsalmSpec s_EndPsalms;
EntryType m_type;
bool m_isTriduum;
};
(Why, yes, I am comfortable with Latin as a second language; why do you ask?)
This now not only does the dispatching for formatting but it has absorbed the former m_isTriduum flag as well.
The formattedPrint() function is where the last of the real ugliness of the variable dispatching goes, and it's now a four-case switch statement where each case is one line:
void LaudsPsalmSpec::EntryHolder::formattedPrint(
const IEncapsulatedOfficeFormatter &inFormatter) const
{
switch (m_type)
{
using enum EntryType;
case Psalm:
std::get<0>(m_data).formattedPrint(inFormatter, m_psalter, m_isTriduum);
break;
case Canticle:
std::get<1>(m_data).formattedPrint(inFormatter);
break;
case DeusDeusMeus:
s_MidPsalms.formattedPrint(inFormatter, m_psalter, m_isTriduum);
break;
case LaudateDominum:
s_EndPsalms.formattedPrint(inFormatter, m_psalter, m_isTriduum);
break;
}
}
The three constructors do the appropriate initializations:
LaudsPsalmSpec::EntryHolder::EntryHolder(const Days inCanticleDay)
: m_data(Canticle(inCanticleDay)),
m_psalter(NullPsalter::GetNullPsalter()), m_type(EntryType::Canticle),
m_isTriduum(false)
{ }
LaudsPsalmSpec::EntryHolder::EntryHolder(
const SpecialDays inSpecialCanticleDay, const Days inCanticleDay)
: m_data(Canticle(inSpecialCanticleDay, inCanticleDay)),
m_psalter(NullPsalter::GetNullPsalter()), m_type(EntryType::Canticle),
m_isTriduum(inSpecialCanticleDay != SpecialDays::EASTER_WEEK)
{ }
LaudsPsalmSpec::EntryHolder::EntryHolder(const EntryType inSpecialType,
const IPsalter &inPsalter,
const bool inIsTriduum)
: m_psalter(inPsalter), m_type(inSpecialType), m_isTriduum(inIsTriduum)
{
if ((inSpecialType == EntryType::Psalm)
|| (inSpecialType == EntryType::Canticle))
throw std::runtime_error(
"Cannot initialize Entry holder with bare indicator of psalm or canticle");
}
Those two old constructors for LaudsPsalmSpec look much the same as far as their overall structure goes, as the logic stays the same, but they are a little cleaner, with the help of a couple of lambdas:
LaudsPsalmSpec::LaudsPsalmSpec(const IPsalter &inPsalter,
const SpecialDays inSpecialDay,
const Days inDay)
: m_isInOctave(inSpecialDay == SpecialDays::EASTER_WEEK)
{
auto emplacer = [&](const std::string& inVal) { m_entries.emplace_back(inVal, inPsalter, (inSpecialDay != SpecialDays::EASTER_WEEK)); };
if (inDay == Days::SUNDAY)
{
emplacer("93"s);
}
else
{
emplacer("51"s);
}
if (inSpecialDay == SpecialDays::EASTER_WEEK)
{
emplacer("100"s);
}
else
{
switch (inDay)
{
using enum Days;
case THURSDAY:
{
emplacer("90"s);
}
break;
case FRIDAY:
{
emplacer("143"s);
}
break;
case SATURDAY:
{
emplacer("92"s);
}
break;
default:
{
emplacer("93"s);
}
break;
}
}
m_entries.emplace_back(EntryHolder::EntryType::DeusDeusMeus, inPsalter,
isTriduum);
m_entries.emplace_back(inSpecialDay, inDay);
m_entries.emplace_back(EntryHolder::EntryType::LaudateDominum, inPsalter,
isTriduum);
}
LaudsPsalmSpec::LaudsPsalmSpec(const IPsalter &inPsalter, const Days inDays,
const Grades inGrade,
const HymnSeasons inSeason)
: m_isInOctave((inGrade == Grades::GREATER_OCTAVE)
|| (inGrade == Grades::OCTAVE_DAY))
{
auto emplacer = [&](const std::string& inVal) { m_entries.emplace_back(inVal, inPsalter, false); };
if ((inDays == Days::SUNDAY) && (inSeason != HymnSeasons::SEPTUAGESIMA)
&& (inSeason != HymnSeasons::LENT) && (inSeason != HymnSeasons::MID_LENT)
&& (inSeason != HymnSeasons::PASSIONTIDE))
{
emplacer("93"s);
}
else
{
emplacer("51"s);
}
switch (inDays)
{
using enum Days;
case SUNDAY:
{
if ((inSeason == HymnSeasons::SEPTUAGESIMA)
|| (inSeason == HymnSeasons::LENT)
|| (inSeason == HymnSeasons::MID_LENT)
|| (inSeason == HymnSeasons::PASSIONTIDE))
{
emplacer("118"s);
}
else
{
emplacer("100"s);
}
}
break;
case MONDAY:
{
emplacer("5"s);
}
break;
case TUESDAY:
{
emplacer("43"s);
}
break;
case WEDNESDAY:
{
emplacer("65"s);
}
break;
case THURSDAY:
{
emplacer("90"s);
}
break;
case FRIDAY:
{
emplacer("143"s);
}
break;
case SATURDAY:
{
emplacer("92"s);
}
break;
default:
{
emplacer("93"s);
}
break;
}
m_entries.emplace_back(EntryHolder::EntryType::DeusDeusMeus, inPsalter,
false);
m_entries.emplace_back(inDays);
m_entries.emplace_back(EntryHolder::EntryType::LaudateDominum, inPsalter,
false);
}
The formatting function is now rather simpler, and certainly more regular:
void LaudsPsalmSpec::outputPsalms(const IEncapsulatedOfficeFormatter &inFormatter,
const IAntiphons &inAntiphons,
const IPsalter &inPsalter) const
{
if (m_isInOctave || (inAntiphons.antiphons() == 1))
{
inAntiphons.formatAntiphon(inFormatter, 0);
std::ranges::for_each(m_entries, [&](const auto &inEntry) {
inEntry.formattedPrint(inFormatter);
});
inAntiphons.formatAntiphon(inFormatter, 0);
}
else
{
std::ranges::for_each(std::ranges::iota_view{ 0, 5 },
[&](const int inIndex) {
inAntiphons.formatAntiphon(inFormatter, inIndex);
m_entries[inIndex].formattedPrint(inFormatter);
inAntiphons.formatAntiphon(inFormatter, inIndex);
});
}
}
The improvements aren't dramatic, but they are real.
Comments
Post a Comment