LSP: ουσία, αρχή υποκατάστασης της Barbara Liskov στην ανάπτυξη

Συγγραφέας: IT Sectr Δημοσιεύτηκε: 2026-05-12 Χρόνος ανάγνωσης: 9 λεπ

LSP (Liskov Substitution Principle) — η τρίτη αρχή SOLID που καθορίζει τις προϋποθέσεις σωστής κληρονομικότητας στον αντικειμενοστραφή προγραμματισμό. Η αρχή διατυπώθηκε από την Barbara Liskov το 1987 και επισημοποιήθηκε ως: αν το S είναι υποτύπος του T, τότε τα αντικείμενα T μπορούν να αντικατασταθούν από αντικείμενα S χωρίς αλλαγή των ιδιοτήτων του προγράμματος. Όπως σημειώνεται στο βιβλίο του Robert Martin Clean Architecture (2017), η αρχή υποκατάστασης απαιτεί η υποκλάση να μην αποδυναμώνει τη σύμβαση της βασικής κλάσης.

Κύρια σημεία

  • LSP — αρχή υποκατάστασης Liskov, η τρίτη αρχή SOLID για σωστή κληρονομικότητα
  • Υποκλάση πρέπει να διατηρεί τη σύμβαση της βασικής κλάσης — προϋποθέσεις και επακόλουθες συνθήκες
  • Παραβίαση LSP εκδηλώνεται στο πρόβλημα τετραγώνου-ορθογωνίου και στις ριχθείσες εξαιρέσεις
  • Σύνθεση είναι συχνά προτιμότερη από την κληρονομικότητα για τήρηση του LSP
  • Σχεδίαση βάσει συμβολαίου (Design by Contract) — τυπικός τρόπος ελέγχου LSP

Τι είναι το LSP (Liskov Substitution Principle);

LSP (Liskov Substitution Principle) — η αρχή υποκατάστασης που διατυπώθηκε από την Barbara Liskov στο συνέδριο OOPSLA το 1987. Τυπικός ορισμός: έστω q(x) μια αποδείξιμη ιδιότητα αντικειμένων x τύπου T. Τότε το q(y) πρέπει να είναι αποδείξιμο για αντικείμενα y τύπου S, όπου το S είναι υποτύπος του T. Με απλά λόγια: τα αντικείμενα της υποκλάσης πρέπει να συμπεριφέρονται έτσι ώστε ο κώδικας που λειτουργεί με τη βασική κλάση να συνεχίσει να λειτουργεί σωστά και με την υποκλάση.

Στην πράξη, LSP σημαίνει ότι η υποκλάση δεν πρέπει να παραβιάζει τη σύμβαση της βασικής κλάσης. Η σύμβαση περιλαμβάνει προϋποθέσεις (τι απαιτείται για την κλήση μεθόδου), επακόλουθες συνθήκες (τι εγγυάται μετά την κλήση) και αμετάβλητες (συνθήκες που διατηρούνται κατά τη διάρκεια ζωής του αντικειμένου). Η υποκλάση μπορεί να ενισχύει τις προϋποθέσεις ή να αποδυναμώνει τις επακόλουθες συνθήκες — αυτή είναι παραβίαση LSP.

Κλασικό παράδειγμα παραβίασης LSP — τετράγωνο που κληρονομεί από ορθογώνιο. Η μέθοδος setWidth στο ορθογώνιο ορίζει το πλάτος, στο τετράγωνο — και το πλάτος και το ύψος. Ο πελάτης που περιμένει συμπεριφορά ορθογωνίου (η αλλαγή μιας πλευράς δεν επηρεάζει την άλλη) λαμβάνει απροσδόκητο αποτέλεσμα. Το τετράγωνο δεν είναι σωστός υποτύπος του ορθογωνίου.

Τυπικές συνθήκες LSP

Το LSP θέτει τρεις συνθήκες για σωστή κληρονομικότητα: οι προϋποθέσεις της υποκλάσης δεν μπορεί να είναι ισχυρότερες από τις προϋποθέσεις της βασικής κλάσης (η υποκλάση δεν απαιτεί περισσότερα), οι επακόλουθες συνθήκες της υποκλάσης δεν μπορεί να είναι ασθενέστερες από τις επακόλουθες συνθήκες της βασικής κλάσης (η υποκλάση δεν εγγυάται λιγότερα), οι αμετάβλητες της βασικής κλάσης πρέπει να διατηρούνται στην υποκλάση. Αυτές οι συνθήκες είναι γνωστές ως κανόνας σχεδίασης βάσει συμβολαίου του Bertrand Meyer.

Αν τουλάχιστον μία συνθήκη παραβιαστεί — ο κώδικας που χρησιμοποιεί πολυμορφισμό μπορεί να δυσλειτουργήσει. Ο μεταγλωττιστής δεν ελέγχει σημασιολογικές συμβάσεις, μόνο συντακτικές. Γι' αυτό το LSP είναι θέμα αρχιτεκτονικής πειθαρχίας, όχι στατικής τυποποίησης.

Πώς λειτουργεί η αρχή υποκατάστασης Liskov

Ο μηχανισμός LSP βασίζεται στη συμπεριφορική συμβατότητα τύπων. Αν η κλάση S κληρονομεί από την κλάση T, ο κώδικας πελάτη πρέπει να μπορεί να χρησιμοποιεί S παντού όπου αναμένεται T, χωρίς να αλλάζει τη συμπεριφορά του. Αυτό περιλαμβάνει όχι μόνο τις υπογραφές μεθόδων, αλλά και τη σημασιολογία τους.

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

Σε πραγματικά έργα, το LSP παραβιάζεται συχνότερα με την προσθήκη υπό συνθήκη λογικής στις μεθόδους της υποκλάσης: «αν συνθήκη — ρίξε εξαίρεση», «αν συνθήκη — επέστρεψε null». Κάθε τέτοια «έκπληξη» υπονομεύει τον πολυμορφισμό και αναγκάζει τον κώδικα πελάτη να ελέγχει τον τύπο του αντικειμένου πριν από την κλήση — που έρχεται σε αντίθεση με την ίδια την ιδέα του αντικειμενοστραφούς σχεδιασμού.

Σε έργα για κινητά, τυπική παραβίαση LSP συμβαίνει κατά τη δημιουργία βασικού ViewModel. Αν το BaseViewModel εγγυάται ότι η μέθοδος onCleared απελευθερώνει όλους τους πόρους, και η υποκλάση παρακάμπτει αυτή τη μέθοδο ως κενή — οποιοσδήποτε κώδικας που βασίζεται στην απελευθέρωση πόρων μέσω πολυμορφικής κλήσης onCleared θα λειτουργεί λανθασμένα. Το LSP απαιτεί η υποκλάση είτε να καλεί super.onCleared() είτε να εκτελεί η ίδια την ίδια εργασία. Σύνθεση μέσω LifecycleObserver — εναλλακτική που αποκλείει την παραβίαση LSP στη διαχείριση κύκλου ζωής.

Σημάδια παραβίασης LSP στον κώδικα

Οι κύριοι δείκτες παραβίασης LSP περιλαμβάνουν: έλεγχο τύπου αντικειμένου μέσω instanceof ή is πριν από κλήση μεθόδου, κενές υλοποιήσεις μεθόδων (stubs), ρίψη εξαίρεσης NotImplementedError ή UnsupportedOperationException, επιστροφή null αντί τιμής. Κάθε ένα από αυτά τα μοτίβα υποδηλώνει ότι η υποκλάση δεν είναι σωστός υποτύπος.

Ένα άλλο συχνό σημάδι — κληρονομικότητα με σκοπό την επαναχρησιμοποίηση κώδικα, όχι για τη μοντελοποίηση της σχέσης «είναι» (is-a). Η κλάση Bird έχει μέθοδο fly(). Η κλάση Penguin κληρονομεί από Bird και παρακάμπτει τη fly() ως κενή ή που ρίχνει εξαίρεση. Αυτή είναι παραβίαση LSP: ο πιγκουίνος δεν είναι σωστός υποτύπος του πουλιού.

Στην ανάπτυξη για κινητά, το LSP παραβιάζεται κατά τη δημιουργία βασικού ViewHolder, Fragment ή ViewController με μεθόδους stub. Αν η υποκλάση δεν χρησιμοποιεί τις μισές μεθόδους της βασικής κλάσης — η κληρονομικότητα επιλέχθηκε λανθασμένα. Σύνθεση ή διαχωρισμός διεπαφής λύνουν το πρόβλημα πιο σωστά.

Δοκιμή LSP

Απλή δοκιμή για έλεγχο LSP: γράψτε unit test για τη βασική κλάση που ελέγχει τη σύμβασή της (τιμές επιστροφής, εξαιρέσεις, παρενέργειες). Εκτελέστε αυτό το τεστ για κάθε υποκλάση. Αν το τεστ αποτύχει — το LSP έχει παραβιαστεί. Αυτή η προσέγγιση ονομάζεται «δοκιμή μέσω σύμβασης βασικής κλάσης».

