Strategy Pattern: කෝඩ් එකේ නම්යශීලී බව වැඩි කරමු! | SC Guide

කෝඩ් එකේ නම්යශීලී බව වැඩි කරමු: Strategy Pattern ගැන සම්පූර්ණ Guide එකක්!
කට්ටියටම ආයුබෝවන්! සොෆ්ට්වෙයාර් ඉංජිනේරුවරයෙක් විදියට වැඩ කරනකොට අපි හැමෝටම අත්දකින්න ලැබෙන පොදු අභියෝගයක් තමයි වෙනස්වීම් වලට අනුව කෝඩ් එක හදාගන්න එක. අලුත් ෆීචර්ස් එකතු කරනකොට, දැනට තියෙන ඒවා වෙනස් කරනකොට, අපේ කෝඩ් එක නම්යශීලී (flexible) නැත්නම්, වැඩේ ටිකක් අමාරු වෙනවා. ඒ වගේම, කෝඩ් එක එක තැනක වෙනස් කරනකොට, අනිත් තැන් වලටත් බලපානවා නම්, ඒක ලොකු headache එකක්. Design Patterns කියන්නේ මේ වගේ ප්රශ්න වලට හොඳ විසඳුම් සපයන, ඔප්පු කරපු ක්රමවේද ගොඩක්. අද අපි කතා කරන්න යන්නේ ඒ අතරින් Strategy Pattern එක ගැන.
මේ රටාව අපිට උදව් කරන්නේ අපේ ඇල්ගොරිතම (algorithms) වෙනස් කරන්න පුළුවන් විදියට කෝඩ් කරන්න. ඒ කියන්නේ, අපිට ඕන වෙලාවට අපිට ඕන ඇල්ගොරිතමයක් පාවිච්චි කරන්න පුළුවන් වෙන විදියට අපේ පද්ධතිය හදන්න මේක ගොඩක් ප්රයෝජනවත් වෙනවා. අද අපි මේක උදාහරණ එක්ක පැහැදිලිව බලමු!
Strategy Pattern එක කියන්නේ මොකක්ද?
සරලව කිව්වොත්, Strategy Pattern එක කියන්නේ හැසිරීම් (behaviors) වෙනස් කරන්න පුළුවන් විදියට කෝඩ් කරන්න අපිට උදව් කරන Design Pattern එකක්. අපි හිතමු අපිට එකම කාර්යයක් කරන්න පුළුවන්, හැබැයි විවිධ ක්රම වලට වැඩ කරන ඇල්ගොරිතම කිහිපයක් තියෙනවා කියලා. උදාහරණයක් විදියට, භාණ්ඩයක් සඳහා Delivery Charges ගණනය කරන විවිධ ක්රම (Free Delivery, Flat Rate, Per KM Rate) තියෙන්න පුළුවන්. මේ වගේ අවස්ථාවලදී, අපි සාමාන්යයෙන් if-else
හෝ switch-case
වගේ statement පාවිච්චි කරනවා.
// Bad example with if-else
public double calculateDeliveryCharge(String deliveryType, double distance) {
if ("free".equals(deliveryType)) {
return 0;
} else if ("flatRate".equals(deliveryType)) {
return 150;
} else if ("perKmRate".equals(deliveryType)) {
return distance * 50;
} else {
throw new IllegalArgumentException("Invalid delivery type");
}
}
මේ වගේ කෝඩ් එකක ප්රශ්නේ මොකක්ද? අලුත් Delivery Type එකක් ආවොත්, මේ method එක ඇතුලට ගිහින් වෙනස්කම් කරන්න වෙනවා. එතකොට දැනට තියෙන කෝඩ් එකත් වෙනස් වෙනවා (Violation of Open/Closed Principle). ඒ වගේම මේ method එක ගොඩක් විශාල වෙන්න පුළුවන්, තේරුම් ගන්නත් අමාරු වෙන්න පුළුවන්.
Strategy Pattern එක මේකට විසඳුමක් දෙනවා. මේකෙදි අපි කරන්නේ, එකිනෙකට වෙනස් ඇල්ගොරිතම (Strategies) වෙනම Class වලට වෙන් කරන එක. ඒ හැම Class එකක්ම පොදු Interface එකක් හෝ Abstract Class එකක් Implement කරනවා. මේ නිසා, අපිට ඇල්ගොරිතමයන් ධාවනය වෙමින් තිබෙන අවස්ථාවකදී (runtime) වුවද මාරු කරන්න (interchangeable) පුළුවන් වෙනවා.
ඇයි Strategy Pattern එක පාවිච්චි කරන්න ඕන?
Strategy Pattern එක පාවිච්චි කරන එකෙන් අපිට ගොඩක් වාසි තියෙනවා:
- නම්යශීලී බව (Flexibility): අපිට ඇල්ගොරිතම පහසුවෙන් මාරු කරන්න පුළුවන්. අලුත් ඇල්ගොරිතමයක් එකතු කරනවා නම්, දැනට තියෙන කෝඩ් එක වෙනස් කරන්න ඕන වෙන්නේ නැහැ.
- නැවත භාවිතය (Reusability): එකම ඇල්ගොරිතමයක් පද්ධතියේ විවිධ තැන් වලදී නැවත භාවිත කරන්න පුළුවන්.
- පහසු නඩත්තු කිරීම (Easier Maintenance): එක් එක් ඇල්ගොරිතමය වෙනම Class එකක තියෙන නිසා, වෙනස්කම් කරන්න සහ වැරදි හොයාගන්න පහසුයි.
- SOLID Principles වලට අනුකූල වීම: විශේෂයෙන්ම Open/Closed Principle (මෘදුකාංග entities විවෘත විය යුත්තේ දිගු කිරීමට මිස වෙනස් කිරීමට නොවේ) මේ රටාවෙන් හොඳින් ආරක්ෂා වෙනවා.
- පරීක්ෂා කිරීම (Testability): එක් එක් ඇල්ගොරිතමය වෙනම Unit test කරන්න පුළුවන්.
Strategy Pattern එක වැඩ කරන්නේ කොහොමද?
Strategy Pattern එකට ප්රධාන කොටස් තුනක් තියෙනවා:
- Strategy Interface / Abstract Class:මේක තමයි හැම ඇල්ගොරිතමයකටම පොදු Interface එක. හැම Concrete Strategy Class එකක්ම මේ Interface එක Implement කරන්න ඕන. මේකෙදි අපි අපිට ඕන කරන method එක define කරනවා.
- Concrete Strategy Classes:මේවා තමයි Strategy Interface එක Implement කරලා, තමන්ගේම logic එක තියෙන Class. උදාහරණයක් විදියට, Credit Card Payment, PayPal Payment වගේ වෙන වෙනම Class හැදෙනවා.
- Context Class:මේ Context Class එක තමයි Strategy Object එකක් (interface එකෙන්) තියාගෙන ඉන්නේ. මේ Class එකට තමන්ගේම logic එකක් නැහැ, ඒ වෙනුවට තමන්ට assign කරලා තියෙන Strategy Object එකට වැඩේ බාර දෙනවා (delegates). මේ Class එකට පුළුවන් runtime එකේදී පවා Strategy එක වෙනස් කරන්න.
පහත රූප සටහනෙන් මේ කොටස් අතර සම්බන්ධය දැක්වෙනවා (conceptual diagram).
+-----------------+ +---------------------+ +------------------------+
| Context | | <<interface>> | | ConcreteStrategyA |
|-----------------| | IStrategy | |------------------------|
| - strategy: IStrategy |<----|---------------------| | + execute(): void |
|-----------------| | + execute(): void |<-----+------------------------+
| + setStrategy(IStrategy)| +---------------------+ | ConcreteStrategyB |
| + executeStrategy() | |------------------------|
+-----------------+ | + execute(): void |
+------------------------+
ප්රායෝගික උදාහරණයක්: Payment Strategies
අපි හිතමු අපිට Online Store එකක් තියෙනවා කියලා. Customer කෙනෙක්ට විවිධ ක්රම වලට Payment කරන්න පුළුවන්: Credit Card, PayPal, Bank Transfer. අලුත් Payment Gateway එකක් ආවොත්, අපේ කෝඩ් එකට පහසුවෙන් එකතු කරන්න පුළුවන් විදියට මේක හදමු.
පියවර 1: Payment Strategy Interface එක හදමු
මුලින්ම, හැම Payment ක්රමයකටම පොදු Interface එකක් හදමු.
// PaymentStrategy.java
public interface PaymentStrategy {
void pay(double amount);
}
පියවර 2: Concrete Strategy Classes හදමු
දැන් අපි Credit Card, PayPal, Bank Transfer කියන Payment ක්රම වෙන වෙනම Class විදියට හදමු. මේ හැම එකක්ම PaymentStrategy
Interface එක Implement කරනවා.
// CreditCardPayment.java
public class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
private String name;
public CreditCardPayment(String cardNumber, String name) {
this.cardNumber = cardNumber;
this.name = name;
}
@Override
public void pay(double amount) {
System.out.println(amount + " LKR paid with Credit Card (Card No: " + cardNumber + ").");
// Logic to process credit card payment
}
}
// PayPalPayment.java
public class PayPalPayment implements PaymentStrategy {
private String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(double amount) {
System.out.println(amount + " LKR paid with PayPal (Email: " + email + ").");
// Logic to process PayPal payment
}
}
// BankTransferPayment.java
public class BankTransferPayment implements PaymentStrategy {
private String bankAccount;
public BankTransferPayment(String bankAccount) {
this.bankAccount = bankAccount;
}
@Override
public void pay(double amount) {
System.out.println(amount + " LKR paid with Bank Transfer (Account: " + bankAccount + ").");
// Logic to process bank transfer payment
}
}
පියවර 3: Context Class එක හදමු (ShoppingCart / OrderProcessor)
දැන් අපි Context Class එක හදමු. මේක Payment ක්රියාවලිය handle කරන Class එක. මේකෙදි අපි PaymentStrategy
Object එකක් තියාගෙන ඉන්නවා.
// ShoppingCart.java (or OrderProcessor)
public class ShoppingCart {
private PaymentStrategy paymentStrategy;
private double totalAmount;
public ShoppingCart(double totalAmount) {
this.totalAmount = totalAmount;
}
// Setter method to set/change the payment strategy
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void checkout() {
if (paymentStrategy == null) {
System.out.println("No payment method selected.");
return;
}
System.out.println("Processing payment for total amount: " + totalAmount + " LKR");
paymentStrategy.pay(totalAmount);
}
}
පියවර 4: භාවිත කරන ආකාරය (Client Code)
දැන් අපිට අපේ Online Store එකේදී මේක පාවිච්චි කරන්න පුළුවන්. Customer තෝරන Payment ක්රමයට අනුව අපිට PaymentStrategy
එක set කරන්න පුළුවන්.
// Main application or Client code
public class OnlineStoreApp {
public static void main(String[] args) {
double orderAmount = 1500.00;
// Customer chooses Credit Card payment
ShoppingCart cart1 = new ShoppingCart(orderAmount);
cart1.setPaymentStrategy(new CreditCardPayment("1234-5678-9012-3456", "Kasun Perera"));
System.out.println("--- Order 1 ---");
cart1.checkout();
System.out.println();
// Customer chooses PayPal payment
ShoppingCart cart2 = new ShoppingCart(2500.50);
cart2.setPaymentStrategy(new PayPalPayment("[email protected]"));
System.out.println("--- Order 2 ---");
cart2.checkout();
System.out.println();
// Customer chooses Bank Transfer payment
ShoppingCart cart3 = new ShoppingCart(500.00);
cart3.setPaymentStrategy(new BankTransferPayment("001-2345-6789"));
System.out.println("--- Order 3 ---");
cart3.checkout();
System.out.println();
// What if we need a new payment method like Cryptocurrency?
// We just create a new CryptocurrencyPayment class implementing PaymentStrategy
// No need to modify ShoppingCart class!
// ShoppingCart cart4 = new ShoppingCart(1000.00);
// cart4.setPaymentStrategy(new CryptocurrencyPayment("ETH_Wallet_Address_XYZ"));
// System.out.println("--- Order 4 ---");
// cart4.checkout();
}
}
උඩ කෝඩ් එක බැලුවොත්, අපිට පේනවා ShoppingCart
Class එකට අලුත් Payment ක්රමයක් එකතු කරනකොට ඒක වෙනස් කරන්න ඕන වෙන්නේ නැහැ. අලුත් Payment ක්රමයක් ආවොත්, කරන්න තියෙන්නේ PaymentStrategy
Interface එක Implement කරන අලුත් Class එකක් හදන එක විතරයි. මේක තමයි Strategy Pattern එකේ ලොකුම වාසිය!
කවදාද Strategy Pattern එක පාවිච්චි කරන්නේ? කවදාද නොකර ඉන්නේ?
පාවිච්චි කළ යුතු අවස්ථා:
- එකම කාර්යය ඉටු කරන්න විවිධ ඇල්ගොරිතම රාශියක් තියෙනවා නම්.
- Runtime එකේදී ඇල්ගොරිතම මාරු කරන්න අවශ්ය නම්.
- Class එකක් ඇතුලේ විශාල
if-else
හෝswitch-case
blocks තියෙනවා නම්, ඒවා simplify කරන්න. - Business logic එක නිතර වෙනස් වෙනවා නම්, ඒ වෙනස්වීම් වලට පහසුවෙන් අනුගත වෙන්න.
පාවිච්චි නොකළ යුතු අවස්ථා (Over-engineering avoid කරන්න):
- ඇල්ගොරිතම එකක් හෝ දෙකක් වගේ සුළු ප්රමාණයක් තියෙනවා නම්. මේ රටාව Implement කරන එකෙන් කෝඩ් එකේ complexity එක වැඩි වෙන්න පුළුවන්.
- ඇල්ගොරිතම නිතර වෙනස් වෙන්නේ නැතිනම්.
අවසානයට…
Strategy Pattern එක කියන්නේ අපේ මෘදුකාංග පද්ධති වලට නම්යශීලී බව, නැවත භාවිතය සහ පහසු නඩත්තු කිරීම ලබා දෙන ඉතාම ප්රබල Design Pattern එකක්. Payment Gateways, Sorting Algorithms, Validation Rules වගේ දේවල් Implement කරනකොට මේ රටාව ගොඩක් ප්රයෝජනවත් වෙනවා.
අද අපි කතා කරපු දේවල් ඔයාලට වැදගත් වෙන්න ඇති කියලා හිතනවා. මේ Pattern එක ඔයාලගේ Project වලදී අත්හදා බලන්න, එතකොට මේකේ වටිනාකම හොඳින්ම තේරෙයි. මේ ගැන ඔයාලට මොනව හරි ප්රශ්න තියෙනවා නම්, පහලින් Comment කරන්න. අපි උත්සාහ කරමු ඒ හැම ප්රශ්නයකටම උත්තර දෙන්න. ඒ වගේම, තවත් මොනවා හරි Design Pattern එකක් ගැන දැනගන්න ඕන නම් ඒකත් කියන්න! හැමෝටම ජය!