Επιστροφή στο blog

SOLID: Οι πρώτες 5 αρχές του αντικειμενοστρεφούς σχεδιασμού;

SOLID: Οι πρώτες 5 αρχές του αντικειμενοστρεφούς σχεδιασμού;

Εισαγωγή

Το SOLID είναι ένα μνημονικό ακρωνύμιο για πέντε αρχές σχεδίασης αντικειμενοστρεφούς προγραμματισμού που εισήχθησαν από τον Robert C. Martin, ο οποίος είναι ευρύτερα γνωστός ως Uncle Bob. Αυτές οι αρχές έχουν σκοπό να βοηθήσουν τους σχεδιαστές λογισμικού, τους αρχιτέκτονες, τους μηχανικούς και τους προγραμματιστές να δημιουργήσουν πιο ευέλικτο, συντηρήσιμο και επεκτάσιμο λογισμικό. Ακολουθώντας αυτές τις αρχές, μπορείτε να σχεδιάσετε κλάσεις που είναι ευκολότερο να ελεγχθούν, να αναδιαρθρωθούν (refactor), να επαναχρησιμοποιηθούν και να επεκταθούν.

Το ακρωνύμιο SOLID σημαίνει:

S – Αρχή της Μοναδικής Υπευθυνότητας

O – Αρχή του Ανοικτού/Κλειστού

L – Αρχή Υποκατάστασης της Liskov

I – Αρχή του Διαχωρισμού των Διασυνδέσεων

D – Αρχή της Αντιστροφής Εξαρτήσεων

Σε αυτό το άρθρο, θα εξηγήσουμε κάθε αρχή ξεχωριστά για να κατανοήσουμε πώς μπορεί να σας βοηθήσει να γράψετε καλύτερο κώδικα. Επιπλέον, θα προσθέσουμε αποσπάσματα κώδικα για κάθε αρχή για να σας δείξουμε πώς μπορείτε να τις εφαρμόσετε στο προγραμματιστικό σας ταξίδι, καθώς και τι πρέπει να αποφεύγετε στην αρχιτεκτονική καθαρού κώδικα (clean code). Για να παρουσιάσουμε αυτές τις έννοιες, θα χρησιμοποιήσουμε τη Kotlin programming γλώσσα που αναπτύχθηκε από την JetBrains και συνεισφέροντες ανοιχτού κώδικα.

Αρχή της Μοναδικής Υπευθυνότητας

Η Αρχή της Μοναδικής Υπευθυνότητας (SRP) είναι μια αρχή σχεδίασης λογισμικού που ορίζει ότι κάθε κλάση ή ενότητα σε ένα πρόγραμμα θα πρέπει να έχει μια μοναδική, καλά ορισμένη υπευθυνότητα. Αυτό σημαίνει ότι μια κλάση θα πρέπει να έχει μόνο έναν λόγο για να αλλάξει, και θα πρέπει να είναι υπεύθυνη για ένα μόνο μέρος της λειτουργικότητας του προγράμματος.

Δείτε το ακόλουθο απόσπασμα κώδικα ως παράδειγμα του πώς μπορεί να εφαρμοστεί αυτή η αρχή στην Kotlin:

Το data  keyword στην Kotlin υποδεικνύει ότι αυτή η κλάση είναι μια κλάση δεδομένων (data class), πράγμα που σημαίνει ότι προορίζεται να κρατά δεδομένα και δεν έχει κάποια πολύπλοκη συμπεριφορά. Χρησιμοποιώντας αυτήν την κλάση δεδομένων, μπορούμε να δημιουργήσουμε μια UserService  κλάση για τη διαχείριση χρηστών, όπως φαίνεται παρακάτω:

Σε αυτό το παράδειγμα, η UserService κλάση έχει μία μόνο υπευθυνότητα: τη διαχείριση χρηστών σε μια βάση δεδομένων. Κάθε χρήστης αναπαρίσταται από τον κώδικα data class User  που μοιραστήκαμε νωρίτερα. Όλες οι μέθοδοι στην UserService κλάση σχετίζονται με αυτήν την υπευθυνότητα και επομένως παρουσιάζουν συνοχή. Αυτό καθιστά την κλάση ευκολότερη στην κατανόηση και τη συντήρηση, καθώς είναι σαφές ότι όλες οι μέθοδοι στην κλάση σχετίζονται με μια μοναδική, καλά ορισμένη εργασία.

Μια μέθοδος που παραβιάζει την Αρχή της Μοναδικής Υπευθυνότητας (SRP) στην UserService κλάση θα ήταν μια μέθοδος που δεν σχετίζεται με την κύρια υπευθυνότητα της κλάσης, η οποία είναι η διαχείριση χρηστών σε μια βάση δεδομένων. Για παράδειγμα, εξετάστε την ακόλουθη παραλλαγή της UserService κλάσης:

