0% found this document useful (0 votes)
5 views16 pages

DesignPattern Strategy

The document discusses the Strategy Design Pattern, emphasizing its utility in managing interchangeable algorithms at runtime. It presents a scenario involving a list with varying printing and sorting requirements, illustrating how to avoid subclass explosion through the use of strategy interfaces. The document also provides concrete Java examples for implementing the pattern in both list sorting and checkout fee computation contexts.

Uploaded by

sanaaara2022
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
5 views16 pages

DesignPattern Strategy

The document discusses the Strategy Design Pattern, emphasizing its utility in managing interchangeable algorithms at runtime. It presents a scenario involving a list with varying printing and sorting requirements, illustrating how to avoid subclass explosion through the use of strategy interfaces. The document also provides concrete Java examples for implementing the pattern in both list sorting and checkout fee computation contexts.

Uploaded by

sanaaara2022
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Strategy Design Pattern — Interchangeable

Algorithms at Runtime
Problem Statement (motivating scenario)
Primary scenario (List printing and sorting): You are asked to build a simple integer list
type with the following API:

reverse(), get(i), set(i, a), sort(), print().

Two early clients disagree on printing:

• Horizontal print: all elements on one line, space-separated.

• Vertical print: one element per line.

A clean solution is:

1. Create an abstract base MyIntList with concrete reverse/get/set/sort and an abstract


print().

2. Implement sort() with a general-purpose algorithm (e.g., mergesort/quicksort).

3. Provide two subclasses: HorizontalPrintList and VerticalPrintList, each overriding print().

This is good enough for the initial printing variability.


However, four more clients arrive, each with data-specific sorting constraints:

• Client 1: list will contain only 0 and 1 ⇒ optimal counting/single-pass partition instead of
mergesort/quicksort.

• Client 2: list will be almost sorted ⇒ insertion sort is usually best.

• Client 3: list is already sorted descending ⇒ just reverse for ascending output.

• Client 4: completely random ⇒ quicksort/mergesort is fine.

Question: Do we create more subclasses or add another inheritance layer per sorting variant?
We’ll examine why that doesn’t scale and build toward Strategy.
Secondary scenario 1: Payment/checkout fee computation. A platform must com-
pute checkout fees using different policies (FixedFee, PercentageFee, TieredFee, MixedFee),
selectable at runtime per country/plan and swappable for A/B tests.
Secondary scenario 2: Game AI steering/pathfinding. NPCs select movement algo-
rithms (Seek, Evade) or pathfinding variants (AStarGrid, NavMeshAStar) at runtime per NPC
or difficulty.

Forces, Constraints, Goals


Across all scenarios:

• Runtime selection: tenant policies, level design, and experiments must toggle behaviors
without code edits.

• Testability and rollout: a crisp interface lets us unit-test each algorithm and safely flip it
via config/feature flags.

1
• Performance/determinism: choose the fastest correct algorithm for the data shape (binary
data, nearly sorted, etc.); preserve deterministic behavior (especially in games) and stable
rounding (in money).

• Configuration: inject immutable parameters; avoid mutable global state.

Naïve Alternatives and Why They Fail (with SOLID references)


Explosion of subclasses / deeper inheritance hierarchies
Suppose we start with:

MyIntList → {HorizontalPrintList, VerticalPrintList}

To add sort specialization per client, we might try subclasses like HorizontalBinaryList, VerticalBinaryList,
HorizontalAlmostSortedList, . . . This leads to a combinatorial explosion as we cross-printing
modes (2) with sort variants (4+), then with future axes (e.g., logging, bounds checks). It vi-
olates SRP (each subclass mixes printing and sorting policy) and harms OCP (new algorithm
forces many new classes).

 Listing 1: Anti-pattern: behavior gridlocked in inheritance 


abstract class MyIntListBase { /* reverse / get / set / sort / print ... */ }
class HorizontalPrintList extends MyIntListBase { /* print horizontal */ }
class VerticalPrintList extends MyIntListBase { /* print vertical */ }

// New requirements :
class HorizontalBinaryList extends HorizontalPrintList { /* override sort ()
w / count */ }
class VerticalBinaryList extends VerticalPrintList { /* override sort ()
w / count */ }
// ... and so on for each combination ( almost - sorted , desc , random )
 

