Πώς αποφασίζουμε σωστά όταν ένα σύστημα πρέπει να συμπεριφέρεται διαφορετικά ανά εταιρεία
Εισαγωγή
Σε πολλά επιχειρησιακά συστήματα, ιδιαίτερα σε εφαρμογές που χρησιμοποιούνται από περισσότερους οργανισμούς, εμφανίζεται συχνά μια φαινομενικά απλή αλλά βαθιά αρχιτεκτονική ερώτηση: πώς πρέπει να προσαρμόζεται η συμπεριφορά του συστήματος όταν διαφορετικές εταιρείες έχουν διαφορετικούς κανόνες λειτουργίας;
Στην αρχή το πρόβλημα φαίνεται μικρό. Μια εταιρεία θέλει να επιτρέπεται μια συγκεκριμένη ενέργεια, μια άλλη όχι. Μια εταιρεία θέλει να εμφανίζεται ένα πεδίο, μια άλλη να παραμένει κρυφό. Μια εταιρεία θέλει διαφορετικό τρόπο υπολογισμού σε μια διαδικασία. Πολύ σύντομα όμως ο προγραμματιστής βρίσκεται μπροστά σε μια σημαντική απόφαση σχεδιασμού: πρέπει να υλοποιήσει αυτές τις διαφορές με settings, δηλαδή με παραμετροποίηση που οδηγεί σε διαφορετικές αποφάσεις στον ίδιο κώδικα, ή πρέπει να υιοθετήσει το Strategy Pattern, δηλαδή να δημιουργήσει διαφορετικές υλοποιήσεις συμπεριφοράς;
Η επιλογή δεν είναι μόνο τεχνική. Είναι αρχιτεκτονική. Και όπως συμβαίνει συχνά στην αρχιτεκτονική λογισμικού, η σωστή απόφαση δεν βρίσκεται στο εργαλείο αλλά στην κατανόηση του προβλήματος.
Το πρόβλημα
Ας υποθέσουμε ότι έχουμε ένα σύστημα που χρησιμοποιείται από τρεις διαφορετικές εταιρείες. Κάθε εταιρεία έχει τη δική της βάση δεδομένων, αλλά ο κώδικας της εφαρμογής είναι κοινός. Η βασική λειτουργικότητα του συστήματος είναι ίδια για όλους, αλλά υπάρχουν μικρές διαφοροποιήσεις στους κανόνες λειτουργίας.
Για παράδειγμα:
- Μια εταιρεία επιτρέπει στους χρήστες να προσθέτουν επιπλέον items σε μια παραγγελία, ενώ μια άλλη όχι.
- Μια εταιρεία απαιτεί επιπλέον έλεγχο πριν από την ολοκλήρωση μιας διαδικασίας.
- Μια εταιρεία εμφανίζει συγκεκριμένα πεδία στο UI ενώ μια άλλη τα κρύβει.
- Μια εταιρεία υπολογίζει δικαιώματα με διαφορετικό τρόπο.
Ο προγραμματιστής που υλοποιεί αυτές τις διαφορές αρχίζει συνήθως με κάτι απλό:
if (settings.AllowExtraItems)
{
order.AddItem(item);
}
else
{
throw new BusinessException("Extra items are not allowed.");
}
Στην αρχή όλα λειτουργούν σωστά. Όμως όσο μεγαλώνει το σύστημα, οι διαφορές πολλαπλασιάζονται. Εμφανίζονται νέα settings, νέα branches, νέοι έλεγχοι. Σε κάποιο σημείο η ερώτηση γίνεται αναπόφευκτη:
Είναι σωστό να συνεχίσουμε με settings ή πρέπει να αλλάξουμε αρχιτεκτονική και να χρησιμοποιήσουμε διαφορετικές στρατηγικές συμπεριφοράς;
Για να απαντήσουμε σωστά, πρέπει πρώτα να κατανοήσουμε σε βάθος τι είναι η κάθε προσέγγιση.
Η προσέγγιση των Settings
Τα settings αποτελούν τον πιο άμεσο και συνηθισμένο τρόπο προσαρμογής της συμπεριφοράς μιας εφαρμογής. Με τα settings μπορούμε να παραμετροποιήσουμε την εφαρμογή χωρίς να αλλάξουμε τον ίδιο τον κώδικα. Η ίδια εκτελέσιμη έκδοση του προγράμματος μπορεί να λειτουργεί διαφορετικά απλώς αλλάζοντας τις τιμές των ρυθμίσεων.
Η βασική ιδέα είναι απλή: ο κώδικας παραμένει κοινός για όλους, αλλά οι αποφάσεις μέσα στον κώδικα επηρεάζονται από τις τιμές των ρυθμίσεων.
Ένα χαρακτηριστικό παράδειγμα είναι η ενεργοποίηση ή απενεργοποίηση μιας λειτουργίας.
if (!settings.AllowOrderEditing && order.Status != OrderStatus.Draft)
{
throw new BusinessException("Order editing is not allowed.");
}
Σε αυτή την περίπτωση η λογική της εφαρμογής δεν αλλάζει ουσιαστικά. Η διαδικασία παραμένει η ίδια, αλλά ένα συγκεκριμένο βήμα επιτρέπεται ή όχι.
Παρόμοια παραδείγματα συναντώνται πολύ συχνά:
if (settings.ShowEmployeeCode)
{
viewModel.EmployeeCode = employee.Code;
}
ή
if (settings.RequireManagerApproval)
{
await approvalService.RequestApproval(order);
}
Τα settings είναι εξαιρετικά χρήσιμα γιατί επιτρέπουν στο σύστημα να προσαρμόζεται χωρίς νέο build, χωρίς αλλαγή κώδικα και χωρίς δημιουργία διαφορετικών εκδόσεων της εφαρμογής. Για συστήματα που εξυπηρετούν πολλούς οργανισμούς, αυτό αποτελεί τεράστιο πλεονέκτημα.
Ωστόσο τα settings έχουν και ένα όριο. Αν χρησιμοποιηθούν για να εκφράσουν πολύπλοκες διαφοροποιήσεις συμπεριφοράς, ο κώδικας αρχίζει να γεμίζει με if που επηρεάζουν πολλά σημεία του συστήματος. Σε αυτό το σημείο η παραμετροποίηση αρχίζει να μετατρέπεται σε χαοτική διακλάδωση λογικής.
Η προσέγγιση του Strategy Pattern
Το Strategy Pattern αντιμετωπίζει το ίδιο πρόβλημα με διαφορετικό τρόπο. Αντί να αλλάζει η συμπεριφορά μέσω if και ρυθμίσεων, δημιουργούνται διαφορετικές υλοποιήσεις μιας κοινής διεπαφής. Κάθε υλοποίηση αντιπροσωπεύει έναν διαφορετικό τρόπο εκτέλεσης της ίδιας λειτουργίας.
Ας δούμε ένα παράδειγμα.
Ορίζουμε μια διεπαφή:
public interface IItemPolicy
{
bool CanAddItem(Order order, Item item);
}
Στη συνέχεια δημιουργούμε διαφορετικές υλοποιήσεις:
public class DefaultItemPolicy : IItemPolicy
{
public bool CanAddItem(Order order, Item item)
{
return true;
}
}
και
public class RestrictedItemPolicy : IItemPolicy
{
public bool CanAddItem(Order order, Item item)
{
return order.Items.Count < 5;
}
}
Η εφαρμογή δεν χρειάζεται πλέον να γνωρίζει τα settings. Απλώς χρησιμοποιεί την κατάλληλη στρατηγική.
if (!itemPolicy.CanAddItem(order, item))
{
throw new BusinessException("Item cannot be added.");
}
Η επιλογή της στρατηγικής μπορεί να γίνει μέσω configuration:
if(settings.ItemPolicyType == "Restricted")
{
services.AddScoped<IItemPolicy, RestrictedItemPolicy>();
}
else
{
services.AddScoped<IItemPolicy, DefaultItemPolicy>();
}
Το αποτέλεσμα είναι ότι ο βασικός κώδικας παραμένει καθαρός και κάθε διαφορετική συμπεριφορά βρίσκεται σε ξεχωριστή κλάση.
Τα κριτήρια επιλογής
Η επιλογή μεταξύ settings και strategy δεν πρέπει να γίνεται διαισθητικά αλλά βάσει συγκεκριμένων κριτηρίων.
Το πρώτο κριτήριο αφορά τη φύση της διαφοράς. Αν η διαφορά ανάμεσα στις εταιρείες είναι παραμετρική, δηλαδή αλλάζει απλώς μια τιμή ή μια επιλογή, τότε τα settings είναι η σωστή λύση. Αν όμως η διαφορά αφορά διαφορετικό τρόπο λειτουργίας, διαφορετικό αλγόριθμο ή διαφορετική ροή εργασίας, τότε το Strategy Pattern είναι καταλληλότερο.
Το δεύτερο κριτήριο αφορά την έκταση της αλλαγής στον κώδικα. Αν ένα setting επηρεάζει μόνο ένα σημείο απόφασης, η χρήση του είναι απολύτως φυσιολογική. Αν όμως η ίδια επιλογή εμφανίζεται σε πολλά σημεία του συστήματος, τότε η λογική αρχίζει να διασκορπίζεται και η στρατηγική υλοποίηση γίνεται πιο καθαρή.
Το τρίτο κριτήριο αφορά τη δοκιμή του συστήματος. Αν η σωστή συμπεριφορά μπορεί να ελεγχθεί εύκολα με ένα ή δύο settings, τότε η παραμετροποίηση είναι επαρκής. Αν όμως χρειάζονται πολλοί συνδυασμοί flags για να προκύψει η σωστή συμπεριφορά, τότε η πολυπλοκότητα έχει ήδη ξεφύγει και η στρατηγική προσέγγιση είναι προτιμότερη.
Τέλος, ένα ακόμη κριτήριο είναι η αναγνωσιμότητα του κώδικα. Αν η επιχειρησιακή λογική εκφράζεται πιο καθαρά ως διαφορετικές υλοποιήσεις, τότε η χρήση στρατηγικών οδηγεί σε πιο κατανοητό σύστημα.
Η σωστή ισορροπία
Στην πράξη, τα περισσότερα μεγάλα συστήματα χρησιμοποιούν έναν συνδυασμό των δύο προσεγγίσεων. Τα settings χρησιμοποιούνται για απλές επιλογές και παραμέτρους, ενώ οι στρατηγικές χρησιμοποιούνται για να εκφράσουν διαφορετικές μορφές συμπεριφοράς.
Με αυτόν τον τρόπο διατηρείται ένα κοινό build της εφαρμογής, αλλά αποφεύγεται η υπερβολική πολυπλοκότητα που προκαλείται από δεκάδες flags και if διασκορπισμένα στον κώδικα.
Να θυμάσαι
Να θυμάσαι ότι τα settings είναι ιδανικά όταν η διαφορά είναι παραμετρική. Όταν αλλάζει μια τιμή, ένα όριο, μια απλή επιλογή, τα settings προσφέρουν ευελιξία χωρίς να αυξάνουν την πολυπλοκότητα του κώδικα.
Να θυμάσαι ότι το Strategy Pattern είναι κατάλληλο όταν η διαφορά είναι συμπεριφορική. Όταν αλλάζει ο τρόπος με τον οποίο εκτελείται μια διαδικασία, η απομόνωση της λογικής σε διαφορετικές στρατηγικές διατηρεί τον κώδικα καθαρό και κατανοητό.
Να θυμάσαι επίσης ότι ο στόχος δεν είναι να επιλέξεις ένα pattern και να το εφαρμόσεις παντού. Ο στόχος είναι να κατανοήσεις το πρόβλημα και να επιλέξεις το εργαλείο που εκφράζει καλύτερα τη φύση της διαφοράς.
Και τέλος, να θυμάσαι ότι η καλή αρχιτεκτονική δεν είναι θέμα τεχνικών τεχνασμάτων αλλά καθαρής σκέψης. Όταν η φύση του προβλήματος είναι ξεκάθαρη, η σωστή λύση γίνεται σχεδόν προφανής.
Η προσέγγιση αυτή είναι γνωστή στη σχεδιαστική βιβλιογραφία ως Strategy Pattern, ένα από τα κλασικά behavioral design patterns που επιτρέπουν την εναλλαγή αλγορίθμων κατά τον χρόνο εκτέλεσης. Περισσότερες λεπτομέρειες για το pattern μπορούν να βρεθούν στη σχετική τεκμηρίωση του Refactoring Guru (https://refactoring.guru/design-patterns/strategy).
Strategy & Factory Pattern στην C# Μια ορθολογιστική και SOLID προσέγγιση
Top comments (0)