Liigu peamise sisu juurde

Strategy

Sissejuhatus

Enamik programmidest oma arendustsükklis jõuab lõpuks punkti, kus üht sama operatsiooni tuleb teha mitmel erineval viisil. Lihtsaim lähenemine oleks need valikud luua if/else või switch-lausete abil, kuid see seob kõik variatsioonid ühe klassi külge. Iga uus variatsioon nõuab selle muutmist, kuigi seda ümbritsev loogika jääb samaks.

Strateegia (strategy) muster lahendab seda probleemi, luues varieeruva käitumise jaoks ühise liidese ning teostades iga variatsiooni omaette klassis, võimaldades seda kasutaval klassil hoida viidet liidesele, mitte mõnele konkreetsele teostusele.

Probleemist lähemalt

Toome näiteks videomängu vaenlase tehisintellekti. Vaenlane käitub erinevalt sõltuvalt oma tervise tasemest: täis tervisega ründab ta mängijat otse, keskmise tervisega hoiab distantsi ning kasutab kaugrünnakuid, madala tervise korral põgeneb.

Ilma strateegiamustrita koonduvad kõik kolm käitumist ühte klassi:

public class Enemy {
private int health;

public void act(GameState state) {
if (health > 70) {
// move directly toward player, use melee attack
} else if (health > 30) {
// keep distance, fire projectiles at player
} else {
// retreat toward spawn point
}
}
}

Rakenduse kasvades tekib mitu probleemi:

  • Uue käitumise lisamine (näiteks varjumiskäitumine) tähendab Enemy-klassi avamist ja uue haru lisamist. Enemy ei peaks muutuma lihtsalt selle pärast, et lisati uus algoritm (tuletame avatud/suletud printsiipi meelde).
  • Käitumisi ei saa täiesti eraldatult testida, Enemy objekti olemasolu on vajalik testimisel.
  • Käitumise taaskasutus teistes vaenlaseklassides eeldab koodi duplikeerimist.

Strateegiamuster

Neid probleeme on võimalik lahendada strateegiamustriga, kus iga käitumine on omaette klassis ning kõik need klassid teostavad üht samat liidest:

public interface CombatStrategy {
void act(Enemy enemy, GameState state);
}
public class AggressiveStrategy implements CombatStrategy {
@Override
public void act(Enemy enemy, GameState state) {
// move directly toward player, use melee attack
}
}
public class RangedStrategy implements CombatStrategy {
@Override
public void act(Enemy enemy, GameState state) {
// keep distance from player, fire projectiles
}
}
public class FleeingStrategy implements CombatStrategy {
@Override
public void act(Enemy enemy, GameState state) {
// retreat toward spawn point
}
}

Enemy klass hoiab endas nüüd viidet CombatStrategy liidesele ning delegeerib käitumise sellele:

public class Enemy {
private int health;
private CombatStrategy strategy;

public Enemy(CombatStrategy strategy) {
this.health = 100;
this.strategy = strategy;
}

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

public void act(GameState state) {
strategy.act(this, state);
}

public void receiveDamage(int amount) {
this.health -= amount;
if (health <= 30) {
this.strategy = new FleeingStrategy();
} else if (health <= 70) {
this.strategy = new RangedStrategy();
}
}

public int getHealth() {
return health;
}
}

Tekkinud struktuuri klassidiagramm:

Strateegiat saab vahetada nii väljastpoolt (näiteks mängu raskusaste määrab algstrateegia) kui seestpoolt (vaenlane vahetab strateegiat ise, kui tervis langeb):

// difficulty setting decides starting behaviour
Enemy guardian = new Enemy(new AggressiveStrategy());

// strategy changes automatically as health drops
guardian.receiveDamage(40); // health → 60, switches to RangedStrategy
guardian.act(state); // now keeps distance and fires projectiles

guardian.receiveDamage(35); // health → 25, switches to FleeingStrategy
guardian.act(state); // now retreats toward spawn point

Sama Enemy objekt, erinev käitumine sõltuvalt tervisest:

Pane tähele:

  • Enemy klassis pole enam käitumise loogikat. See teab ainult, et mingisugune strateegia eksisteerib ning selle kaudu on võimalik käituda. Konkreetsetest algoritmidest pole Enemy klass üldse teadlik.
  • receiveDamage sisaldab küll tingimusi, ent need määravad ainult millal strateegiat vahetada, mitte kuidas käituda. Käitumise loogika ise asub strateegiates.
  • Uue käitumise lisamiseks tuleb lihtsalt luua uus klass, mis teostab CombatStrategy liidest. Enemy klass võib puutumata jääda.
  • Igat strateegiat on võimalik isoleeritult testida ilma Enemy objektita.

Seos sõltuvuste süstimisega

Strateegiamuster ja sõltuvuste süstimine näevad sarnased välja, see tähendab, et mõlemad edastavad objekte konstruktori kaudu liidese näol. Nende erinevus seisneb eesmärgis.

Sõltuvuste süstimine keskendub erinevate koodiosade lahtisidumisele - kutsuja annab koostööpartneri ette, et klass ei peaks seda ise looma. Strateegiamuster keskendub konkreetselt vahetatavate algoritmidele. Peamine omadus on, et strateegiat saab käigu pealt muuta setStrategy(...) meetodiga ning klassi käitumine muutub vastavalt.

Teisisõnu strateegiamuster kasutab sõltuvuste süstimist mehhanismina, et antud funktsionaalsust saavutada. Muster lisab arusaama, et süstitud objekt esindab vahetatavat algoritmi, mitte lihtsalt mõnda suvalist koostööpartnerit.

Kasutusjuhud

Strateegiamuster on sobilik, kui:

  • Klassil on käitumine, mis peaks ülejäänud loogikast sõltumatult varieeruma.
  • Sul on mitu klassi, mille ainus erinevus on see, et üht konkreetset operatsiooni on võimalik erinevalt läbi viia.
  • Soovid käitumist programmi käigu pealt vahetada, lähtudes kasutaja sisendist, konfiguratsioonist või kontekstist.
  • Tahad lisada uusi variatsioone ilma olemasolevat koodi muutmata.

See ei ole vajalik olukorras, kus on ainult üks teostus ning puudub realistlik vajadus teise lisamiseks. Kui programmi käitumine ei muutu kunagi, on liides ja lisaklassid lihtsalt üleliigne keerukus ilma tegeliku kasuta.