Giant if/else inside sort()


Placing all data-shape branches inside one sort() violates SRP and OCP; it’s hard to test and
invites shotgun surgery.

 Listing 2: Anti-pattern: one method knows every algorithm 


class MyIntListBad {
void sort ( String dataProfile ) {
if ( " BINARY " . equals ( dataProfile ) ) { /* count sort */ }
else if ( " ALMOST " . equals ( dataProfile ) ) { /* insertion */ }
else if ( " DESC " . equals ( dataProfile ) ) { /* reverse */ }
else { /* quick / merge */ }
}
}
 

Static “god” utils


A [Link](list, profile) centralizes logic but couples clients to a concrete
utility (hurts DIP), hides state, and is awkward to mock.

2
Strategy: Core Idea and Intuition
Definition. Encapsulate a family of algorithms, make them interchangeable, and let the host
object (the context) hold a reference to one strategy and delegate work to it. Clients depend on
the interface, not concrete algorithms.
Intuition for the list problem. Keep printing as a class specialization (two small sub-
classes for horizontal/vertical), but extract sorting behind SortStrategy. Now you can choose
the optimal sorter (counting, insertion, reverse, quick/merge) at runtime per client or even per
call.

Design Heuristics
• Keep the strategy interface minimal : e.g., void sort(List<Integer> data) for the list, Money
fee(...) for checkout, Vector2 steer(...) for AI.

• Prefer stateless strategies with immutable configuration.

• Select via constructor injection, factory/registry, or configuration mapping; avoid instanceof


in clients.

• Document pre/postconditions (e.g., stable rounding; in-place sort).

• Add telemetry hooks (measure latency per strategy).

• Be explicit about time/space complexity and data-shape assumptions.

Concrete Java for the List Scenario


We first show the printing variability with inheritance (which is fine), then refactor sorting into
strategies.

Phase A — Printing via small subclasses

 Listing 3: [Link] – abstract base with default mergesort 


import java . util .*;

public abstract class MyIntList {


protected final List < Integer > data = new ArrayList < >() ;

public void add ( int x ) { data . add ( x ) ; }


public int get ( int i ) { return data . get ( i ) ; }
public void set ( int i , int a ) { data . set (i , a ) ; }

public void reverse () {


Collections . reverse ( data ) ;
}

/* * Default general - purpose sort ( e . g . , mergesort via Collections . sort


for stability ) . */
public void sort () {
Collections . sort ( data ) ; // TimSort ( merge - like ) in OpenJDK ; stable and
fast on runs .
}

3
/* * Abstract printing policy . */
public abstract void print () ;
}
 

 Listing 4: [Link] 
public class HorizontalPrintList extends MyIntList {
@Override public void print () {
for ( int i = 0; i < data . size () ; i ++) {
System . out . print ( data . get ( i ) ) ;
if ( i + 1 < data . size () ) System . out . print ( " " ) ;
}
System . out . println () ;
}
}
 

 Listing 5: [Link] 
public class VerticalPrintList extends MyIntList {
@Override public void print () {
for ( int x : data ) System . out . println ( x ) ;
}
}
 

 Listing 6: [Link] 
public class DemoPrinting {
public static void main ( String [] args ) {
MyIntList h = new HorizontalPrintList () ;
MyIntList v = new VerticalPrintList () ;
for ( int x : new int []{3 ,1 ,4 ,1 ,5}) { h . add ( x ) ; v . add ( x ) ; }

h . sort () ; h . print () ;
v . sort () ; v . print () ;
}
}
 
This solves printing nicely. But per-client sorting optimality still pushes us toward Strategy.

Phase B — Sorting via SortStrategy

 Listing 7: [Link] 
import java . util . List ;

public interface SortStrategy {


/* * Sort the list in - place ascending . Implementations must be
deterministic . */
void sort ( List < Integer > data ) ;
default String name () { return getClass () . getSimpleName () ; }
}
 

 Listing 8: [Link] – for data in {0,1} 


import java . util . List ;