Σε αυτό το παράδειγμα, η sendEmail  μέθοδος δεν σχετίζεται με την κύρια ευθύνη της UserService κλάσης, η οποία είναι η διαχείριση χρηστών σε μια βάση δεδομένων. Αυτή η μέθοδος είναι υπεύθυνη για την αποστολή email, κάτι που αποτελεί ξεχωριστό μέλημα από τη διαχείριση χρηστών. Ως αποτέλεσμα, αυτή η μέθοδος παραβιάζει την αρχή SRP, καθώς εισάγει έναν δεύτερο λόγο για την αλλαγή της κλάσης UserService.

Για να συμμορφωθούμε με την Αρχή της Μίας Ευθύνης, θα ήταν καλύτερο να διαχωρίσουμε τη λειτουργικότητα αποστολής email σε μια ξεχωριστή κλάση, όπως μια κλάση EmailService . Αυτό θα επέτρεπε στην κλάση UserService να επικεντρωθεί στην κύρια ευθύνη της για τη διαχείριση χρηστών, και στην κλάση EmailService να επικεντρωθεί στην ευθύνη της για την αποστολή email.

Θα πρέπει να σημειώσετε ότι η Αρχή της Μίας Ευθύνης δεν αφορά τον αριθμό των μεθόδων που έχει μια κλάση, αλλά μάλλον τη συνοχή των μεθόδων και τον σαφή διαχωρισμό των ευθυνών μέσα σε μια κλάση.

Αρχή Ανοιχτού-Κλειστού (Open-Closed Principle)

Η Αρχή Ανοιχτού-Κλειστού (Open-Closed Principle - OCP) είναι μια αρχή σχεδιασμού λογισμικού που ορίζει ότι οι οντότητες λογισμικού (όπως κλάσεις, ενότητες ή συναρτήσεις) θα πρέπει να είναι ανοιχτές για επέκταση, αλλά κλειστές για τροποποίηση. Αυτό σημαίνει ότι θα πρέπει να είναι δυνατή η προσθήκη νέας λειτουργικότητας σε μια κλάση ή ενότητα χωρίς την αλλαγή του υπάρχοντος κώδικά της.

Εξετάστε την κλάση UserService από το προηγούμενο παράδειγμα. Ας υποθέσουμε ότι θέλουμε να προσθέσουμε μια νέα δυνατότητα στην κλάση UserService που να μας επιτρέπει να αναζητούμε χρήστες με βάση τη διεύθυνση email τους. Ένας τρόπος για να γίνει αυτό θα ήταν να προσθέσουμε μια νέα μέθοδο στην κλάση UserService όπως επισημαίνεται στον παρακάτω κώδικα:

Αυτή η προσέγγιση λειτουργεί, αλλά παραβιάζει την Αρχή Ανοιχτού-Κλειστού, καθώς έπρεπε να τροποποιήσουμε την υπάρχουσα κλάση UserService προκειμένου να προσθέσουμε τη νέα δυνατότητα. Μια καλύτερη προσέγγιση θα ήταν να χρησιμοποιήσουμε κληρονομικότητα ή σύνθεση για να επεκτείνουμε τη λειτουργικότητα της κλάσης UserService χωρίς να τροποποιήσουμε τον κώδικά της.

Για να το επιτύχουμε αυτό, θα μπορούσαμε να δημιουργήσουμε μια νέα κλάση με όνομα UserSearchService  που επεκτείνει την κλάση UserService και προσθέτει τη λειτουργικότητα αναζήτησης μέσω email:

Σε αυτό το παράδειγμα, η κλάση UserSearchService είναι ανοιχτή για επέκταση, καθώς παρέχει πρόσθετη λειτουργικότητα πέρα από αυτήν που προσφέρεται από την UserService κλάση. Ταυτόχρονα, η UserService κλάση παραμένει κλειστή για τροποποίηση, καθώς δεν χρειάστηκε να τροποποιήσουμε τον κώδικά της για να προσθέσουμε τη δυνατότητα αναζήτησης μέσω email.

Αρχή της Υποκατάστασης Liskov

Η Αρχή της Υποκατάστασης Liskov (LSP) είναι μια αρχή σχεδιασμού λογισμικού που ορίζει ότι τα αντικείμενα μιας υπερκλάσης θα πρέπει να μπορούν να αντικατασταθούν από αντικείμενα μιας υποκλάσης χωρίς να επηρεάζεται η ορθότητα του προγράμματος. Αυτό σημαίνει ότι μια υποκλάση θα πρέπει να είναι ένα έγκυρο υποκατάστατο της υπερκλάσης της και θα πρέπει να συμπεριφέρεται με τον ίδιο τρόπο όπως η υπερκλάση όταν χρησιμοποιείται στο ίδιο πλαίσιο.

Θα συνεχίσουμε την επίδειξη χρησιμοποιώντας τις User και UserService κλάσεις από τα προηγούμενα παραδείγματα. Για να επιτρέψουμε την επέκταση της κλάσης User, χρησιμοποιούμε τη λέξη-κλειδί open  στην Kotlin:

Ορίστε η αρχική UserService κλάση που χρησιμοποιεί την παραπάνω User data class:

Ας υποθέσουμε ότι θέλουμε να δημιουργήσουμε μια υποκλάση της User με όνομα AdminUser που αναπαριστά χρήστες με δικαιώματα διαχειριστή. Θα μπορούσαμε να το κάνουμε ως εξής:

Σε αυτό το παράδειγμα, η κλάση AdminUser είναι ένα έγκυρο υποκατάστατο για την κλάση User, καθώς συμπεριφέρεται με τον ίδιο τρόπο όπως η κλάση User και μπορεί να χρησιμοποιηθεί οπουδήποτε αναμένεται ένα αντικείμενο User. Για παράδειγμα, μπορούμε να χρησιμοποιήσουμε την κλάση AdminUser με την κλάση UserService  ως εξής:

Αυτός ο κώδικας είναι σωστός, καθώς η κλάση AdminUser είναι ένα έγκυρο υποκατάστατο για την κλάση User και μπορεί να χρησιμοποιηθεί με τον ίδιο τρόπο όπως ένα αντικείμενο User .

Είναι σημαντικό να σημειωθεί ότι η Αρχή της Υποκατάστασης Liskov αφορά κάτι περισσότερο από την απλή κληρονομικότητα. Αφορά τη διασφάλιση ότι τα αντικείμενα μιας υποκλάσης συμπεριφέρονται με τον ίδιο τρόπο όπως τα αντικείμενα της υπερκλάσης, ανεξάρτητα από τον τρόπο υλοποίησης της υποκλάσης. Για παράδειγμα, εάν η κλάση AdminUser συμπεριφερόταν διαφορετικά από την κλάση User με κάποιον τρόπο, αυτό θα παραβίαζε την Αρχή της Υποκατάστασης Liskov, καθώς δεν θα αποτελούσε έγκυρο υποκατάστατο για την κλάση User .

Αρχή του Διαχωρισμού Διασυνδέσεων

Η Αρχή του Διαχωρισμού Διασυνδέσεων (ISP) είναι μια αρχή σχεδιασμού λογισμικού που ορίζει ότι οι πελάτες δεν θα πρέπει να αναγκάζονται να εξαρτώνται από διεπαφέςπου δεν χρησιμοποιούν. Αυτό σημαίνει ότι είναι γενικά καλή ιδέα να δημιουργούνται μικρές, εστιασμένες διεπαφές που κάνουν ένα πράγμα καλά, αντί να δημιουργούνται μεγάλες διεπαφές γενικής χρήσης διεπαφές που προσπαθούν να κάνουν πολλά πράγματα.

Ορίστε ένα παράδειγμα για το πώς μπορεί να εφαρμοστεί αυτή η αρχή, ξαναγράφοντας τις κλάσεις User  και UserService  από τα προηγούμενα παραδείγματα:

Σε αυτό το παράδειγμα, η UserService interface defines four methods that are related to managing users in a database. The DatabaseUserService  κλάση υλοποιεί αυτή τη διεπαφή και παρέχει συγκεκριμένες υλοποιήσεις για αυτές τις μεθόδους.

Suppose we want to add a new feature to the UserService interface that allows us to search for users by email address. One way to do this would be to add a new method to the UserService interface:

Ο κώδικάς σας δεν θα εκτελεστεί, εκτός εάν υλοποιήσετε επίσης αυτή τη μέθοδο σε όλες τις κλάσεις που υλοποιούν τη UserService interface:

Αν και αυτή η προσέγγιση λειτουργεί, παραβιάζει την Αρχή Διαχωρισμού Διεπαφών (Interface Segregation Principle), καθώς αναγκάζει την DatabaseUserService  κλάση να υλοποιήσει μια μέθοδο ( searchUsersByEmail ) την οποία μπορεί να μην χρειάζεται ή να μην χρησιμοποιεί.

Μια καλύτερη προσέγγιση θα ήταν η δημιουργία μιας ξεχωριστής interface  για τη λειτουργικότητα αναζήτησης email:

Τώρα έχουμε ξεχωριστές, μικρές και εστιασμένες διεπαφές, δηλ. UserService  και UserSearchServiceπου έχουν μία μόνο ευθύνη. Μια κλάση που απαιτεί όλες τις λειτουργικότητες αυτών των διεπαφών μπορεί να τις υλοποιήσει όπως φαίνεται στο παρακάτω απόσπασμα κώδικα:

