2. Framework and Domain Logic Separation¶
The objective of this step is to remove, reduce, or isolate art concepts that would otherwise leak deep into algorithm code and make later migration more complicated than necessary. The goal is not to remove every art type immediately, but to decide which framework concepts are required to preserve the module’s framework-visible output and which are only convenience mechanisms that should be reduced to ordinary C++ before migration.
In many code bases, art concepts appear throughout the module code as a result of incremental history rather than a true requirement. The first task is to identify which framework constructs are essential and which are merely convenient.
For each art concept that appears in a code base:
determine whether the concept is required to preserve the module’s framework-visible output, or whether it is only an implementation convenience,
remove it where it is only being used for convenience,
isolate it in framework-boundary code where it is still required, and
convert the algorithm code to ordinary C++ inputs and outputs.
2.1. Cross-Cutting Rules¶
A few separation rules apply across all art concepts:
keep framework API use at the module boundary,
do not cache event-scoped objects across events,
prefer explicit inputs over implicit framework access,
prefer ordinary C++ containers and references in helper code, and
preserve art-specific types only where persisted behavior requires them.
In addition to preparing algorithm code for migration, applying these rules makes the data flow is easier to see and object ownership easier to reason about.
2.2. art::Ptr and Pointer-Like Access¶
art::Ptr combines object access with framework metadata.
That is sometimes required, especially when a module writes products whose
meaning depends on persisted data-product references. In many code paths,
however, art::Ptr is only being used as a convenient way to refer to an
object that is already present in a collection.
The key distinction is whether the code really needs persistent references.
art::Ptr can usually be removed when the code only needs:
read-only object access,
stable ordering within one collection,
a simple relationship represented by an index or key, or
temporary navigation during one computation.
As a practical rule, a module typically needs art::Ptr only when it places
an art::Ptr-based product into the event, for example art::Assns,
art::PtrVector, art::PtrMaker-produced pointers, or a
std::vector<art::Ptr<T>> written to the event. If provenance or persisted
references are genuinely required, or if association chaining depends on
art::Ptr<T>::key(), then art::Ptr may need to remain at the framework
boundary during an intermediate migration step.
The separation work is to inventory art::Ptr and fill_ptr_vector
usage, separate persisted-reference cases from convenience usage, and replace
the convenience cases with ordinary data access. In helper code that usually
means passing object values, references, indices, a std::vector<T> const&,
a std::vector<T const*> const&, or a lookup table built at the boundary.
Any unavoidable art::Ptr usage should stay near framework I/O.
Single-element access from a product can often be simplified to a reference:
// Before
art::Handle<std::vector<T>> h;
evt.getByLabel(label, h);
art::Ptr<T> p(h, 0);
p->method();
// After
T const& p = evt.getProduct<std::vector<T>>(label).at(0);
p.method();
Loop-local pointer construction often disappears entirely:
// Before
for (size_t i = 0; i < h->size(); ++i) {
art::Ptr<T> p(h, i);
p->method();
}
// After
for (auto const& p : *h) {
p.method();
}
FindManyP can often become FindMany when only object access is needed:
// Before
art::Handle<std::vector<T>> h;
evt.getByLabel(label, h);
std::vector<art::Ptr<T>> v;
art::fill_ptr_vector(v, h);
art::FindManyP<U> fm(v, evt, label2);
std::vector<art::Ptr<U>> const& items = fm.at(i);
items[j]->method();
// After
auto const h = evt.getValidHandle<std::vector<T>>(label);
art::FindMany<U> fm(h, evt, label2);
std::vector<U const*> const& items = fm.at(i);
items[j]->method();
When a helper only reads the object, update its signature to accept the object rather than the framework pointer:
// Before
void f(art::Ptr<T> const& p) { p->method(); }
// After
void f(T const& p) { p.method(); }
Use FindMany<T> when only the pointed-to objects are needed and no further
association chaining is required. Keep FindManyP<T> only when the code must
chain into another association and depends on art::Ptr<T>::key() as the
identifier. Do not substitute pointer arithmetic for .key().
When passing collections into helper code, prefer std::vector<T> const&
when the original product can be passed, std::vector<T const*> const& for
a filtered subset, and std::vector<art::Ptr<T>> only when persisted references
are needed.
When art::Ptr usage disappears, related includes often disappear as well,
including canvas/Persistency/Common/Ptr.h,
canvas/Persistency/Common/FindManyP.h when FindMany is sufficient, and
lardata/Utilities/AssociationUtil.h when it was only needed for
fill_ptr_vector.
AI tools are useful here because many art::Ptr uses are repetitive and can
be classified mechanically. They can inventory usage, classify each case as
persisted-output, association-chaining, or convenience, propose rewrites to
references or raw pointers, identify stale includes, and flag helper signatures
that still expose framework pointer types. Review those changes carefully when
a module writes associations or other pointer-based products, when
FindManyP may still be required for chaining, when the code depends on
.key() or provenance semantics, or when a helper stores pointers beyond
local event scope.
Appendix A is a self-contained AI memory file covering this task: it contains a candidate-finding script, all replacement patterns, decision rules, and a completed example, and is designed to be provided directly to an AI tool as reusable context when performing this separation work.
2.3. Data Product Retrieval and Event-Boundary Access¶
art naturally encourages product retrieval inside module callbacks, but that convenience often hides the real inputs to the algorithm. When helper code pulls products directly from the event, its dependencies become implicit and the code becomes harder to test, reuse, or map into Phlex dataflow declarations.
The separation rule is simple: move retrieval to the top-level framework boundary and pass retrieved products as explicit function parameters. Identify every call path that reaches into the event, list the actual products the algorithm requires, and remove event access from helper classes unless they are intentionally part of the framework layer.
art::Handle objects do not outlive the event and must never be stored as
module data members. More generally, event-scoped products, pointers,
references, and derived data must not be cached across events.
When retrieving data from the event, choose the appropriate call:
evt.getProduct<C>(tag)when only the product is needed downstream,evt.getValidHandle<C>(tag)when a handle is required and the product must exist,evt.getHandle<C>(tag)when the product may legitimately be absent.
Do not introduce new getByLabel(tag, handle) code. If a handle is
optional, test it with if (!h) rather than an explicit .isValid()
check. If older code throws an explicit exception when a handle is invalid,
replace it with getProduct or getValidHandle and remove the manual
check.
getByLabel is the most common legacy retrieval pattern in larreco
modules. The separation step is to replace it with the appropriate modern
alternative.
When only the product is needed:
// Before
art::Handle<std::vector<recob::Wire>> wireVecHandle;
evt.getByLabel(fCalDataModuleLabel, wireVecHandle);
// After
auto const& wires = evt.getProduct<std::vector<recob::Wire>>(fCalDataModuleLabel);
When a handle is still required (e.g. to construct art::Ptr for
associations):
// Before
art::Handle<std::vector<recob::Wire>> wireVecHandle;
evt.getByLabel(fCalDataModuleLabel, wireVecHandle);
// After
auto const wireVecHandle =
evt.getValidHandle<std::vector<recob::Wire>>(fCalDataModuleLabel);
// art::Ptr<recob::Wire>(wireVecHandle, i) still works
When the product may legitimately be absent:
// Before
art::Handle<std::vector<recob::Hit>> hitcol;
evt.getByLabel(fHitModuleLabel, hitcol);
if (!hitcol.isValid()) return;
// After
auto const hitcol = evt.getHandle<std::vector<recob::Hit>>(fHitModuleLabel);
if (!hitcol) return;
When getByLabel is combined with fill_ptr_vector and art::Ptr is
still required, replace only the retrieval:
// Before
art::Handle<std::vector<recob::Hit>> hitcol;
evt.getByLabel(fHitModuleLabel, hitcol);
std::vector<art::Ptr<recob::Hit>> hits;
art::fill_ptr_vector(hits, hitcol);
// After
auto const hitcol = evt.getValidHandle<std::vector<recob::Hit>>(fHitModuleLabel);
std::vector<art::Ptr<recob::Hit>> hits;
art::fill_ptr_vector(hits, hitcol);
When a helper class receives the event object and calls retrieval APIs internally, retrieval should instead be performed at the module boundary and the product passed in explicitly:
// Before: helper retrieves data from the event
class SomeAlg {
void run(art::Event const& evt) {
auto const& tracks = evt.getProduct<std::vector<recob::Track>>(fLabel);
// ...
}
};
fAlg.run(evt);
// After: module retrieves at the boundary; helper receives explicit input
class SomeAlg {
void run(std::vector<recob::Track> const& tracks) {
// ...
}
};
auto const& tracks = evt.getProduct<std::vector<recob::Track>>(fLabel);
fAlg.run(tracks);
AI tools can help locate retrieval APIs, group them by usage pattern, propose
getProduct or getValidHandle replacements for legacy getByLabel
code, identify helper methods that should accept explicit data instead of an
event or handle, and detect stale Handle.h includes. Review those changes
carefully when optional products are part of the intended behavior.
2.5. Services¶
Phlex does not have a concept of services.
An art service is a globally accessible stateful object: constructed before
the first module, destroyed after the last, able to register callbacks for
framework transitions, and able to depend on other services through
art::ServiceHandle. That flexibility is also a frequent source of hidden
dependencies that complicate migration.
The key rule is: art::ServiceHandle use belongs at the framework boundary, not
inside reusable algorithm code.
2.5.1. Preparing existing code¶
Start by inventorying every place that constructs a ServiceHandle or
depends on a service-provided object. For each use, identify what the
downstream code actually needs:
a read-only value or data structure,
a helper object that can be passed in explicitly,
a side effect such as logging or output production, or
a framework-provided service feature that cannot yet be represented another way.
In most cases, the module should retrieve the service-provided data at the boundary and pass ordinary C++ inputs into the algorithm. Concretely:
remove
art::ServiceHandleconstruction from algorithms, helpers, and utility classes,retrieve the needed value or object in the module callback,
replace hidden service access with explicit function parameters or constructor arguments,
treat read-only shared state (geometry, calibration constants, channel maps, etc.) as data inputs rather than global handles, and
record any remaining service dependencies as explicit migration-design items.
The most common problematic pattern is an algorithm that constructs a
ServiceHandle inside its own body:
// Before: service access inside algorithm — problematic
Tracks make_tracks(Hits const& hits)
{
art::ServiceHandle<Calibration> calibration;
ScalarOffset const& offset = calibration->Offset();
// ...
}
void TrackMaker::produce(art::Event& e)
{
auto const& hits = e.getProduct<Hits>("GoodHits");
Tracks tracks = make_tracks(hits);
e.put(std::make_unique<Tracks>(std::move(tracks)), "GoodTracks");
}
This couples the algorithm directly to the framework and hides the calibration data as an implicit dependency.
The fix is to retrieve the service data at the module boundary and pass it as an explicit argument:
// After: service access confined to module boundary
Tracks make_tracks(Hits const& hits, ScalarOffset const& offset)
{
// no framework types; offset is an ordinary C++ value
// ...
}
void TrackMaker::produce(art::Event& e)
{
auto const& hits = e.getProduct<Hits>("GoodHits");
art::ServiceHandle<Calibration> calibration;
Tracks tracks = make_tracks(hits, calibration->Offset());
e.put(std::make_unique<Tracks>(std::move(tracks)), "GoodTracks");
}
After this change the algorithm is framework-independent, and the dependency on the calibration offset is explicit.
This is the intended separation shape even when the art service cannot yet be removed. Once the codebase uses Phlex, the same algorithm function can remain unchanged. The framework registration declares where the offset comes from, but the algorithm code is not touched.
When completing this work, check that:
art::ServiceHandledoes not appear inside any function or class that is not itself a module or framework-boundary object,every value extracted from a service is passed as an explicit parameter or constructor argument to downstream code,
helpers that previously received an event or service handle now accept the extracted value directly, and
any service dependencies that genuinely cannot be removed are recorded as explicit migration-design items.
2.5.2. When to introduce a new service¶
The migration guidance above is for code that already uses services. If you are considering introducing a new service, first ask whether the capability really needs to be modeled as one.
Many common uses do not require a service:
Message logging. Standard logging libraries (e.g.,
spdlog,std::cerr) are ordinary C++ and need no framework wrapper. Phlex currently usesspdlog, although a framework-supported logging solution will be formally decided on later.Profiling and monitoring. Facilities such as
TimeTrackerandMemoryTrackerare infrastructure concerns that will be provided by the framework runtime; they do not need to expose a service handle to user code. Phlex does not currently provide profiling and monitoring facilities, yet they will likely be expressed as natively provided framework facilities rather than a user-facing service.Global-state wrappers. Objects like
TFileServicemanage global state in external libraries. That is a real need, but the management object can be provided as a constructor argument or explicit parameter rather than throughart::ServiceHandle.Shared read-only objects (e.g., Geometry). Objects that are read-only after initialization are not fundamentally services. In Phlex, such objects are data products belonging to a long-lived data layer and are provided to algorithms through the normal dataflow, not through a global handle.
Conditions and database-derived data. Calibration offsets, channel maps, and similar data that vary by run or time interval are exactly what framework-managed data layers are for. Rather than wrapping a database client in a service, the data should be fetched by a dedicated algorithm and placed into the appropriate data layer so downstream algorithms receive it as an explicit input. Currently, Phlex supports conditions data only through the same data layers that are used to drive the framework job. Support for more sophisticated conditions access is planned for future releases.
Some service uses are harder to eliminate before a full migration has been completed and may remain at the framework boundary during a staged separation. Even in such cases, the separation goal is the same: keep service calls confined to the module boundary and pass whatever the service provides as an ordinary C++ argument to the algorithm.