public final class CountSort01 implements SortStrategy {


@Override public void sort ( List < Integer > data ) {

4
int zeros = 0;
for ( int x : data ) if ( x == 0) zeros ++;
for ( int i = 0; i < data . size () ; i ++) data . set (i , i < zeros ? 0 : 1) ;
}
}
 

 Listing 9: [Link] – great for nearly-sorted data 


import java . util . List ;

public final class InsertionSort implements SortStrategy {


@Override public void sort ( List < Integer > data ) {
for ( int i = 1; i < data . size () ; i ++) {
int key = data . get ( i ) ;
int j = i - 1;
while ( j >= 0 && data . get ( j ) > key ) {
data . set ( j + 1 , data . get ( j ) ) ;
j - -;
}
data . set ( j + 1 , key ) ;
}
}
}
 

 Listing 10: [Link] – if list is known to be descending 


import java . util . Collections ;
import java . util . List ;

public final class ReverseAlreadyDesc implements SortStrategy {


@Override public void sort ( List < Integer > data ) {
// Preconditions : data already sorted descending
Collections . reverse ( data ) ;
}
}
 

 Listing 11: [Link] – general-purpose for random data 


import java . util . List ;
import java . util . Random ;

public final class QuickSort implements SortStrategy {


private final Random rnd = new Random (42) ; // deterministic pivoting

@Override public void sort ( List < Integer > a ) {


qsort (a , 0 , a . size () - 1) ;
}
private void qsort ( List < Integer > a , int lo , int hi ) {
if ( lo >= hi ) return ;
int p = lo + rnd . nextInt ( hi - lo + 1) ;
int pivot = a . get ( p ) ;
int i = lo , j = hi ;
while ( i <= j ) {
while ( a . get ( i ) < pivot ) i ++;
while ( a . get ( j ) > pivot ) j - -;
if ( i <= j ) { int tmp = a . get ( i ) ; a . set (i , a . get ( j ) ) ; a . set (j , tmp ) ;
i ++; j - -; }
}

5
if ( lo < j ) qsort (a , lo , j ) ;
if ( i < hi ) qsort (a , i , hi ) ;
}
}
 

 Listing 12: [Link] – composition over inheritance 


import java . util . Objects ;

public class StrategyEnabledList extends HorizontalPrintList {


private SortStrategy sorter ;

public StrategyEnabledList ( SortStrategy initial ) {


this . sorter = Objects . requireNonNull ( initial ) ;
}

public void setSorter ( SortStrategy s ) { this . sorter = Objects .


requireNonNull ( s ) ; }

@Override public void sort () {


long t0 = System . nanoTime () ;
sorter . sort ( this . data ) ;
long t1 = System . nanoTime () ;
System . out . printf ( " [ Sort ] % -20 s took %.3 f ms % n " , sorter . name () , ( t1 -
t0 ) /1 e6 ) ;
}
}
 

 Listing 13: [Link] 


public class DemoSortingStrategies {
public static void main ( String [] args ) {
// Client 1: binary data
StrategyEnabledList bin = new StrategyEnabledList ( new CountSort01 () ) ;
for ( int x : new int []{1 ,0 ,1 ,1 ,0 ,0 ,1}) bin . add ( x ) ;
bin . sort () ; bin . print () ; // fast counting pass

// Client 2: almost sorted


StrategyEnabledList near = new StrategyEnabledList ( new InsertionSort () )
;
for ( int x : new int []{1 ,2 ,3 ,5 ,4 ,6 ,7}) near . add ( x ) ;
near . sort () ; near . print () ;

// Client 3: known descending


StrategyEnabledList desc = new StrategyEnabledList ( new
ReverseAlreadyDesc () ) ;
for ( int x : new int []{9 ,8 ,7 ,6 ,5}) desc . add ( x ) ;
desc . sort () ; desc . print () ;

// Client 4: random
StrategyEnabledList rnd = new StrategyEnabledList ( new QuickSort () ) ;
for ( int x : new int []{9 ,3 ,7 ,1 ,8 ,2 ,5}) rnd . add ( x ) ;
rnd . sort () ; rnd . print () ;

// \ emph { Hot - swap } at runtime :


rnd . setSorter ( new InsertionSort () ) ; // if we know it 's almost - sorted
next time
}

6
}
 

Why this is better than deeper inheritance. Printing remains a small, stable specializa-
tion (two subclasses). Sorting is a separate axis of variability, so we compose it via SortStrategy.
We avoid subclass multiplication, adhere to SRP (each strategy owns one algorithm), and sup-
port runtime switching (experiments, client-specific tuning).

Detailed Java Example (checkout fee strategies)


We now keep the earlier checkout example unchanged to reinforce Strategy in a different domain.

Money value object and checkout context

 Listing 14: [Link] 


// Money . java
import java . math . BigDecimal ;
import java . math . RoundingMode ;
import java . util . Objects ;

/* * Immutable money ( currency + amount ) with canonical 2 - decimal rounding .


*/
public final class Money implements Comparable < Money > {
private final String currency ;
private final BigDecimal amount ;

public Money ( String currency , BigDecimal amount ) {


this . currency = Objects . requireNonNull ( currency ) ;
this . amount = amount . setScale (2 , RoundingMode . HALF_UP ) ;
}

public static Money of ( String currency , String amount ) {


return new Money ( currency , new BigDecimal ( amount ) ) ;
}

public static Money zero ( String currency ) { return new Money ( currency ,
BigDecimal . ZERO ) ; }

public String currency () { return currency ; }


public BigDecimal amount () { return amount ; }

public Money plus ( Money other ) {


requireSameCurrency ( other ) ;
return new Money ( currency , amount . add ( other . amount ) ) ;
}

public Money minus ( Money other ) {


requireSameCurrency ( other ) ;
return new Money ( currency , amount . subtract ( other . amount ) ) ;
}

/* * Multiply by a percent : 2.9 -> 2.9% */


public Money percent ( BigDecimal pct ) {
BigDecimal factor = pct . movePointLeft (2) ; // 2.9 -> 0.029
return new Money ( currency , amount . multiply ( factor ) ) ;

7
}

public Money multiply ( BigDecimal factor ) {


return new Money ( currency , amount . multiply ( factor ) ) ;
}

public Money max ( Money other ) {


requireSameCurrency ( other ) ;
return this . compareTo ( other ) >= 0 ? this : other ;
}

@Override public int compareTo ( Money o ) {


requireSameCurrency ( o ) ;
return this . amount . compareTo ( o . amount ) ;
}

private void requireSameCurrency ( Money other ) {


if (! this . currency . equals ( other . currency ) )
throw new IllegalArgumentException ( " Currency mismatch : " + this .
currency + " vs " + other . currency ) ;
}

@Override public String toString () { return currency + " " + amount ; }


}
 

 Listing 15: [Link] 


// CheckoutContext . java
import java . util . Objects ;

public final class CheckoutContext {


private final String country ; // ISO -3166 -1 alpha -2 , e . g . , " US "
private final String currency ; // ISO -4217 , e . g . , " USD "
private final String gateway ; // e . g . , " Stripe " , " Adyen "
private final String merchantPlan ; // e . g . , " BASIC " , " PRO " , " ENTERPRISE "
private final Money cartSubtotal ;

public CheckoutContext ( String country , String currency , String gateway ,


String merchantPlan , Money cartSubtotal ) {
this . country = Objects . requireNonNull ( country ) ;
this . currency = Objects . requireNonNull ( currency ) ;
this . gateway = Objects . requireNonNull ( gateway ) ;
this . merchantPlan = Objects . requireNonNull ( merchantPlan ) ;
this . cartSubtotal = Objects . requireNonNull ( cartSubtotal ) ;
}

public String country () { return country ; }


public String currency () { return currency ; }
public String gateway () { return gateway ; }
public String merchantPlan () { return merchantPlan ; }
public Money subtotal () { return cartSubtotal ; }

@Override public String toString () {


return " ctx { " + country + " ," + currency + " ," + gateway + " ," +
merchantPlan + " , subtotal = " + cartSubtotal + " } " ;
}
}
 

8
Strategy interface and concrete strategies

 Listing 16: [Link] 


// FeeStrategy . java
public interface FeeStrategy {
/* * Compute the fee charged to the merchant . Must be >= 0 and same
currency as subtotal . */
Money fee ( Money subtotal , CheckoutContext ctx ) ;
/* * Optional : a short label for telemetry . */
default String name () { return getClass () . getSimpleName () ; }
}
 

 Listing 17: [Link] 


