Refactor OceanConfig/Ocean to the Builder pattern#1343
Conversation
OceanConfig.build() now constructs the Prescriber and the surface-temperature model (prescribed or slab) and passes those built collaborators into Ocean, which no longer receives OceanConfig or builds any sub-object (Builder pattern rule 2). The prescribed/slab dispatch that Ocean.__call__ did via a self.type string becomes two small callable SurfaceTemperature objects, following the merged corrector template (#1313): a private _build() on the config assembles the collaborators; the impl is thin and reads no config fields. Add OceanConfig.is_slab so the coupled-stepper validation (and the downstream step-config wave) reads a public accessor instead of the raw slab field. Behavior-preserving; ocean unit tests build via config.build().
|
Claude: Independent pre-review (worktree-isolated, no authoring context). Ran the ace Audit passes
Verification (env slot on PR HEAD On the three judgment calls — I agree with all three:
Minor optional note (non-blocking): with no Ready for review. No structural issues, no blocking findings, behavior-preserving. |
| surface_temperature = PrescribedSurfaceTemperature( | ||
| self.surface_temperature_name | ||
| ) | ||
| else: | ||
| surface_temperature = SlabSurfaceTemperature( | ||
| surface_temperature_name=self.surface_temperature_name, | ||
| q_flux_name=self.slab.q_flux_name, | ||
| mixed_layer_depth_name=self.slab.mixed_layer_depth_name, | ||
| timestep=timestep, | ||
| ) |
There was a problem hiding this comment.
Note for reviewers: We could consider refactoring to have these two classes be explicitly defined config classes in this configuration, but the scope of this PR is explicitly not to change configuration or behavior.
There was a problem hiding this comment.
Claude: Agreed — kept promoting these to explicit config classes out of scope (no configuration/behavior change). Separately, per your other note I made the dataclasses frozen where it makes sense: both value objects (PrescribedSurfaceTemperature, SlabSurfaceTemperature) and the never-mutated OceanConfig/SlabOceanConfig are now frozen=True (29e3672). Full test suite still passes.
| return [self.mixed_layer_depth_name, self.q_flux_name] | ||
|
|
||
|
|
||
| class SurfaceTemperature(Protocol): |
There was a problem hiding this comment.
nit: Because we directly control all implementations of this class locally, make this an ABC inherited by its implementors, instead of a protocol.
There was a problem hiding this comment.
Claude: Done in 29e3672 — SurfaceTemperature is now an abc.ABC with an abstract __call__, inherited by PrescribedSurfaceTemperature and SlabSurfaceTemperature (no longer a structural Protocol).
- SurfaceTemperature is now an abc.ABC inherited by PrescribedSurfaceTemperature and SlabSurfaceTemperature, rather than a structural Protocol, since all implementations are controlled locally. - Freeze the value-object dataclasses (PrescribedSurfaceTemperature, SlabSurfaceTemperature) and the never-mutated config dataclasses (OceanConfig, SlabOceanConfig) with frozen=True.
spencerkclark
left a comment
There was a problem hiding this comment.
I think the way this simplifies the Ocean class is an improvement. I'm good with these changes, especially if it makes things more consistent with patterns used elsewhere in the code.
|
|
||
|
|
||
| @dataclasses.dataclass(frozen=True) | ||
| class SlabSurfaceTemperature(SurfaceTemperature): |
There was a problem hiding this comment.
nit: I have a slight preference for calling this SlabOceanSurfaceTemperature
| class SlabSurfaceTemperature(SurfaceTemperature): | |
| class SlabOceanSurfaceTemperature(SurfaceTemperature): |
Corrects the
fme/core/ocean.pyBuilder-pattern violation (the calibration example the pre-review skill cites) under theace obeys its Builder pattern passburn-down.OceanConfig.build()returnedOcean(config=self, ...), andOcean.__init__read raw config fields and built its ownPrescriber(rule 1 + rule 2 violations for a non-leaf config).OceanConfig.build()now constructs thePrescriberand the surface-temperature model, and passes those built collaborators intoOcean.Oceanno longer receivesOceanConfigand reads no config fields. Following the merged corrector template (#1313), a private_build()on the config does the assembly and the prescribed/slab dispatch thatOcean.__call__did via aself.typestring becomes two small callableSurfaceTemperatureobjects, each bundling the field names/operators it needs.Judgment calls (
OceanConfigis non-leaf viaslab):Oceanneeds (surface_temperature_name,ocean_fraction_name) are passed as explicit constructor arguments rather than packed into a non-dacite leaf dataclass — they are just field names, so a params bag would be boilerplate, not clearer separation. (Ocean.prescriberstays public: production step code calls it directly.)OceanConfig.is_slabas the public accessor the coupled-stepper validation (and the downstream step-config wave) reads instead of the rawslabfield; adopted at the one coupled-stepper.slab is not Noneread.The step family (
fme/core/step/*,fme/ace/step/fcn3.py, coupled steppers) still readsself._config.ocean.*and buildsOceanitself — those are the separate, downstream step-level fixes; this PR lands the Ocean exemplar they depend on.Changes:
fme.core.ocean:OceanConfig.build/_buildconstruct thePrescriberand aSurfaceTemperature(newPrescribedSurfaceTemperature/SlabSurfaceTemperaturecallables +SurfaceTemperatureprotocol);Ocean.__init__takes built collaborators, not the config; addOceanConfig.is_slab.fme.coupled.stepper: coupled-emulation validation readsOceanConfig.is_slab.fme.core.test_ocean: constructOceanviaconfig.build(...).Tests added (existing ocean unit tests updated to build via
config.build(); behavior-preserving)If dependencies changed, "deps only" image rebuilt and "latest_deps_only_image.txt" file updated