I'm struggling to balance encapsulation with managing side effects in a specific scenario described here.
- PriceSettings
< ActiveRecord::Base
(side-effect-based class methods, value-based instances) stores user parameters for price calculations - APIClient (adapter object) fetches statistics from an external API to use in price calculations
- PriceMatrix (value object) creates a matrix of prices for a range of days and quantities
- PriceAdjuster (value object) determines a total price adjustment for a specific day and quantity by combining several adjustments
- Adjustment::A (value object) calculates an adjustment factor from PriceSettings data only
- Adjustment::B (value object) calculates an adjustment factor from PriceSettings data, plus data from the APIClient
- GeneratePrices action (shell/integration object) Create and render a
PricingMatrix
- GeneratePrices loads a PriceSettings
- GeneratePrices creates PriceMatrix
- PriceMatrix(PriceSettings, ranges) creates a PriceAdjuster for each combination of date/quantity range values
- PriceAdjuster(PriceSettings, date, quantity) creates several Adjustment objects that require different inputs
- Adjustment::B(PriceSettings, date)
- Needs values from Price Settings
- Needs API data from APIClient for the specific
date
- Adjustment::B(PriceSettings, date)
- PriceAdjuster(PriceSettings, date, quantity) creates several Adjustment objects that require different inputs
- PriceMatrix(PriceSettings, ranges) creates a PriceAdjuster for each combination of date/quantity range values
From an encapsulation perspective, it makes sense for Adjustment::B to do its own API calls in order to fulfill its specific responsibilities. Other objects shouldn't do Adjustment::B's job for it. But this could be wildly inefficient, as the API calls would be scoped at the lowest level and therefore, might need to be repeated for each date/quantity combination.
From a value object perspective, it makes sense for Adjustment::B NOT to do its own API calls because value objects shouldn't have side effects. This could also solve potential efficiency issues because it would do a larger API call at the top level of the heirarchy and pass that data through the stack. But this would also seem to violate encapsulation since GeneratePrices would know about data that Adjustment::B needs and therefore would be passing all that data through PriceMatrix to PriceAdjuster, then through PriceAdjuster to Adjustment::B. Do you agree?
How would you structure things differently to avoid both the issue of value objects having side effects and the encapsulation problems?
Potential Solution
:pricing_input
or:"weekly_historical_prices(2015:5)"
), can look in this AdjustmentData container for data it needs by these keys. Multiple adjustments can use the same keys to share data.