// FixedFee . java
import java . math . BigDecimal ;

public final class FixedFee implements FeeStrategy {


private final Money fixed ;

public FixedFee ( Money fixed ) {


if ( fixed . amount () . compareTo ( BigDecimal . ZERO ) < 0)
throw new IllegalArgumentException ( " fixed fee must be >= 0 " ) ;
this . fixed = fixed ;
}

@Override public Money fee ( Money subtotal , CheckoutContext ctx ) {


return Money . of ( subtotal . currency () , fixed . amount () . toPlainString () ) ;
}
}
 

 Listing 18: [Link] 


// PercentageFee . java
import java . math . BigDecimal ;

public final class PercentageFee implements FeeStrategy {


private final BigDecimal pct ; // e . g . , 2.9 means 2.9%

public PercentageFee ( BigDecimal pct ) {


if ( pct . compareTo ( BigDecimal . ZERO ) < 0) throw new
IllegalArgumentException ( " pct >= 0 " ) ;
this . pct = pct ;
}

@Override public Money fee ( Money subtotal , CheckoutContext ctx ) {


return subtotal . percent ( pct ) ;
}
}
 

 Listing 19: [Link] 


// TieredFee . java
import java . math . BigDecimal ;
import java . util . ArrayList ;
import java . util . List ;
import java . util . Objects ;

9
/* * Piecewise percentage schedule : for amount in [ threshold , next ) , apply
its pct . */
public final class TieredFee implements FeeStrategy {
public static final class Tier {
public final BigDecimal upToInclusive ; // e . g . , 100.00
public final BigDecimal pct ; // e . g . , 1.9 for 1.9%
public Tier ( BigDecimal upToInclusive , BigDecimal pct ) {
this . upToInclusive = upToInclusive ; this . pct = pct ;
}
}

private final List < Tier > tiers = new ArrayList < >() ;
private final BigDecimal lastPct ;

public TieredFee ( List < Tier > tiersOrderedByLimit , BigDecimal overflowPct )


{
Objects . requireNonNull ( tiersOrderedByLimit ) ;
if ( tiersOrderedByLimit . isEmpty () ) throw new IllegalArgumentException ( "
tiers required " ) ;
this . tiers . addAll ( tiersOrderedByLimit ) ;
this . lastPct = Objects . requireNonNull ( overflowPct ) ;
}

@Override public Money fee ( Money subtotal , CheckoutContext ctx ) {


BigDecimal remaining = subtotal . amount () ;
BigDecimal acc = BigDecimal . ZERO ;
BigDecimal prev = BigDecimal . ZERO ;

for ( Tier t : tiers ) {


BigDecimal span = t . upToInclusive . subtract ( prev ) ;
BigDecimal take = remaining . min ( span ) . max ( BigDecimal . ZERO ) ;
if ( take . signum () > 0) {
acc = acc . add ( take . multiply ( t . pct . movePointLeft (2) ) ) ;
remaining = remaining . subtract ( take ) ;
}
prev = t . upToInclusive ;
if ( remaining . signum () <= 0) break ;
}
// overflow
if ( remaining . signum () > 0) {
acc = acc . add ( remaining . multiply ( lastPct . movePointLeft (2) ) ) ;
}
return new Money ( subtotal . currency () , acc ) ;
}
}
 

 Listing 20: [Link] 


// MixedFee . java
/* * Example composition : maximum of a percentage fee and a fixed floor . */
public final class MixedFee implements FeeStrategy {
private final PercentageFee percentage ;
private final FixedFee floor ;

public MixedFee ( PercentageFee percentage , FixedFee floor ) {


this . percentage = percentage ; this . floor = floor ;
}

10
@Override public Money fee ( Money subtotal , CheckoutContext ctx ) {
Money a = percentage . fee ( subtotal , ctx ) ;
Money b = floor . fee ( subtotal , ctx ) ;
return a . max ( b ) ;
}
}
 

Context, registry, and demo

