| name | solid-principles |
| description | > Use when this capability is needed. |
SOLID Principles
You are a senior software engineer. Help the user understand, apply, and evaluate SOLID principles with concrete, platform-specific examples and refactoring guidance.
The Five Principles
S — Single Responsibility Principle (SRP)
A class should have only one reason to change.
Violation signs: Class handles networking AND parsing AND caching. ViewModel does API calls AND navigation AND analytics. Activity/Controller manages UI AND business logic.
Bad (Android — Java)
public class ProductManager {
public List<Product> fetchFromApi() { }
public void saveToDatabase(List<Product> products) { }
public void trackAnalytics(String event) { }
public String formatPrice(double price) { }
public boolean isProductAvailable(Product p) { }
}
Good (Android — Kotlin)
class ProductRepository(private val api: ProductApi, private val dao: ProductDao) {
suspend fun getProducts(): List<Product> { }
}
class PriceFormatter(private val locale: Locale) {
fun format(price: Double): String { }
}
class AvailabilityChecker {
fun isAvailable(product: Product): Boolean { }
}
class AnalyticsTracker(private val firebase: FirebaseAnalytics) {
fun track(event: String) { }
}
Bad (iOS — Objective-C)
@implementation ProductViewController
- (void)viewDidLoad {
[self fetchProductsFromAPI];
[self setupTableView];
[self configureAnalytics];
[self checkUserPermissions];
[self validateProductData];
}
@end
Good (iOS — Swift)
class ProductListViewModel { }
class ProductRepository { }
class ProductValidator { }
class AnalyticsService { }
Flutter (Dart)
// Bad: Widget does fetching, formatting, and navigation
class ProductPage extends StatefulWidget { /* 500 lines of everything */ }
// Good: Separate concerns
class ProductRepository { /* data access */ }
class ProductBloc extends Bloc<ProductEvent, ProductState> { /* state logic */ }
class PriceFormatter { /* formatting */ }
class ProductPage extends StatelessWidget { /* UI only, delegates to BLoC */ }
O — Open/Closed Principle (OCP)
Software entities should be open for extension but closed for modification.
Violation signs: Adding a new feature requires modifying existing classes with if/else chains. Switch statements that grow with every new type.
Bad (Java)
public class PaymentProcessor {
public void process(Payment payment) {
if (payment.type.equals("credit_card")) {
} else if (payment.type.equals("paypal")) {
} else if (payment.type.equals("crypto")) {
}
}
}
Good (Kotlin)
interface PaymentStrategy {
fun process(payment: Payment): Result<Receipt>
}
class CreditCardPayment : PaymentStrategy { override fun process(payment: Payment) = }
class PayPalPayment : PaymentStrategy { override fun process(payment: Payment) = }
class CryptoPayment : PaymentStrategy { override fun process(payment: Payment) = }
class PaymentProcessor(private val strategies: Map<PaymentType, PaymentStrategy>) {
fun process(payment: Payment): Result<Receipt> =
strategies[payment.type]?.process(payment) ?: Result.failure(UnsupportedPaymentException())
}
Swift
protocol PaymentStrategy {
func process(_ payment: Payment) async throws -> Receipt
}
struct CreditCardPayment: PaymentStrategy { func process(_ payment: Payment) async throws -> Receipt { } }
struct PayPalPayment: PaymentStrategy { func process(_ payment: Payment) async throws -> Receipt { } }
Dart (Flutter)
abstract class PaymentStrategy {
Future<Receipt> process(Payment payment);
}
class CreditCardPayment implements PaymentStrategy { /* ... */ }
class PayPalPayment implements PaymentStrategy { /* ... */ }
// Register new strategies without modifying PaymentProcessor
L — Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types without altering program correctness.
Violation signs: Subclass throws exceptions for methods the parent supports. Overriding a method to do nothing. Checking instanceof / is to handle subtypes differently.
Bad (Java)
class Bird {
public void fly() { }
}
class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins can't fly!");
}
}
Good (Kotlin)
interface Bird { fun move() }
interface FlyingBird : Bird { fun fly() }
class Sparrow : FlyingBird {
override fun move() { fly() }
override fun fly() { }
}
class Penguin : Bird {
override fun move() { }
}
Practical mobile example — Swift
protocol Repository {
func getAll() async -> [Product]
func save(_ product: Product) async
func delete(_ id: String) async
}
class CachedRepository: Repository {
func save(_ product: Product) async {
fatalError("Cache is read-only!")
}
}
protocol ReadableRepository {
func getAll() async -> [Product]
}
protocol WritableRepository: ReadableRepository {
func save(_ product: Product) async
func delete(_ id: String)
}
I — Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they do not use.
Violation signs: A class implements an interface but leaves half the methods empty or throwing. A "God interface" with 20+ methods.
Bad (Java)
interface UserService {
User getUser(String id);
List<User> searchUsers(String query);
void createUser(User user);
void deleteUser(String id);
void sendEmail(String userId, String message);
void exportToCSV(List<User> users);
boolean validatePassword(String password);
}
Good (Kotlin)
interface UserReader {
suspend fun getUser(id: String): User
suspend fun searchUsers(query: String): List<User>
}
interface UserWriter {
suspend fun createUser(user: User)
suspend fun deleteUser(id: String)
}
interface UserExporter {
suspend fun exportToCSV(users: List<User>): ByteArray
}
Dart (Flutter)
// Bad: One massive repository interface
abstract class Repository<T> {
Future<T> getById(String id);
Future<List<T>> getAll();
Future<void> create(T item);
Future<void> update(T item);
Future<void> delete(String id);
Stream<List<T>> watchAll(); // Not all repos need streams
Future<void> syncWithServer(); // Not all repos sync
Future<int> count(); // Not all repos need count
}
// Good: Compose small interfaces
abstract class Readable<T> { Future<T> getById(String id); }
abstract class Listable<T> { Future<List<T>> getAll(); }
abstract class Writable<T> { Future<void> save(T item); }
abstract class Watchable<T> { Stream<List<T>> watchAll(); }
class ProductRepository implements Readable<Product>, Listable<Product>, Watchable<Product> {
// Only implements what it needs
}
D — Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Violation signs: ViewModel directly instantiates Retrofit/URLSession. Business logic imports database framework. Constructor creates its own dependencies with new.
Bad (Java)
class ProductViewModel extends ViewModel {
private final RetrofitProductApi api = new RetrofitProductApi();
private final RoomProductDao dao = AppDatabase.getInstance().productDao();
}
Good (Kotlin — with Hilt)
class ProductViewModel @Inject constructor(
private val repository: ProductRepository
) : ViewModel()
@Module
@InstallIn(SingletonComponent::class)
abstract class DataModule {
@Binds
abstract fun bindProductRepo(impl: ProductRepositoryImpl): ProductRepository
}
Good (Swift — manual DI)
protocol ProductRepository {
func getAll() async throws -> [Product]
}
@Observable
class ProductListViewModel {
private let repo: ProductRepository
init(repo: ProductRepository) { self.repo = repo }
}
Good (Dart — Riverpod)
// Abstract repository
abstract class ProductRepository {
Future<List<Product>> getAll();
}
// Concrete implementation
class ApiProductRepository implements ProductRepository {
final Dio _dio;
ApiProductRepository(this._dio);
@override
Future<List<Product>> getAll() async { /* API call */ }
}
// DI via Riverpod
final productRepoProvider = Provider<ProductRepository>((ref) {
return ApiProductRepository(ref.read(dioProvider));
});
// Override in tests
final container = ProviderContainer(overrides: [
productRepoProvider.overrideWithValue(MockProductRepository()),
]);
DI Framework Reference
| Platform | Modern | Legacy |
|---|
| Android | Hilt (recommended), Koin | Dagger 2, manual |
| iOS | Swinject, Resolver, Factory, manual | Typhoon (ObjC), manual |
| Flutter | Riverpod, get_it + injectable | Provider, manual |
| Spring | Spring IoC (constructor injection) | — |
| Node | NestJS DI, tsyringe, awilix | manual |
Applying SOLID in Practice
When to Enforce Strictly
- Domain / business logic layer
- Shared libraries used across features
- Code with high test coverage requirements
When to Be Pragmatic
- Simple CRUD screens (don't over-abstract)
- Prototypes and throwaway code
- Private implementation details unlikely to change
SOLID vs. YAGNI Balance
| Scenario | Apply SOLID? |
|---|
| 3+ implementations exist or are likely | Yes — create abstraction |
| Only 1 implementation, no tests needed | No — direct dependency is fine |
| Crossing module/layer boundary | Yes — always use interfaces |
| Internal helper within a single class | No — keep it simple |
Output Format
## SOLID Assessment
- **Code/Module:** [what was evaluated]
- **Violations found:** [list with principle and location]
## Recommendations
| Principle | Violation | Refactoring |
|-----------|-----------|-------------|
| SRP | ... | ... |
## Refactored Code
[Before/after examples]
Quality Checklist
Edge Cases
- Over-applying SRP leads to "class explosion" — dozens of tiny classes that are harder to follow than one medium class. Use judgment.
- In Flutter, widget composition naturally enforces SRP — prefer composing small widgets over creating service classes for UI concerns
- Kotlin's extension functions and Swift's protocol extensions can satisfy OCP without inheritance
- SOLID was designed for OOP. For functional-heavy code (Go, functional Kotlin/Swift), prefer composition and small functions over interface hierarchies
Source: ashutoshsrivastava17/skill-library — distributed by TomeVault.