OOP, SOLID Principles & Composition
Architecture Interview Questions Start Here
Open interactive version (quiz + challenge)Real-world analogy
What is it?
OOP in Dart combines classes, mixins, abstract classes, and interfaces. SOLID principles guide how to structure code: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion. Composition over inheritance means building behavior from smaller pieces rather than deep class hierarchies. These concepts are tested in every architecture interview.
Real-world relevance
In a SaaS collaboration app, Clean Architecture embodies all SOLID principles: BLoCs have single responsibilities (ChatBloc, AuthBloc). Repositories implement abstract interfaces (DIP). New features extend the system without modifying existing code (OCP). Sealed states model each feature exhaustively. GetIt injects dependencies as abstractions.
Key points
- Classes and Constructors in Dart — Dart classes have named constructors, factory constructors, const constructors, and initializer lists. Const constructors create compile-time constant instances. Factory constructors can return cached instances or subtypes. Interview: Explain const constructor benefits.
- Single Responsibility Principle (SRP) — A class should have ONE reason to change. UserRepository handles data access. UserValidator handles validation. UserNotifier handles notifications. NOT: UserManager that does all three. Interview: Break a god class into focused classes.
- Open/Closed Principle (OCP) — Classes should be open for extension, closed for modification. Add new behavior by creating new classes (strategy pattern, new subclasses), not by modifying existing code. In Flutter: add new BLoC events without changing existing event handlers.
- Liskov Substitution Principle (LSP) — Subtypes must be substitutable for their parent types. If fetchData(Repository repo) works with BaseRepository, it must work with any Repository subclass. Breaking LSP: a ReadOnlyRepo that throws on write() — it violates the contract of its parent.
- Interface Segregation Principle (ISP) — Don't force clients to depend on methods they don't use. Split fat interfaces: instead of CrudRepository with read/write/delete, have ReadRepository and WriteRepository. Classes implement only what they need.
- Dependency Inversion Principle (DIP) — High-level modules should NOT depend on low-level modules. Both should depend on abstractions. UserBloc depends on abstract UserRepository, not on concrete ApiUserRepository. This enables testing (inject mock) and flexibility (swap implementations).
- Composition Over Inheritance — Prefer composing behavior from smaller pieces rather than deep inheritance chains. Use mixins, DI, and delegation instead of class A extends B extends C extends D. Deep inheritance is fragile — changing a parent breaks all children.
- Abstract Classes vs Interfaces — In Dart, every class is implicitly an interface. Abstract classes provide partial implementation. Use abstract class for shared behavior + contract. Use 'implements' for pure contract. Interview: When would you use extends vs implements?
- Factory Pattern — Factory constructors decide which concrete type to create: factory Logger(String type) { if (type == 'file') return FileLogger(); return ConsoleLogger(); }. Used for DI containers, service locators, and creating instances from JSON.
- SOLID in Flutter Practice — BLoC follows SRP (one feature per BLoC). Repository pattern follows DIP (BLoC depends on abstract repo). GetIt follows DIP (register abstractions, resolve concretions). Sealed states follow OCP (add states without changing BLoC logic).
Code example
// OOP & SOLID in Dart/Flutter
// --- SINGLE RESPONSIBILITY ---
// BAD: God class doing everything
// class UserManager {
// Future<User> fetchUser() { ... }
// bool validateEmail(String email) { ... }
// Future<void> sendNotification() { ... }
// Future<void> cacheUser() { ... }
// }
// GOOD: Focused classes
abstract class UserRepository {
Future<User> getById(String id);
Future<void> save(User user);
}
class UserValidator {
bool isValidEmail(String email) => email.contains('@') && email.contains('.');
bool isValidName(String name) => name.length >= 2;
}
class UserNotificationService {
Future<void> sendWelcome(User user) async { /* ... */ }
}
// --- DEPENDENCY INVERSION ---
// High-level (BLoC) depends on abstraction, not concrete class
class UserBloc extends Bloc<UserEvent, UserState> {
final UserRepository _repository; // Abstract! Not ApiUserRepository
UserBloc({required UserRepository repository})
: _repository = repository,
super(const UserInitial()) {
on<LoadUser>(_onLoadUser);
}
Future<void> _onLoadUser(LoadUser event, Emitter<UserState> emit) async {
emit(const UserLoading());
try {
final user = await _repository.getById(event.id);
emit(UserLoaded(user));
} catch (e) {
emit(UserError(e.toString()));
}
}
}
// Concrete implementations — swappable
class ApiUserRepository implements UserRepository {
final Dio _dio;
ApiUserRepository(this._dio);
@override
Future<User> getById(String id) async {
final response = await _dio.get('/users/$id');
return User.fromJson(response.data);
}
@override
Future<void> save(User user) async {
await _dio.post('/users', data: user.toJson());
}
}
class MockUserRepository implements UserRepository {
@override
Future<User> getById(String id) async => User(id: id, name: 'Test User');
@override
Future<void> save(User user) async {}
}
// --- COMPOSITION OVER INHERITANCE ---
// BAD: Deep inheritance
// class Animal { void eat() {} }
// class Bird extends Animal { void fly() {} }
// class Penguin extends Bird {} // Penguin can fly?! Broken.
// GOOD: Composition with mixins
mixin CanSwim {
void swim() => print('Swimming!');
}
mixin CanFly {
void fly() => print('Flying!');
}
class Duck with CanSwim, CanFly {}
class Penguin with CanSwim {} // No flying!
// --- FACTORY CONSTRUCTORS ---
abstract class Logger {
void log(String message);
factory Logger(String env) {
return switch (env) {
'production' => CrashlyticsLogger(),
'debug' => ConsoleLogger(),
_ => ConsoleLogger(),
};
}
}
class ConsoleLogger implements Logger {
@override
void log(String message) => print('[LOG] $message');
}
class CrashlyticsLogger implements Logger {
@override
void log(String message) {
// Send to Crashlytics in production
}
}
// --- CONST CONSTRUCTORS ---
class AppColors {
final Color primary;
final Color secondary;
const AppColors({required this.primary, required this.secondary});
static const light = AppColors(
primary: Color(0xFF2196F3),
secondary: Color(0xFF4CAF50),
);
}
// const creates compile-time constants — reused, not recreatedLine-by-line walkthrough
- 1. Abstract UserRepository — the contract that BLoC depends on
- 2. UserValidator — separate class, single responsibility: validation
- 3. UserBloc depends on abstract UserRepository, not a concrete class
- 4. Constructor takes the abstraction — enables injection of any implementation
- 5. On event, delegates to repository — BLoC doesn't know or care about HTTP/DB details
- 6. ApiUserRepository — concrete implementation of the abstract contract
- 7. MockUserRepository — another implementation for testing
- 8. Composition with mixins — Duck can swim AND fly
- 9. Penguin only mixes in CanSwim — no broken fly() method
- 10. Factory constructor — returns different implementations based on input
- 11. Const constructor — enables compile-time constant instances
Spot the bug
class OrderService {
final ApiClient _api;
OrderService(this._api);
Future<Order> createOrder(Cart cart) async {
final isValid = _validateCart(cart);
if (!isValid) throw Exception('Invalid cart');
final order = await _api.post('/orders', cart.toJson());
await _sendConfirmationEmail(order);
await _updateInventory(order);
await _notifyWarehouse(order);
return Order.fromJson(order);
}
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Dart Classes (Dart Official)
- SOLID Principles in Dart (Dart Official)
- Mixins in Dart (Dart Official)
- Abstract Classes (Dart Official)