 Listing 21: [Link] 


// StrategyRegistry . java
import java . util . Map ;
import java . util . Objects ;
import java . util . concurrent . ConcurrentHashMap ;

/* * Simple runtime registry mapping ( country , plan ) -> FeeStrategy . */


public final class StrategyRegistry {
private final Map < String , FeeStrategy > byKey = new ConcurrentHashMap < >() ;

private static String key ( String country , String plan ) {


return country + " :: " + plan ;
}

public void register ( String country , String plan , FeeStrategy s ) {


byKey . put ( key ( country , plan ) , Objects . requireNonNull ( s ) ) ;
}

public FeeStrategy resolve ( CheckoutContext ctx , FeeStrategy fallback ) {


return byKey . getOrDefault ( key ( ctx . country () , ctx . merchantPlan () ) ,
fallback ) ;
}
}
 

 Listing 22: [Link] 


// FeeEngine . java
import java . util . Objects ;

public final class FeeEngine {


private FeeStrategy strategy ; // current policy
private final StrategyRegistry reg ; // selection source ( optional )

public FeeEngine ( FeeStrategy initial , StrategyRegistry reg ) {


this . strategy = Objects . requireNonNull ( initial ) ;
this . reg = reg ;
}

public void setStrategy ( FeeStrategy s ) { this . strategy = Objects .


requireNonNull ( s ) ; }

public Money compute ( CheckoutContext ctx ) {


FeeStrategy s = ( reg != null ) ? reg . resolve ( ctx , strategy ) : strategy ;
long t0 = System . nanoTime () ;
Money fee = s . fee ( ctx . subtotal () , ctx ) ;
long t1 = System . nanoTime () ;

11
System . out . printf ( " [ FeeEngine ] % -16 s fee =% s latency =%.3 fms % n " , s . name ()
, fee , ( t1 - t0 ) /1 e6 ) ;
return fee ;
}
}
 

