Lesson 9 of 77 intermediate

OOP, SOLID Principles & Composition

Architecture Interview Questions Start Here

Open interactive version (quiz + challenge)

Real-world analogy

SOLID principles are like rules for building with LEGO. Single Responsibility: each block does one thing. Open/Closed: you can add new blocks without breaking existing ones. Liskov: any blue block should work wherever a blue block is expected. Interface Segregation: don't force a car kit on someone building a house. Dependency Inversion: the instructions depend on the blocks, not the other way around.

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

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 recreated

Line-by-line walkthrough

  1. 1. Abstract UserRepository — the contract that BLoC depends on
  2. 2. UserValidator — separate class, single responsibility: validation
  3. 3. UserBloc depends on abstract UserRepository, not a concrete class
  4. 4. Constructor takes the abstraction — enables injection of any implementation
  5. 5. On event, delegates to repository — BLoC doesn't know or care about HTTP/DB details
  6. 6. ApiUserRepository — concrete implementation of the abstract contract
  7. 7. MockUserRepository — another implementation for testing
  8. 8. Composition with mixins — Duck can swim AND fly
  9. 9. Penguin only mixes in CanSwim — no broken fly() method
  10. 10. Factory constructor — returns different implementations based on input
  11. 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?
How many responsibilities does this class have? What happens when email logic changes?
Show answer
OrderService violates SRP — it validates, creates orders, sends emails, updates inventory, AND notifies warehouse. Any change to email/inventory/notification logic requires modifying this class. Fix: Extract into OrderValidator, OrderRepository, EmailService, InventoryService, WarehouseNotifier. OrderService should coordinate, not implement.

Explain like I'm 5

Imagine you're building a robot. SRP says each part does one job — the arm grabs, the legs walk, the eyes see. Composition is like snapping parts together — you can give your robot wings OR wheels, mix and match. Inheritance is like saying 'my robot must be exactly like Dad-robot plus one extra thing' — but what if Dad-robot has a broken part? Now your robot is broken too!

Fun fact

The SOLID principles were introduced by Robert C. Martin (Uncle Bob) in the early 2000s. The acronym SOLID was actually coined by Michael Feathers. Uncle Bob says the most important principle is Dependency Inversion because it enables all the others — and it's exactly what makes Flutter's testable architecture possible.

Hands-on challenge

Refactor a monolithic UserService class (that handles auth, profile CRUD, avatar upload, and notifications) into SOLID-compliant classes. Show the abstract interfaces, concrete implementations, and how they'd be wired together with dependency injection.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter Interview Mastery