Σε έργα Android, ένα τέτοιο τεστ είναι χρήσιμο για ViewModel και Repository. Αν το BaseViewModel εγγυάται κατάσταση Loading πριν από σφάλμα, και η υποκλάση ρίχνει σφάλμα χωρίς Loading — το τεστ θα καταγράψει παραβίαση LSP στο στάδιο CI.

Παραδείγματα LSP στην ανάπτυξη για κινητά

Ας εξετάσουμε το παράδειγμα Android με χειρισμό ClickListener. Η παραβίαση LSP συμβαίνει όταν η βασική υλοποίηση εγγυάται κάτι και η υποκλάση το παραβιάζει.

kotlin
// Βασική κλάση με εγγύηση: το onClick θα κληθεί
open class BaseClickListener {
    open fun onClick(view: View) {
        // βασική επεξεργασία
    }
}

// Παραβίαση LSP: η υποκλάση προσθέτει συνθήκη που ρίχνει εξαίρεση
class RestrictedClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (!isLoggedIn) {
            throw IllegalStateException("Not logged in")
        }
        super.onClick(view)
    }
}

// Σωστή λύση: η σύμβαση δεν παραβιάστηκε
class ConditionalClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (isLoggedIn) {
            super.onClick(view)
        }
    }
}

Το παράδειγμα iOS με το πρωτόκολλο DataSource δείχνει παραβίαση LSP μέσω επιστροφής nil αντί δεδομένων:

swift
// Πρωτόκολλο με σύμβαση: επιστρέφει δεδομένα ή σφάλμα
protocol DataProvider {
    func fetchData() async throws -> [String]
}

// Παραβίαση LSP: επιστρέφει nil χωρίς σφάλμα
class SilentFailProvider: DataProvider {
    func fetchData() async throws -> [String] {
        return [] // κενός πίνακας αντί σφάλματος
    }
}

// Σωστή τήρηση LSP
class NetworkProvider: DataProvider {
    func fetchData() async throws -> [String] {
        throw NetworkError.timeout
    }
}

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

LSP και κληρονομικότητα: πότε να επιλέξετε σύνθεση

Σύνθεση είναι προτιμότερη από κληρονομικότητα σε καταστάσεις όπου η σχέση «είναι» (is-a) είναι διφορούμενη ή υπό όρους. Κλασικό παράδειγμα: Ο Manager είναι Employee; Ναι. Αλλά είναι το Square σωστό Rectangle; Το LSP λέει «όχι». Αν αμφιβάλλετε για την ορθότητα της κληρονομικότητας — επιλέξτε σύνθεση.

Στην ανάπτυξη για κινητά, η σύνθεση χρησιμοποιείται συχνά μέσω έγχυσης εξαρτήσεων: αντί να κληρονομεί συμπεριφορά από βασική κλάση, η κλάση τη λαμβάνει μέσω κατασκευαστή. Το ViewModel δεν κληρονομεί από Repository, αλλά το αποδέχεται ως εξάρτηση. Αυτό αποκλείει την παραβίαση LSP εξ ορισμού — δεν υπάρχει κληρονομικότητα, δεν υπάρχει παραβίαση σύμβασης.

Σημάδια ότι η κληρονομικότητα πρέπει να αντικατασταθεί με σύνθεση: η υποκλάση δεν χρησιμοποιεί μέρος των μεθόδων της βασικής κλάσης, η υποκλάση παρακάμπτει μεθόδους με κενά stubs, ο κώδικας πελάτη ελέγχει τον τύπο αντικειμένου μέσω instanceof. Σε αυτές τις περιπτώσεις, η κληρονομικότητα επιλέχθηκε λανθασμένα και το LSP έχει παραβιαστεί.

Λύση μέσω διεπαφών

Διεπαφές λύνουν το πρόβλημα LSP χωρίς κληρονομικότητα: κάθε τύπος υλοποιεί ακριβώς τις μεθόδους που χρειάζεται. Αντί για κοινή βασική κλάση Bird με μέθοδο fly (όπου ο Penguin δεν πετά) — η διεπαφή Flyable, την οποία υλοποιούν μόνο τα πουλιά που πετούν. Ο Penguin υλοποιεί Bird χωρίς μέθοδο fly — το LSP δεν παραβιάζεται.

