Table of Contents
Strategy Design Pattern
Το Strategy Pattern (Πρότυπο Στρατηγικής) ανήκει στην κατηγορία των προτύπων συμπεριφοράς (behavioral design patterns). Η βασική του ιδέα είναι να ορίσετε μια οικογένεια αλγορίθμων, να τους ενθυλακώσετε (encapsulate) σε ξεχωριστές κλάσεις και να τους καταστήσετε διαθέσιμους προς χρήση από άλλες κλάσεις που για χάρην συντομίας θα τους ονομάσουμε πελάτες. Με αυτόν τον τρόπο, ο αλγόριθμος που χρησιμοποιεί ένας πελατης μπορεί να αλλάζει ανάλογα με τις ανάγκες του.
Το Strategy Pattern αποτελείται από τρία κύρια μέρη:
- Strategy (Interface): Μια κοινή διεπαφή για όλους τους υποστηριζόμενους αλγορίθμους.
- Concrete Strategies: Κλάσεις που υλοποιούν τη διεπαφή Strategy χρησιμοποιώντας έναν συγκεκριμένο αλγόριθμο.
- Context: Η κλάση που διατηρεί μια αναφορά σε ένα αντικείμενο Strategy και τη χρησιμοποιεί για να εκτελέσει την εργασία της.
Παρακάτω δίνεται μία περιγραφή της διάρθρωσης των κλάσεων του Strategy Pattern.
classDiagram
class Context {
- strategy: Strategy
+ setStrategy(Strategy)
+ callStrategy()
}
interface Strategy {
+ execute_strategy()
}
class ConcreteStrategyA extends Strategy {
+ execute_strategy()
}
class ConcreteStrategyB extends Strategy {
+ execute_strategy()
}
Context --> Strategy
Strategy <|.. ConcreteStrategyA
Strategy <|.. ConcreteStrategyB
1o Παράδειγμα - Shopping Cart με διαφορετικούς τρόπους πληρωμής
Φανταστείτε ένα e-shop όπου ο χρήστης μπορεί να επιλέξει πώς θα πληρώσει: με Πιστωτική Κάρτα ή με PayPal. Αντί να γεμίσουμε τον κώδικα με if-else ή switch, χρησιμοποιούμε το Strategy Pattern. Αρχικά ορίζουμε ένα interface που περιγράφει τη μέθοδο (“στρατηγική”) πληρωμής.
- PaymentStrategy.java
public interface PaymentStrategy { void pay(double amount); }
- StrategyImplementations.java
// Στρατηγική 1η: Πιστωτική Κάρτα class CreditCardStrategy implements PaymentStrategy { private String name; private String cardNumber; public CreditCardStrategy(String name, String cardNumber) { this.name = name; this.cardNumber = cardNumber; } @Override public void pay(double amount) { System.out.println("Πληρωμή " + amount + "€ με Πιστωτική Κάρτα (Κάτοχος: " + name + ")."); } } // Στρατηγική 2η: PayPal class PayPalStrategy implements PaymentStrategy { private String email; public PayPalStrategy(String email) { this.email = email; } @Override public void pay(double amount) { System.out.println("Πληρωμή " + amount + "€ μέσω PayPal (Email: " + email + ")."); } }
Αφού έχουμε ορίσει τις διαφορετικές στρατηγικές μπορούμε να τις χρησιμοποιήσουμε για να γίνονται πληρωμές στο καλάθι αγορών. Ο τελικός χρήστης μπορεί να ορισει δυναμικά τον τρόπο πληρωμής, αφού δημιουργηθεί το καλάθι.
- ShoppingCart.java
public class ShoppingCart { private double totalAmount; private PaymentStrategy strategy; public ShoppingCart(double amount) { this.totalAmount = amount; } // Επιτρέπει την αλλαγή στρατηγικής κατά το runtime (Dependency Injection) public void setPaymentStrategy(PaymentStrategy strategy) { this.strategy = strategy; } public void checkout() { if (strategy == null) { System.out.println("[ERROR]: No payment method specified!"); } else { strategy.pay(totalAmount); } } }
Τα παραπάνω μας δίνουν τη δυνατότητα να προσομοιώσουμε το καλάθι πληρωμών, δημιουργώντας διαφορετικά καλάθια, που στο καθένα θέτουμε διαφορετικό τρόπο πληρωμής. Ο τρόπος πληρωμής μπορεί να αλλάξει σε ένα καλάθι, όπως συμβαίνει στο cart3.
- ShoppingCartUsage.java
public class ShoppingCartUsage { public static void main(String[] args) { // Επιλογή 1: Πληρωμή με PayPal ShoppingCart cart1 = new ShoppingCart(150.75); cart1.setPaymentStrategy(new PayPalStrategy("user@example.com")); cart1.checkout(); // Επιλογή 2: Ο χρήστης αλλάζει γνώμη και βάζει κάρτα ShoppingCart cart2 = new ShoppingCart(23.5); cart2.setPaymentStrategy(new CreditCardStrategy("John Doe", "1234-5678-9012")); cart2.checkout(); ShoppingCart cart3 = new ShoppingCart(18.20); cart3.setPaymentStrategy(new CreditCardStrategy("Marry Roberts", "9012-5678-1234")); cart1.setPaymentStrategy(new PayPalStrategy("marry.roberts@hello.com")); cart3.checkout(); } }
Τα πλεονεκτήματα εδώ:
- Loose Coupling: Η κλάση ShoppingCart εξαρτάται από το PaymentStrategy (abstraction) και όχι από το CreditCardStrategy (concretion).
- Testability: Μπορείτε εύκολα να δημιουργήσετε ένα “Mock” Payment Strategy για να ελέγξετε αν το ShoppingCart δουλεύει σωστά χωρίς να κάνετε πραγματικές πληρωμές.
- Επεκτασιμόττα: Αν αύριο θέλετε να προσθέσετε πληρωμή με Bitcoin, απλώς δημιουργείτε την κλάση BitcoinStrategy implements PaymentStrategy χωρίς να πειράξετε ούτε μία γραμμή από τον υπάρχοντα κώδικα του καλαθιού.
Σχέση Strategy Pattern και Κληρονομικότητας
Η σχέση μεταξύ Strategy Pattern και Κληρονομικότητας είναι μια από τις πιο θεμελιώδεις συζητήσεις στον αντικειμενοστραφή προγραμματισμό. Συνοψίζεται σε ένα κλασικό ρητό: “Προτιμήστε τη Σύνθεση αντί για την Κληρονομικότητα” (Favor Composition over Inheritance). Η διαφορά μεταξύ κληρονομικότητας και Strategy Pattern συνοψίζεται στο εξής:
- Κληρονομικότητα (Inheritance): Ορίζει τι ΕΙΝΑΙ ένα αντικείμενο (σχέση “is-a”). Η συμπεριφορά καθορίζεται κατά το compile-time και είναι στατική.
- Strategy Pattern (Composition): Ορίζει τι ΚΑΝΕΙ ή τι ΕΧΕΙ ένα αντικείμενο (σχέση “has-a”). Η συμπεριφορά καθορίζεται κατά το runtime και είναι δυναμική.
Πότε επιλέγουμε το κάθε ένα;
| Χαρακτηριστικό | Κληρονομικότητα | Strategy Pattern (Σύνθεση) |
|---|---|---|
| Συμπεριφορά | Στατική (δεν αλλάζει αφού δημιουργηθεί το αντικείμενο). | Δυναμική (αλλάζει “on the fly”). |
| Ευελιξία | Δύσκαμπτη. Αν θες διαφορετικό συνδυασμό συμπεριφορών, καταλήγεις σε μεγάλο αριθμό διαφορετικών κλάσεων. | Μεγάλη. Μπορείς να συνδυάζεις διαφορετικά αντικείμενα στρατηγικής. |
| Σχέση | Ισχυρή σύζευξη (Tight Coupling). | Χαλαρή σύζευξη (Loose Coupling). |
2ο Παράδειγμα: Σύστημα Χαρακτήρων Παιχνιδιού
Φανταστείτε ένα παιχνίδι με χαρακτήρες που χρησιμοποιούν όπλα.
Α. Προσέγγιση με Κληρονομικότητα
Αν χρησιμοποιήσουμε κληρονομικότητα, θα έπρεπε να φτιάξουμε κλάσεις όπως WarriorWithSword, WarriorWithBow, KnightWithSword κ.λπ. Αν προσθέσουμε ένα νέο όπλο, πρέπει να φτιάξουμε δεκάδες νέες κλάσεις.
- DifferentCharacters.java
// Στατικό και δύσκαμπτο class WarriorWithSword extends Character { void attack() { System.out.println("Attack using sword!"); } } class WarriorWithBow extends Character { void attack() { System.out.println("Attack using bow!"); } } class WarriorWithSpear extends Character { void attack() { System.out.println("Attack using long spear!"); }
Ενώ έχουμε έχουμε δημιουργήσει πολλές κλάσεις, δεν έχουμε την δυνατότητα ένας πολλεμιστής να αλλάξει όπλο ή να προσθέσουμε όπλο. Είναι “κλειδωμένος” στην υλοποίηση της κλάσης του.
Β. Προσέγγιση με Strategy Pattern
Εδώ, ο χαρακτήρας έχει ένα όπλο που υλοποιείται μέσω του Strategy Pattern, αλλά δεν είναι άρρικτα συνδεδεμένος με το όπλο του. Το όπλο μπορεί να αλλάξει στην εξέλιξη του παιχνιδιού.
- ActionGame.java
// 1. Το Strategy Interface interface WeaponBehavior { void useWeapon(); } // 2. Concrete Strategies class SwordBehavior implements WeaponBehavior { public void useWeapon() { System.out.println("Attack with sword!"); } } class BowBehavior implements WeaponBehavior { public void useWeapon() { System.out.println("Attack with bow!"); } } // 3. Το Context (Χαρακτήρας) abstract class GameCharacter { protected WeaponBehavior weapon; // Σύνθεση αντί για κληρονομικότητα public void setWeapon(WeaponBehavior w) { this.weapon = w; } public void attack() { weapon.useWeapon(); } } class Knight extends GameCharacter { public Knight(WeaponBehavior) { setWeapeon(WeaponBehavior); // Αρχικό όπλο } } // 4. Η διαφορά στην πράξη public class ActionGame { public static void main(String[] args) { GameCharacter arthurs = new Knight(new SwordBehavior()); arthurs.performAttack(); // Χτύπημα με Σπαθί! // ΑΛΛΑΓΗ ΣΥΜΠΕΡΙΦΟΡΑΣ at RUNTIME (Αδύνατο με κληρονομικότητα) System.out.println("The knight finds a bow!!!"); arthurs.setWeapon(new BowBehavior()); arthurs.performAttack(); // Επίθεση με Τόξο! } }