 Listing 23: [Link] 


// MainDemo . java
import java . math . BigDecimal ;
import java . util . List ;

public class MainDemo {


public static void main ( String [] args ) {
Money subtotal = Money . of ( " USD " , " 125.00 " ) ;
CheckoutContext ctxUSBasic = new CheckoutContext ( " US " , " USD " , " Stripe " ,
" BASIC " , subtotal ) ;
CheckoutContext ctxUSPro = new CheckoutContext ( " US " , " USD " , " Stripe " ,
" PRO " , subtotal ) ;

// Define strategies
FeeStrategy fixed30c = new FixedFee ( Money . of ( " USD " , " 0.30 " ) ) ;
FeeStrategy pct29 = new PercentageFee ( new BigDecimal ( " 2.9 " ) ) ;
FeeStrategy mixed = new MixedFee ( new PercentageFee ( new BigDecimal ( "
1.9 " ) ) , new FixedFee ( Money . of ( " USD " ," 0.50 " ) ) ) ;
FeeStrategy tiered = new TieredFee (
List . of (
new TieredFee . Tier ( new BigDecimal ( " 100.00 " ) , new BigDecimal ( " 1.9 " ) )
,
new TieredFee . Tier ( new BigDecimal ( " 200.00 " ) , new BigDecimal ( " 1.7 " ) )
),
new BigDecimal ( " 1.5 " )
);

// Registry mapping country + plan to strategies


StrategyRegistry reg = new StrategyRegistry () ;
reg . register ( " US " , " BASIC " , pct29 ) ;
reg . register ( " US " , " PRO " , mixed ) ;

// Engine with default strategy and registry


FeeEngine engine = new FeeEngine ( fixed30c , reg ) ;

// Compute under registry - driven selection


engine . compute ( ctxUSBasic ) ; // PercentageFee (2.9%)
engine . compute ( ctxUSPro ) ; // MixedFee ( max (1.9% , 0.50) )

// Hot - swap at runtime


System . out . println ( " -- swap to tiered for everyone --" ) ;
engine . setStrategy ( tiered ) ;
CheckoutContext ctxCAEnt = new CheckoutContext ( " CA " , " USD " , " Adyen " , "
ENTERPRISE " , subtotal ) ;
engine . compute ( ctxCAEnt ) ; // uses TieredFee as fallback
}
}
 

Secondary Java Example (game development)

12
 Listing 24: [Link] 
// Vector2 . java
public final class Vector2 {
public final double x , y ;
public Vector2 ( double x , double y ) { this . x = x ; this . y = y ; }
public Vector2 add ( Vector2 o ) { return new Vector2 ( x + o .x , y + o . y ) ; }
public Vector2 sub ( Vector2 o ) { return new Vector2 ( x - o .x , y - o . y ) ; }
public Vector2 scale ( double s ) { return new Vector2 ( x * s , y * s ) ; }
public double length () { return Math . hypot (x , y ) ; }
public Vector2 normalized () { double L = length () ; return L == 0 ? this :
scale (1.0/ L ) ; }
@Override public String toString () { return String . format ( " (%.2 f , %.2 f ) " ,
x, y); }
}
 

 Listing 25: [Link] 


// SteeringState . java
public final class SteeringState {
public final Vector2 position ;
public final Vector2 target ;
public final Vector2 threat ;
public final double maxAccel ;
public SteeringState ( Vector2 position , Vector2 target , Vector2 threat ,
double maxAccel ) {
this . position = position ; this . target = target ; this . threat = threat ;
this . maxAccel = maxAccel ;
}
}
 

 Listing 26: [Link] 


// SteeringStrategy . java
public interface SteeringStrategy {
/* * Return desired acceleration vector . Deterministic per state . */
Vector2 steer ( SteeringState s ) ;
default String name () { return getClass () . getSimpleName () ; }
}
 

 Listing 27: [Link] 


// Seek . java
public final class Seek implements SteeringStrategy {
@Override public Vector2 steer ( SteeringState s ) {
Vector2 dir = s . target . sub ( s . position ) . normalized () ;
return dir . scale ( s . maxAccel ) ;
}
}
 

 Listing 28: [Link] 


// Evade . java
public final class Evade implements SteeringStrategy {
@Override public Vector2 steer ( SteeringState s ) {
Vector2 away = s . position . sub ( s . threat ) . normalized () ;
return away . scale ( s . maxAccel ) ;
}
}
 

13
 Listing 29: [Link] 
// Mover . java
public final class Mover {
private Vector2 velocity = new Vector2 (0 , 0) ;
private SteeringStrategy strategy ;

public Mover ( SteeringStrategy initial ) { this . strategy = initial ; }


public void setStrategy ( SteeringStrategy s ) { this . strategy = s ; }

public Vector2 update ( SteeringState state , double dt ) {


Vector2 accel = strategy . steer ( state ) ;
velocity = velocity . add ( accel . scale ( dt ) ) ;
return state . position . add ( velocity . scale ( dt ) ) ;
}
}
 

 Listing 30: [Link] 


// GameDemo . java
public class GameDemo {
public static void main ( String [] args ) {
SteeringStrategy seek = new Seek () ;
SteeringStrategy evade = new Evade () ;
Mover npc = new Mover ( seek ) ;

SteeringState state = new SteeringState (


new Vector2 (0 ,0) ,
new Vector2 (10 ,0) , // target
new Vector2 ( -5 ,0) , // threat
5.0 // max acceleration
);

// Move with Seek


Vector2 pos1 = npc . update ( state , 0.1) ;
System . out . println ( seek . name () + " pos = " + pos1 ) ;

// Low health ? Swap to Evade at runtime


npc . setStrategy ( evade ) ;
Vector2 pos2 = npc . update ( new SteeringState ( pos1 , state . target , state .
threat , 5.0) , 0.1) ;
System . out . println ( evade . name () + " pos = " + pos2 ) ;
}
}
 

Valid doubt: Is Strategy the same as State? / Answer:


Valid doubt: “Isn’t this the same as State?”
Answer: No. Strategy externalizes which algorithm to apply (chosen by configuration or con-
text). State models an object’s internal state machine; behavior changes as it transitions. You
may change a Strategy arbitrarily; State transitions are constrained by the state model.

14
Valid doubt: How is Strategy different from Template Method? /
Answer:
Valid doubt: “How is this different from Template Method ?”
Answer: Template Method fixes the skeleton of an algorithm in an abstract base class with
overridable steps (inheritance). Strategy replaces the entire algorithm behind a stable interface
(composition), enabling runtime swapping and reducing inheritance coupling.

Valid doubt: Why not Decorator? / Answer:


Valid doubt: “Why not Decorator ?”
Answer: Decorator stacks behaviors (often many at once). Strategy selects one algorithm. In
our list scenario, using Decorator to add “counting” on top of “quicksort” would double-sort; we
need a single chosen policy.

Valid doubt: Different exceptions/return shapes? / Answer:


Valid doubt: “Can strategies throw different exceptions or return different shapes?”
Answer: Maintain a consistent contract (LSP). For sorting, all strategies must sort ascending
in-place and be deterministic; for fees, always return non-negative Money in the same currency.
Expose diagnostics via telemetry instead of changing signatures.

Testing Strategy
List strategies: property tests (idempotence of sort; monotonicity; stability expectations where
relevant), edge cases (empty, singleton, all equal, already sorted, reverse-sorted).
Fees: contract tests (non-negative, currency preservation), golden cases for tiers, and micro-
benchmarks per strategy.

 Listing 31: [Link] (JUnit 5 snippet) 


// TieredFeeTest . java
import static org . junit . jupiter . api . Assertions .*;
import org . junit . jupiter . api . Test ;
import java . math . BigDecimal ;
import java . util . List ;

class TieredFeeTest {
@Test void thresholds_are_applied_correctly () {
TieredFee tf = new TieredFee (
List . of (
new TieredFee . Tier ( new BigDecimal ( " 100.00 " ) , new BigDecimal ( " 1.0 " ) )
, // 1% up to 100
new TieredFee . Tier ( new BigDecimal ( " 200.00 " ) , new BigDecimal ( " 2.0 " ) )
// 2% up to 200
),
new BigDecimal ( " 3.0 " ) // 3% beyond
);
Money fee150 = tf . fee ( Money . of ( " USD " ," 150.00 " ) ,
new CheckoutContext ( " US " ," USD " ," G " ," BASIC " , Money . of ( " USD " ," 150.00 "
)));
// 100*1% + 50*2% = 1.00 + 1.00 = 2.00
assertEquals ( " USD 2.00 " , fee150 . toString () ) ;
}

15
}
 

Mapping to SOLID
SRP. Each strategy class owns one algorithm (printing subclasses only print; SortStrategy
only sorts).
OCP. Add a new sorting or fee policy without modifying the clients or contexts.
LSP. All strategies respect the same contract (sorted ascending; non-negative fees).
ISP. Lean interfaces (sort(List<Integer>), fee(...)).
DIP. Clients depend on SortStrategy/FeeStrategy/SteeringStrategy, not concretes.

Pitfalls and How to Avoid Them


Leaky selection logic. Centralize mapping from context to strategy in a registry/factory.
Hidden mutability. Keep configuration immutable; seed any randomness for determinism.
Units/rounding bugs. Define canonical rounding policy (see Money).
Strategy proliferation. Assign ownership, regularly prune/merge similar strategies.
Overuse. If you never change the algorithm, Strategy might be over-engineering (YAGNI).

When to Use / When Not to Use


Use when behavior must be selected at runtime, per tenant/level/data shape, or when running
A/B tests.
Avoid when you need composed cross-cutting behavior (Decorator), workflow coordination (Fa-
cade), interface translation (Adapter), or when there’s only ever one algorithm.

Mini Walkthrough / Exercise


List:

1. Start with MyIntList + HorizontalPrintList/VerticalPrintList; verify printing variants.

2. Introduce SortStrategy and wire StrategyEnabledList; run the four client profiles using
CountSort01, InsertionSort, ReverseAlreadyDesc, QuickSort.

3. Hot-swap sorters at runtime and record timings.

Checkout: run MainDemo and flip registry mappings.


Game: start NPC with Seek; switch to Evade when health drops; observe trajectory change.

Takeaway
Strategy separates policy from mechanics. In the list example, printing is a simple subclass
choice, while sorting becomes a plug-in policy chosen for data shape and switched at runtime.
In payments and games, the same idea prevents if/else bloat, enables safe experiments, and
keeps clients stable as algorithms evolve. Small interface, big leverage.

16

You might also like