Στην αρχιτεκτονική Android, αυτή η προσέγγιση εφαρμόζεται μέσω διαχωρισμένων διεπαφών UseCase: αντί για ένα μεγάλο UseCase με μεθόδους getAll, getById, save, delete — ξεχωριστές διεπαφές GetItemsUseCase, SaveItemUseCase. Ο πελάτης εξαρτάται μόνο από την απαιτούμενη διεπαφή και κάθε κλάση που υλοποιεί αυτή τη διεπαφή είναι σωστή από την άποψη του LSP.

Συχνές ερωτήσεις

Σε τι διαφέρει το LSP από την απλή κληρονομικότητα;

Κληρονομικότητα — μηχανισμός γλώσσας, LSP — κανόνας σωστής χρήσης αυτού του μηχανισμού. Η κληρονομικότητα εγγυάται συμβατότητα υπογραφών (σύνταξη), το LSP απαιτεί συμβατότητα συμπεριφοράς (σημασιολογία). Κληρονομικότητα χωρίς LSP δίνει πολυμορφισμό που σπάει κατά τον χρόνο εκτέλεσης.

Παραβιάζει πάντα το null σε υποκλάση το LSP;

Αν η βασική κλάση εγγυάται μη-null επιστροφή — ναι. Αν η σύμβαση επιτρέπει null (προαιρετική τιμή) — όχι. Το LSP δεν απαγορεύει το null, απαγορεύει την αποδυνάμωση της σύμβασης. Μελετήστε την τεκμηρίωση της βασικής κλάσης και ελέγξτε αν η σύμβαση της υποκλάσης είναι συμβατή.

Πώς εφαρμόζεται το LSP σε πρωτόκολλα στο Swift;

Σε πρωτόκολλα το LSP εφαρμόζεται όπως και στις κλάσεις. Η υλοποίηση του πρωτοκόλλου πρέπει να τηρεί τη σημασιολογική σύμβαση: αν το πρωτόκολλο ορίζει μια μέθοδο ως non-throwing, η υλοποίηση δεν πρέπει να ρίχνει σφάλματα. Το Swift δεν το ελέγχει σε επίπεδο μεταγλώττισης — η ευθύνη είναι στον προγραμματιστή.

Μπορεί να παραβιαστεί το LSP με χρήση sealed class;

Sealed class στην Kotlin — ειδική περίπτωση, καθώς η ιεραρχία είναι κλειστή και γνωστή στον μεταγλωττιστή. Το LSP εφαρμόζεται σε μικρότερο βαθμό στο sealed class, επειδή όλοι οι υποτύποι αναφέρονται ρητά στην έκφραση when. Το σφάλμα της sealed-υποκλάσης θα είναι τοπικό, όχι κρυφό πολυμορφικό σφάλμα.

Πώς να ελέγξουμε την τήρηση LSP σε ένα έργο;

Γράψτε μια παραμετροποιημένη δοκιμή για τη βασική κλάση που εκτελείται για όλες τις υποκλάσεις της. Η δοκιμή ελέγχει βασικές συμβάσεις συμπεριφοράς: τιμές επιστροφής, εξαιρέσεις, καταστάσεις. Αν η δοκιμή αποτύχει σε μία από τις υποκλάσεις — το LSP έχει παραβιαστεί. Στο CI, μια τέτοια δοκιμή αποτρέπει την παλινδρόμηση πολυμορφικού κώδικα.

Περίληψη

  • LSP (Liskov Substitution Principle) — αρχή υποκατάστασης, τρίτη στο SOLID, για σημασιολογική συμβατότητα κληρονομικότητας
  • Υποκλάση πρέπει να διατηρεί τη σύμβαση της βασικής κλάσης: προϋποθέσεις, επακόλουθες συνθήκες και αμετάβλητες
  • Έλεγχος instanceof και κενές παρακάμψεις μεθόδων — κύρια σημάδια παραβίασης LSP
  • Σύνθεση και διεπαφές λύνουν το πρόβλημα LSP όπου η κληρονομικότητα είναι λανθασμένη
  • Πρόβλημα τετραγώνου-ορθογωνίου — κλασικό παράδειγμα ασυμβατότητας υποτύπων
  • Δοκιμή σύμβασης για βασική κλάση, που εκτελείται για όλες τις υποκλάσεις, εντοπίζει παραβίαση LSP στο CI
  • Sealed class στην Kotlin μειώνει τους κινδύνους LSP χάρη στην κλειστή ιεραρχία γνωστή στον μεταγλωττιστή

Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση

Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.

Συζήτηση έργου

Διαβάστε επίσης