Combining Patterns: Decorator + Strategy + Factory in One Feature
What This Concept Is
In textbooks, each pattern lives on its own page. In real code, several patterns often show up in the same feature because they absorb different pressures:
- Strategy swaps an algorithm.
- Decorator adds behavior around an object without subclassing.
- Factory (or Factory Method, or Abstract Factory) centralizes construction so the calling code does not need to know which concrete class it is getting.
Combining them is normal and useful, if each one still answers to its own pressure. Combining them to look clever is pattern salad.
Why It Matters Here
A Semester 3 project realistically has more than one variation point. A well-designed feature might legitimately:
- use a Strategy for a varying algorithm
- wrap that strategy in Decorators to layer logging, retries, or caching
- hide all the construction behind a Factory so call sites stay short
You should be able to compose these on purpose and, more importantly, recognize when someone else has stacked them in a PR. Reviewing a three-pattern feature is a different skill from naming a single pattern on a whiteboard.
Concrete Example
Feature: a PricingEngine for an online shop.
Pressures:
- pricing algorithms vary (regular, seasonal discount, loyalty-tier) -> Strategy
- every pricing call needs to be logged and cached, independent of algorithm -> Decorator
- different storefronts pick different strategies based on configuration; call sites should not
switchon storefront -> Factory
class PricingStrategy:
def price(self, cart): ...
class RegularPricing(PricingStrategy): ...
class SeasonalPricing(PricingStrategy): ...
class LoyaltyPricing(PricingStrategy): ...
class LoggingPricing(PricingStrategy):
def __init__(self, inner: PricingStrategy, logger):
self.inner, self.logger = inner, logger
def price(self, cart):
total = self.inner.price(cart)
self.logger.info("priced %s -> %s", cart.id, total)
return total
class CachingPricing(PricingStrategy):
def __init__(self, inner: PricingStrategy, cache):
self.inner, self.cache = inner, cache
def price(self, cart):
if cart.id in self.cache:
return self.cache[cart.id]
total = self.inner.price(cart)
self.cache[cart.id] = total
return total
class PricingFactory:
def __init__(self, logger, cache, config):
self.logger, self.cache, self.config = logger, cache, config
def build(self, storefront_id) -> PricingStrategy:
base = {
"regular": RegularPricing,
"seasonal": SeasonalPricing,
"loyalty": LoyaltyPricing,
}[self.config[storefront_id]]()
return LoggingPricing(CachingPricing(base, self.cache), self.logger)
Each pattern has a named pressure. Removing any one would either reintroduce a smell (Strategy gone -> big if) or a duplication (Decorator gone -> every strategy hand-rolls its own logging).
Common Confusion / Misconception
Pattern salad: stacking patterns where there is no pressure for them. A PR that introduces Strategy + Observer + Visitor for a feature with one variation point, one caller, and no traversal needs is not clever. It is noise. The review question is: what smell was each pattern absorbing? Every missing answer is a pattern to remove.
Layer confusion: using Decorator when Strategy was needed (or vice versa). Decorator adds around a consistent operation; Strategy replaces the operation. If the behavior itself changes, use Strategy. If the same behavior gains a side-duty, use Decorator.
Factory worship: hiding every new behind a Factory, even where direct construction is fine. Factories pay when construction is complex or varies by context. For a simple POJO/DTO, direct construction is clearer.
How To Use It
For a feature with more than one suspected pattern, write a small decision table before coding:
| Variation axis | Pressure | Candidate pattern | Accept the cost? |
|---|---|---|---|
| pricing algorithm | behavior swap | Strategy | yes |
| logging / caching | cross-cutting addition | Decorator | yes |
| storefront configuration | central assembly | Factory | yes |
| payment gateway | one implementation today | none yet | no |
The last row is critical. You say no to every pattern whose pressure is not already present.
Then, when you compose, follow the order: Strategy chooses the behavior, Decorator wraps it, Factory assembles both. If your composition cannot be stated in one or two sentences, it is probably salad.
Check Yourself
- Which pattern replaces behavior, and which pattern adds to behavior?
- Why might you wrap a Strategy with a Decorator rather than baking the extra behavior into each Strategy class?
- What would tell you in review that one of the three patterns does not belong?
Mini Drill or Application
For each feature, choose zero, one, two, or three patterns with justification:
- a notification service that sends SMS, email, or push, and needs retry and audit logging uniformly
- a report generator with a single PDF output and plans for HTML later "if the PM approves"
- an auth middleware that needs to run rate limiting then token validation then authorization, in order
- a storage adapter used by one service, backed by one database, with no plan to change