Revisiting the FullOfficeGenerator
A little while back I presented the code for the FullOfficeGenerator, which spins the seven or eight offices out out of the parsed information, and noted that the fairly heavy blocks of logic were not easily reducible.
I returned to it because I had asked myself two related questions:
1) The parallelisms between the individual office generation calls got a little lost in all the clutter;
2) Was there some way of making this somewhat overly-long class shorter by extracting logic in such a way as to make the logic clearer?
Yes, I decided, there was. It's still afflicted by all the small variances in logic, but it pulls more of the logic out into nicely parallel and clearly named blocks and to that degree it does assist somewhat with clarity.
First, we define a single small interface:
class IGenerationHandler
{
public:
virtual ~IGenerationHandler();
virtual void process(const IEncapsulatedOfficeFormatter &inFormatter,
const IPsalter &inPsalter, const IHymnSource &inHymns,
const bool inIsPriest)
= 0;
};
Then we create a different instance for each kind of office and each way in which the office is itself assembled before being called on to generate text, e.g.
class LaudsGenerationHandler : public IGenerationHandler
{
public:
LaudsGenerationHandler(const IHoursInfo &inInfo,
std::unique_ptr<ILaudsInfo> &&inLaudsInfo,
const Days inDay);
~LaudsGenerationHandler() override;
void process(const IEncapsulatedOfficeFormatter &inFormatter,
const IPsalter &inPsalter, const IHymnSource &inHymns,
const bool inIsPriest) override
{
Lauds l(m_info, *m_laudsInfo, m_day);
l.formatOffice(inFormatter, inIsPriest, inHymns);
}
private:
const IHoursInfo &m_info;
std::unique_ptr<ILaudsInfo> m_laudsInfo;
Days m_day;
};
class SpecialDayLaudsGenerationHandler : public IGenerationHandler
{
public:
SpecialDayLaudsGenerationHandler(const SpecialDays inSpecialDay,
const Days inDay):
m_specialDay(inSpecialDay),
m_day(inDay)
{ }
~SpecialDayLaudsGenerationHandler() override;
void process(const IEncapsulatedOfficeFormatter &inFormatter,
const IPsalter &inPsalter, const IHymnSource &inHymns,
const bool inIsPriest) override
{
Lauds l(inPsalter, m_specialDay, m_day);
l.formatOffice(inFormatter, inIsPriest, inHymns);
}
private:
SpecialDays m_specialDay;
Days m_day;
};
class VespersGenerationHandler : public IGenerationHandler
{
public:
VespersGenerationHandler(const IHoursInfo &inInfo,
std::unique_ptr<IVespersInfo> &&inVespersInfo):
m_info(inInfo),
m_vespersInfo(std::move(inVespersInfo))
{ }
~VespersGenerationHandler() override;
void process(const IEncapsulatedOfficeFormatter &inFormatter,
const IPsalter &inPsalter, const IHymnSource &inHymns,
const bool inIsPriest) override
{
Vespers v(m_specialDay, m_day);
v.formatOffice(inFormatter, inPsalter, inIsPriest, inHymns);
}
private:
const IHoursInfo &m_info;
std::unique_ptr<IVespersInfo> m_vespersInfo;
};
There are 15 different implementations:
- LaudsGenerationHandler
- SpecialDayLaudsGenerationHandler
- VespersGenerationHandler
- SpecialDayVespersGenerationHandler
- PrimeGenerationHandler
- SpecialDayPrimeGenerationHandler
- ComplineGenerationHandler
- SpecialAntiphonDayComplineGenerationHandler
- SpecialDayComplineGenerationHandler
- TerceGenerationHandler
- SpecialDayTerceGenerationHandler
- SextGenerationHandler
- SpecialDaySextGenerationHandler
- NoneGenerationHandler
- SpecialDayNoneGenerationHandler
Each class is small, which helps with making them comprehensible, and they all have a consistent interface.
Now we create a vector to hold them:
mutable std::vector<std::unique_ptr<IGenerationHandler>> m_generators;
and a (private) function to invoke them:
void generate() const
{
std::ranges::for_each(m_generators, [&](auto &inPtr) {
inPtr->process(m_formatter, m_psalter, m_hymns, m_isPriest);
});
m_generators.clear();
}
Because the generators are implementation details we make the collection mutable. We could have made the vector local, but in that case a class-level invocation would have has to take a reference to the vector, which isn't quite as clean.
Now what used to look like this:
void FullOfficeGenerator::generateFromSingleSource(
const IOfficeInfoSource &inSource, const IHoursInfo &inGeneralInfo) const
{
if (inSource.isPartial())
throw OfficeGenerationException("generateFromSingleSource", "partial");
const IOfficeInfoHolder &info = inSource.getStandardInfo();
if (inSource.hasFirstVespers())
{
VespersBuilder vb(inGeneralInfo, m_day, m_name, true);
auto vi = vb.generateVespersInfo(m_psalter, info, m_season);
generateVespers(inGeneralInfo, *vi);
}
{
LaudsBuilder lb(m_psalter, inGeneralInfo, m_day, m_name);
auto li = lb.generateLaudsInfo(info, m_season);
generateLauds(inGeneralInfo, *li);
}
{
PrimeBuilder pb;
auto pi = pb.generatePrimeInfo(m_psalter, info, m_season);
generatePrime(inGeneralInfo, *pi);
}
{
MinorHourBuilder mhb;
{
auto ti = mhb.generateTerceInfo(info, m_season);
generateTerce(inGeneralInfo, *ti);
}
{
auto si = mhb.generateSextInfo(info, m_season);
generateSext(inGeneralInfo, *si);
}
{
auto ni = mhb.generateNoneInfo(info, m_season);
generateNone(inGeneralInfo, *ni);
}
}
{
VespersBuilder vb(inGeneralInfo, m_day, m_name, false);
generateVespers(inGeneralInfo,
*vb.generateVespersInfo(m_psalter, info, m_season));
}
{
if (!m_romanUse)
{
auto l = [&](const Compline::SpecialAntiphonDays inDays) {
Compline c(inDays);
c.formatOffice(m_formatter, m_psalter, m_isPriest, m_hymns);
};
if (m_name == "Christmas Eve")
{
l(Compline::SpecialAntiphonDays::CHRISTMAS_EVE);
return;
}
else if (m_name == "Epiphany")
{
l(Compline::SpecialAntiphonDays::EPIPHANY);
return;
}
else if (m_name == "Purification of the Virgin")
{
l(Compline::SpecialAntiphonDays::CANDLEMAS);
return;
}
else if (m_name == "Holy Name of Jesus")
{
l(Compline::SpecialAntiphonDays::HOLY_NAME);
return;
}
}
generateCompline(inGeneralInfo, *ComplineBuilder().generateComplineInfo(
info, m_season, m_romanUse));
}
}
can be made to look like this:
void FullOfficeGenerator::generateFromSingleSource(
const IOfficeInfoSource &inSource, const IHoursInfo &inGeneralInfo) const
{
if (inSource.isPartial())
throw OfficeGenerationException("generateFromSingleSource", "partial");
const IOfficeInfoHolder &info = inSource.getStandardInfo();
if (inSource.hasFirstVespers())
{
m_generators.emplace_back(std::make_unique<VespersGenerationHandler>(
inGeneralInfo, VespersBuilder(inGeneralInfo, m_day, m_name, true)
.generateVespersInfo(m_psalter, info, m_season)));
}
m_generators.emplace_back(std::make_unique<LaudsGenerationHandler>(
inGeneralInfo,
LaudsBuilder(m_psalter, inGeneralInfo, m_day, m_name)
.generateLaudsInfo(info, m_season),
m_day));
m_generators.emplace_back(std::make_unique<PrimeGenerationHandler>(
inGeneralInfo,
PrimeBuilder().generatePrimeInfo(m_psalter, info, m_season)));
{
MinorHourBuilder mhb;
m_generators.emplace_back(std::make_unique<TerceGenerationHandler>(
inGeneralInfo, mhb.generateTerceInfo(info, m_season)));
m_generators.emplace_back(std::make_unique<SextGenerationHandler>(
inGeneralInfo, mhb.generateSextInfo(info, m_season)));
m_generators.emplace_back(std::make_unique<NoneGenerationHandler>(
inGeneralInfo, mhb.generateNoneInfo(info, m_season)));
}
m_generators.emplace_back(std::make_unique<VespersGenerationHandler>(
inGeneralInfo, VespersBuilder(inGeneralInfo, m_day, m_name, false)
.generateVespersInfo(m_psalter, info, m_season)));
if (!m_romanUse)
{
if (m_name == "Christmas Eve")
{
m_generators.emplace_back(
std::make_unique<SpecialAntiphonDayComplineGenerationHandler>(
Compline::SpecialAntiphonDays::CHRISTMAS_EVE));
}
else if (m_name == "Epiphany")
{
m_generators.emplace_back(
std::make_unique<SpecialAntiphonDayComplineGenerationHandler>(
Compline::SpecialAntiphonDays::EPIPHANY));
}
else if (m_name == "Purification of the Virgin")
{
m_generators.emplace_back(
std::make_unique<SpecialAntiphonDayComplineGenerationHandler>(
Compline::SpecialAntiphonDays::CANDLEMAS));
}
else if (m_name == "Holy Name of Jesus")
{
m_generators.emplace_back(
std::make_unique<SpecialAntiphonDayComplineGenerationHandler>(
Compline::SpecialAntiphonDays::HOLY_NAME));
}
else
m_generators.emplace_back(std::make_unique<ComplineGenerationHandler>(
inGeneralInfo, ComplineBuilder().generateComplineInfo(
info, m_season, m_romanUse)));
}
else
m_generators.emplace_back(std::make_unique<ComplineGenerationHandler>(
inGeneralInfo,
ComplineBuilder().generateComplineInfo(info, m_season, m_romanUse)));
generate();
}
aside from the length of the emplace_back calls this is simpler because more directly repetitive in structure than the earlier version, and very slightly shorter (by about ten lines). The actual office implementations like Vespers and Lauds are also now not direct dependencies. Otherwise, the logical structure is still essentially the same.
(I had originally wondered about having an IGenerationHandler generation class which would return the different instances successively on successive calls, and then building STL-style iterators for it. I decided that the extra overhead outweighed the benefits of the calling model - essentially, all the extra logic gets inverted into a function which looks very like the one to be replaced, but which returns rather than stores the executing classes, but with the additional need to build an STL-forward-iterator compatible frame around it, so I settled for storing them in a vector for processing. It would be entirely possible to prefer on other grounds the original model in which the office is generated immediately rather than stored and iterated, and in fact if pure efficiency were at the top of the list, it would be desirable. As it is, my opinion is that the discipline of saying, "these are the same types, they can be stored together and executed together" made the structure and intent a little clearer than an immediate execution model.)
Comments
Post a Comment