Αυτό συμμορφώνεται με την Αρχή Διαχωρισμού Διασυνδέσεων, καθώς διασφαλίζει ότι οι πελάτες (όπως η DatabaseUserService κλάση) δεν αναγκάζονται να εξαρτώνται από διεπαφές που δεν χρησιμοποιούν.

Για να κατανοήσετε καλύτερα αυτήν την έννοια, ας υποθέσουμε ότι έχουμε μια άλλη κλάση που ονομάζεται MemoryUserService  που υλοποιεί τη UserService  διεπαφή αλλά δεν χρειάζεται τη λειτουργικότητα αναζήτησης email, μπορούμε να γράψουμε τον κώδικα ως εξής:

In this example, the MemoryUserService class only needs to implement the methods defined in the UserService interface, and does not need to worry about the email search functionality. This allows the MemoryUserService class to focus on its primary responsibility of managing users in memory, rather than being forced to implement unrelated functionality.

Αρχή Αντιστροφής Εξαρτήσεων

Η Αρχή Αντιστροφής Εξαρτήσεων (Dependency Inversion Principle - DIP) είναι μια αρχή σχεδιασμού λογισμικού που ορίζει ότι τα δομοστοιχεία υψηλού επιπέδου δεν πρέπει να εξαρτώνται από δομοστοιχεία χαμηλού επιπέδου, αλλά και τα δύο θα πρέπει να εξαρτώνται από αφαιρέσεις. Αυτό σημαίνει ότι είναι γενικά καλή ιδέα να σχεδιάζετε το λογισμικό σας με τέτοιο τρόπο ώστε τα στοιχεία υψηλού επιπέδου να μην είναι συνδεδεμένα με συγκεκριμένες υλοποιήσεις στοιχείων χαμηλού επιπέδου, αλλά μάλλον να εξαρτώνται από αφαιρέσεις (όπως διεπαφές ή αφηρημένες κλάσεις) που μπορούν να υλοποιηθούν με διαφορετικούς τρόπους.

Ας δούμε ένα παράδειγμα για το πώς μπορεί να εφαρμοστεί αυτή η αρχή στις κλάσεις User και UserService που χρησιμοποιήθηκαν στα προηγούμενα αποσπάσματα κώδικα:

In this example, the UserService class depends on an abstraction called the UserRepository interface, rather than depending on a specific implementation of a user repository. This allows us to implement the UserRepository interface in different ways, such as with a database or in-memory storage, without affecting the UserService class.

For example, here’s an implementation of the UserRepository interface that uses a database:

Here is another implementation of the UserRepository interface that uses in-memory storage:

This makes the system more flexible and maintainable, as it allows us to change the repository implementation without affecting the rest of the system. It also makes it easier to test the UserService class, as we can mock the UserRepository dependency in our tests.

Conclusion

In this article, we talked about the five principles of SOLID  κώδικα και κοινόχρηστων αποσπασμάτων κώδικα που ικανοποιούν κάθε αρχή. Η τήρηση αυτών των αρχών μπορεί να σας βοηθήσει να σχεδιάσετε συστήματα λογισμικού που είναι πιο ευέλικτα, συντηρήσιμα και επεκτάσιμα. Ωστόσο, είναι σημαντικό να έχετε υπ’ όψιν σας ότι αυτές οι αρχές είναι κατευθυντήριες γραμμές, παρά αυστηροί κανόνες, και εναπόκειται στον προγραμματιστή να αποφασίσει πότε και πώς θα τις εφαρμόσει στο πλαίσιο του συγκεκριμένου έργου του. Συνεχίστε τη μάθησή σας ελέγχοντας το ιστολόγιό μας για περισσότερα σε βάθος και ενημερωμένα άρθρα και οδηγούς σχετικά με το υπολογιστικό νέφος και το DevOps, σχεδιασμό και ανάπτυξη λογισμικού, τεχνολογικές τάσεις που πρέπει να προσέξετε, και πολλά άλλα.

Καλό προγραμματισμό!

author

Preslav Dobrev

Συγγραφέας · CloudSigma

Ο Preslav Dobrev είναι Δημιουργικός Σχεδιαστής στην CloudSigma, με εστίαση στη συνεπή επιχειρηματική ταυτότητα μέσω παραδοσιακών και καινοτόμων καναλιών μάρκετινγκ. Διαθέτει την ικανότητα να συνδυάζει το καλλιτεχνικό όραμα με το στρατηγικό μάρκετινγκ για τη δημιουργία εντυπωσιακών αφηγήσεων επωνυμίας.

Σχόλια

Δεν υπάρχουν σχόλια ακόμα. Γράψτε το πρώτο.