SOLID — πέντε αρχές αντικειμενοστραφούς προγραμματισμού, οι οποίες διατυπώθηκαν από τον Robert C. Martin (Uncle Bob) στις αρχές της δεκαετίας του 2000. Σύμφωνα με το DigitalOcean, 2024, το SOLID αποκρυπτογραφείται ως Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation και Dependency Inversion. Αυτές οι αρχές αποτελούν τη βάση της Clean Architecture και εφαρμόζονται στην ανάπτυξη Android (MVP, MVVM, Clean Architecture) και iOS (VIPER, TCA).
Κύρια σημεία
SOLID — ένα μνημονικό ακρωνύμιο που υποδηλώνει πέντε αρχές αντικειμενοστραφούς σχεδιασμού. Ο όρος εισήχθη από τον Robert C. Martin στο άρθρο “Design Principles and Design Patterns” (2000) και αργότερα διαδόθηκε στο βιβλίο “Agile Software Development: Principles, Patterns, and Practices” (2002). Το SOLID δεν είναι framework ή βιβλιοθήκη — είναι μια συλλογή πρακτικών που κάνουν τον κώδικα λιγότερο συνδεδεμένο, πιο δοκιμάσιμο και ευκολότερο στις τροποποιήσεις.
Σύμφωνα με το Clean Coder Blog, 2014, κάθε αρχή SOLID λύνει ένα συγκεκριμένο πρόβλημα σχεδιασμού: το SRP καταπολεμά τις God-classes, το OCP — τις αλυσιδωτές αλλαγές, το LSP — την λανθασμένη κληρονομικότητα, το ISP — τα παχιά interfaces, το DIP — τη σφιχτή σύζευξη. Μαζί αποτελούν το θεμέλιο της Clean Architecture, η οποία χρησιμοποιείται σε Android projects με MVP, MVVM και MVI.
Single Responsibility Principle (SRP) — η αρχή της μίας ευθύνης. Διατύπωση: “Μια κλάση πρέπει να έχει μόνο έναν λόγο για να αλλάξει.” Αυτό σημαίνει ότι κάθε module ή κλάση είναι υπεύθυνη για ακριβώς μία λειτουργικότητα ή μία οντότητα τομέα. Αν μια κλάση διαχειρίζεται τόσο τους χρήστες όσο και την αποστολή email — έχει δύο λόγους για αλλαγή, πράγμα που παραβιάζει το SRP.
Σύμφωνα με Robert C. Martin, 2002, το SRP είναι η πιο σημαντική και ταυτόχρονα η πιο συχνά παραβιαζόμενη αρχή. Στην κινητή ανάπτυξη, το SRP παραβιάζεται συχνά σε Activity/Fragment, συνδυάζοντας UI λογική, πλοήγηση, εργασία δικτύου και επιχειρηματική λογική. Λύση — διαχωρισμός κάθε επιπέδου σε ξεχωριστή κλάση: ViewModel για UI λογική, Repository για δεδομένα, NavController για πλοήγηση.
Εξετάστε την κλάση UserManager, η οποία φορτώνει το προφίλ, αποθηκεύει ρυθμίσεις και στέλνει emails. Αυτές είναι τρεις διαφορετικές ευθύνες, κάθε μία πρέπει να διαχωριστεί σε ξεχωριστή κλάση: UserProfileRepository (φόρτωση), UserSettingsStorage (αποθήκευση) και EmailService (αποστολή). Ο κώδικας πελάτη (ViewModel) χρησιμοποιεί και τις τρεις μέσω Dependency Injection, και κάθε κλάση δοκιμάζεται εύκολα απομονωμένα και αλλάζει χωρίς να επηρεάζει τις άλλες.
// ❌ Παράβαση SRP: Το Activity γνωρίζει για δίκτυο, DB και UI
class ProfileActivity : AppCompatActivity() {
fun loadProfile() {
api.getUser() // Κλήση δικτύου
db.saveUser() // Εργασία με DB
updateUI() // Ενημέρωση UI
}
}
// ✅ SRP τηρείται: τα επίπεδα διαχωρίστηκαν
class ProfileViewModel : ViewModel() {
private val repo = UserRepository()
fun loadProfile() { repo.getUser() }
}
Σημάδια παραβίασης SRP: η κλάση περιέχει πάνω από 200 γραμμές, έχει μεθόδους από διαφορετικούς τομείς, αλλάζει συχνά για διαφορετικούς λόγους. Για την ανάπτυξη Android ο κανόνας είναι απλός: το Activity είναι υπεύθυνο μόνο για τον κύκλο ζωής της οθόνης, το ViewModel — για την κατάσταση UI, το Repository — για τις πηγές δεδομένων.
Η αρχή SRP εφαρμόζεται όχι μόνο σε κλάσεις, αλλά και σε αρχιτεκτονική επιπέδου υπηρεσίας. Κάθε μικρουπηρεσία είναι υπεύθυνη για μία οντότητα τομέα: UserService — μόνο χρήστες, PaymentService — μόνο πληρωμές, NotificationService — μόνο ειδοποιήσεις. Αυτό επιτρέπει την κλιμάκωση, ανάπτυξη και δοκιμή υπηρεσιών ανεξάρτητα. Σε μια κινητή εφαρμογή, το SRP σε επίπεδο μικρουπηρεσιών εκδηλώνεται στη διαίρεση των API πελατών ανά τομέα.
Open-Closed Principle (OCP) — η αρχή ανοικτού/κλειστού. Οι κλάσεις πρέπει να είναι ανοικτές για επέκταση (μπορεί να προστεθεί νέα συμπεριφορά) και κλειστές για τροποποίηση (ο υπάρχων κώδικας δεν αλλάζει). Επιτυγχάνεται μέσω πολυμορφισμού, αφηρημένων κλάσεων και interfaces. Αντί να προστεθεί if-else σε μια υπάρχουσα μέθοδο, δημιουργείται μια νέα υλοποίηση του interface.
Σύμφωνα με Clean Coder Blog, 2014, το OCP είναι πιο αποτελεσματικό σε συνδυασμό με το πρότυπο Strategy. Για παράδειγμα, αν μια εφαρμογή υποστηρίζει διαφορετικές μεθόδους πληρωμής (Google Pay, Apple Pay, PayPal), δεν χρειάζεται να προστεθεί switch-case στον επεξεργαστή πληρωμών. Κάθε μέθοδος πληρωμής υλοποιεί το κοινό interface PaymentGateway, και ένα νέο σύστημα πληρωμής προστίθεται ως νέα κλάση χωρίς τροποποίηση του υπάρχοντος κώδικα.
// ✅ OCP: ανοιχτό για επέκταση, κλειστό για τροποποίηση
interface PaymentGateway {
fun processPayment(amount: Double): Boolean
}
class GooglePayGateway : PaymentGateway {
override fun processPayment(amount: Double) = true
}
// Νέο σύστημα πληρωμών — χωρίς αλλαγή υπάρχοντος κώδικα
class ApplePayGateway : PaymentGateway {
override fun processPayment(amount: Double) = true
}
Liskov Substitution Principle (LSP) — η αρχή αντικατάστασης της Barbara Liskov. Αν το S είναι υποτύπος του T, τότε τα αντικείμενα του T μπορούν να αντικατασταθούν από αντικείμενα S χωρίς να αλλάζουν οι ιδιότητες του προγράμματος. Τυπικά: μια συνάρτηση που χρησιμοποιεί τη βασική κλάση πρέπει να λειτουργεί σωστά με οποιαδήποτε υποκλάση της. Αν η υποκλάση πετάει exception εκεί που η βασική κλάση δεν πετάει — το LSP παραβιάζεται.
Σύμφωνα με Robert C. Martin, 2002, το LSP είναι η πιο δύσκολη στην κατανόηση αρχή SOLID. Κλασικό παράδειγμα παραβίασης — η κλάση Square (τετράγωνο) που κληρονομεί από Rectangle (ορθογώνιο). Αν το setWidth για το Square ορίζει και το πλάτος και το ύψος, ο κώδικας πελάτη που περιμένει συμπεριφορά Rectangle θα λάβει απροσδόκητο αποτέλεσμα. Στην κινητή ανάπτυξη, το LSP παραβιάζεται συχνά κατά την κληρονομικότητα ViewModel, όταν το θυγατρικό ViewModel προσθέτει υποχρεωτικές εξαρτήσεις.
// ❌ Παράβαση LSP: Το Square σπάει τη συμπεριφορά του Rectangle
open class Rectangle(open var width: Int, open var height: Int)
class Square(side: Int) : Rectangle(side, side) {
override var width
get() = super.width
set(value) { super.setBoth(value, value) }
}
Interface Segregation Principle (ISP) — η αρχή διαχωρισμού interfaces. Οι πελάτες δεν πρέπει να εξαρτώνται από interfaces που δεν χρησιμοποιούν. Αντί για ένα “παχύ” interface, δημιουργούνται πολλά στενά, εξειδικευμένα interfaces. Αν μια κλάση υλοποιεί ένα interface αλλά μέρος των μεθόδων πετάει UnsupportedOperationException ή παραμένει κενό — αυτό είναι σαφές σημάδι παραβίασης ISP.
Σύμφωνα με DigitalOcean, 2024, το ISP είναι ιδιαίτερα σχετικό στην κινητή ανάπτυξη κατά τον σχεδιασμό ViewModel και Repository. Αντί για ένα UserRepository interface με όλες τις CRUD μεθόδους, είναι καλύτερο να δημιουργήσετε QueryUserRepository (μόνο ανάγνωση) και CommandUserRepository (εγγραφή). Τότε ο αναγνώστης πελάτης (στοιχείο UI) εξαρτάται μόνο από το Query interface και δεν γνωρίζει για τις μεθόδους εγγραφής.
// ❌ Παχύ interface — ο πελάτης αναγκάζεται να υλοποιεί περιττές μεθόδους
interface UserOperations {
fun getUser(id: String): User
fun saveUser(user: User)
fun deleteUser(id: String)
fun exportUsers(): File
}
// ✅ ISP: διαχωρισμένα interfaces
interface UserReader { fun getUser(id: String): User }
interface UserWriter { fun saveUser(user: User) }
interface UserDeleter { fun deleteUser(id: String) }
Dependency Inversion Principle (DIP) — η αρχή αντιστροφής εξαρτήσεων. Τα modules υψηλού επιπέδου δεν πρέπει να εξαρτώνται από modules χαμηλού επιπέδου. Και τα δύο επίπεδα πρέπει να εξαρτώνται από abstractions (interfaces). Τα abstractions δεν πρέπει να εξαρτώνται από λεπτομέρειες — οι λεπτομέρειες εξαρτώνται από abstractions. Αυτό δεν είναι το ίδιο με το “Dependency Injection” (εισαγωγή εξαρτήσεων), αν και το DI είναι ένας συνηθισμένος τρόπος υλοποίησης του DIP.
Σύμφωνα με Robert C. Martin, 2019, το DIP είναι η βάση της Clean Architecture. Το ViewModel (υψηλό επίπεδο) δεν πρέπει να δημιουργεί άμεσα ένα στιγμιότυπο RetrofitApi (λεπτομέρεια). Αντίθετα, το ViewModel εξαρτάται από το UserRepository interface, και η συγκεκριμένη υλοποίηση UserRepositoryImpl με Retrofit μεταβιβάζεται μέσω του κατασκευαστή. Στο Android, το DIP υλοποιείται μέσω Hilt/Dagger ή Koin: όλες οι εξαρτήσεις παρέχονται μέσω του DI container.
// ✅ DIP: Το Module εξαρτάται από abstraction, όχι από λεπτομέρεια
class UserRepositoryImpl(
private val api: UserApi, // Εξαρτάται από interface
private val db: UserDao // Εξαρτάται από interface
) : UserRepository {
override suspend fun getUser(id: String): User {
return api.fetchUser(id)
}
}
// Hilt DI: οι λεπτομέρειες συνδέονται μέσω DI module
@Module
object NetworkModule {
@Provides
fun provideUserApi(retrofit: Retrofit): UserApi =
retrofit.create(UserApi::class.java)
}
Το SOLID στην κινητή ανάπτυξη εφαρμόζεται σε όλα τα επίπεδα: από την αρχιτεκτονική της εφαρμογής μέχρι μεμονωμένες κλάσεις. Σε Android projects, η Clean Architecture χωρίζει τον κώδικα σε τρία επίπεδα: domain (επιχειρηματική λογική — ανεξάρτητη από frameworks), data (αποθετήρια, API, DB) και presentation (UI, ViewModel). Το επίπεδο domain χρησιμοποιεί τις αρχές SOLID: use case (SRP), interfaces αποθετηρίων (DIP), κλάσεις οντοτήτων (OCP + LSP).
Σύμφωνα με Android Developers Guide, 2025, το SRP στο Android εκδηλώνεται στη διαίρεση ViewModel, Repository και Mapper. OCP — κατά την προσθήκη νέων πηγών δεδομένων μέσω του DataSource interface. LSP — στην ομοιόμορφη επεξεργασία του Result από διαφορετικά αποθετήρια. ISP — στην προσέγγιση CQRS (διαχωρισμός αποθετηρίων Read/Write). DIP — μέσω Hilt/Koin για εισαγωγή εξαρτήσεων.
| Αρχή | Πρόβλημα χωρίς αυτήν | Λύση σε κινητό project |
|---|---|---|
| SRP | Activity 1000+ γραμμών | ViewModel + UseCase + Repository |
| OCP | switch-case ανά τύπο πληρωμής | Strategy: PaymentGateway interface |
| LSP | Σφάλμα κατά αντικατάσταση BaseViewModel | Έλεγχος σύμβασης υποκλάσεων |
| ISP | UnsupportedOperationException | Διαχωρισμός Reader / Writer |
| DIP | ViewModel δημιουργεί χειροκίνητα Retrofit | DI container Hilt / Koin |
Λάθη SOLID συνήθως σχετίζονται με υπερβολική περιπλοκή του κώδικα. Πρώτο — κυριολεκτική τήρηση των αρχών χωρίς να λαμβάνεται υπόψη το πλαίσιο. Η διαίρεση μιας κλάσης UserService σε 10 interfaces και 15 κλάσεις για “καθαρό” ISP είναι υπερσχεδιασμός. Το SOLID είναι εργαλείο, όχι σκοπός. Δεύτερο λάθος — σύγχυση μεταξύ SRP και “μία μέθοδος = μία ευθύνη”. Μια κλάση μπορεί να έχει πολλές μεθόδους, αν όλες ανήκουν σε μία περιοχή ευθύνης.
Σύμφωνα με Simple Thread, 2024, τρίτο λάθος — αγνόηση του LSP κατά την κληρονομικότητα ViewModel στο Android. Αν το βασικό ViewModel περιμένει LiveData και το θυγατρικό χρησιμοποιεί StateFlow — ο κώδικας πελάτη που είναι εγγεγραμμένος στο LiveData δεν θα λάβει ενημερώσεις. Τέταρτο — παραβίαση DIP για χάρη δοκιμών: το RepositoryImpl δημιουργεί άμεσα ένα στιγμιότυπο OkHttpClient, καθιστώντας αδύνατες τις μοναδιαίες δοκιμές.
Χρυσός κανόνας: εφαρμόστε το SOLID όταν λύνει ένα πραγματικό πρόβλημα (συχνές αλλαγές, δυσκολία δοκιμών, επανάληψη). Για απλές οθόνες CRUD, η αυστηρή τήρηση και των πέντε αρχών είναι υπερβολή. Για επιχειρηματική λογική, οικονομικούς υπολογισμούς και αλληλεπιδράσεις API, το SOLID είναι υποχρεωτικό.
Clean Architecture (Robert C. Martin, 2012) — άμεση εφαρμογή του SOLID σε επίπεδο επιπέδων εφαρμογής. Το SRP καθορίζει τα όρια use case (κάθε use case — μία κλάση). Το OCP υλοποιείται μέσω interfaces αποθετηρίων (το επίπεδο Data μπορεί να αλλάξει χωρίς να τροποποιηθεί το Domain). Το ISP παρέχει τον διαχωρισμό του Use Case σε input/output boundary. Το DIP — την κατεύθυνση εξαρτήσεων προς τα μέσα του επιπέδου Domain. Το LSP εγγυάται ότι κάθε υλοποίηση αποθετηρίου είναι αντικαταστάσιμη χωρίς να σπάει το use case.
Συχνές ερωτήσεις
SOLID — πέντε κανόνες γραφής κώδικα για να είναι εύκολο να αλλάξει, να δοκιμαστεί και να κατανοηθεί. Κάθε γράμμα είναι μία αρχή: μην γράφετε μεγάλες κλάσεις (SRP), μην αλλάζετε τον υπάρχοντα κώδικα — προσθέστε νέο (OCP), μην σπάτε τη συμπεριφορά των κληρονόμων (LSP) και άλλα.
SRP (Single Responsibility) θεωρείται η πιο σημαντική επειδή η παραβίασή της οδηγεί σε God-classes — τεράστιες κλάσεις που είναι δύσκολο να δοκιμαστούν και να αλλάξουν. Ωστόσο, χωρίς DIP (Dependency Inversion) ο κώδικας παραμένει σφιχτά συνδεδεμένος, που είναι επίσης κρίσιμο.
Δεν είναι υποχρεωτικό, αλλά συνιστάται ιδιαίτερα για εμπορικά projects με μεγάλο κύκλο ζωής. Για απλές εφαρμογές (μία οθόνη, χωρίς επιχειρηματική λογική) το SOLID μπορεί να είναι υπερβολικό. Για projects με 50+ οθόνες και 3+ προγραμματιστές, το SOLID είναι το απαραίτητο ελάχιστο.
Συνέπειες: οι κλάσεις γίνονται “παχιές” (1000+ γραμμές), μια αλλαγή σε ένα μέρος σπάει τρία άλλα, είναι αδύνατο να γραφούν μοναδιαίες δοκιμές, η προσθήκη νέας λειτουργίας παίρνει εβδομάδες αντί για ημέρες. Με τον καιρό, ο κώδικας μετατρέπεται σε “Big Ball of Mud” — μπερδεμένος και εύθραυστος.
Σημάδια τήρησης: κάθε κλάση έχει λιγότερες από 200 γραμμές, μια αλλαγή λειτουργίας δεν επηρεάζει 5+ αρχεία, οι δοκιμές γράφονται χωρίς mocking 10 εξαρτήσεων, ένας νέος προγραμματιστής κατανοεί τη δομή σε μία μέρα. Εργαλεία όπως SonarQube και detekt βοηθούν στον εντοπισμό παραβιάσεων SRP και DIP.
Περίληψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης