February 25, 2025 · 4 min read
The Pillars of OOP in Dart
Encapsulation, inheritance, polymorphism, abstraction — plus interfaces, mixins, and composition. Each pillar of object-oriented Dart, with a concrete example.
Once you can write a class, the next step is relating classes to one another. That's what the pillars of OOP are about. Dart supports the classic four — encapsulation, inheritance, polymorphism, abstraction — plus first-class tools like interfaces, mixins, and composition. Here's each with a concrete example.
Encapsulation
Hide internal state and expose a controlled surface. In Dart, a leading underscore makes a member library-private.
class BankAccount {
BankAccount(this._balance);
double _balance; // private — not accessible outside this library
double get balance => _balance; // read-only view
void deposit(double amount) {
if (amount <= 0) throw ArgumentError('amount must be positive');
_balance += amount;
}
}
final acc = BankAccount(100);
acc.deposit(50);
print(acc.balance); // 150 — but you can't set _balance directly
Encapsulation lets you enforce invariants: there's no way to reach an invalid balance from outside.
Inheritance
extends lets a class reuse and specialize another. super calls the parent; @override replaces its behavior.
class Animal {
Animal(this.name);
final String name;
void describe() => print('$name is an animal');
}
class Dog extends Animal {
Dog(super.name);
@override
void describe() {
super.describe(); // reuse the parent
print('$name is a dog'); // then specialize
}
}
Dog('Rex').describe(); // Rex is an animal / Rex is a dog
Use inheritance sparingly — it's the tightest coupling in OOP. Prefer it only for genuine "is-a" relationships.
Polymorphism
Code written against a base type works with any subtype. The right method is chosen at runtime — dynamic dispatch.
void makeItSpeak(Animal a) => a.describe(); // knows only Animal
makeItSpeak(Dog('Rex')); // runs Dog.describe
makeItSpeak(Animal('??')); // runs Animal.describe
One function, many behaviors, chosen by the object's actual type. This is what makes OOP extensible.
Abstraction
An abstract class defines what without how. It can't be instantiated; subclasses must fill in the abstract methods.
abstract class Shape {
double area(); // no body — subclasses must implement
void printArea() => print('Area is ${area()}'); // shared behavior
}
class Rectangle extends Shape {
Rectangle(this.w, this.h);
final double w, h;
@override
double area() => w * h;
}
Rectangle(3, 4).printArea(); // Area is 12
Interfaces
Dart has no separate interface keyword — every class defines an implicit interface. Use implements to promise a type's API without inheriting its code. abstract interface class makes the intent explicit.
abstract interface class Logger {
void log(String message);
}
class ConsoleLogger implements Logger {
@override
void log(String message) => print('[LOG] $message');
}
class SilentLogger implements Logger {
@override
void log(String message) {} // does nothing
}
Now any function can accept a Logger and callers choose the implementation — the foundation of the Dependency Inversion principle.
Mixins
A mixin injects reusable behavior into many classes without inheritance. Add it with with.
mixin Swimmer {
void swim() => print('swimming');
}
mixin Flyer {
void fly() => print('flying');
}
class Duck extends Animal with Swimmer, Flyer {
Duck(super.name);
}
final d = Duck('Donald');
d.swim(); // swimming
d.fly(); // flying
Mixins solve the "I need this behavior in several unrelated classes" problem that single inheritance can't.
Composition
Instead of inheriting, a class can hold other objects and delegate to them — a "has-a" relationship. Composition is more flexible than inheritance because you can swap the parts.
class Engine {
void start() => print('engine started');
}
class Car {
Car(this._engine);
final Engine _engine; // Car HAS-A Engine
void drive() {
_engine.start();
print('driving');
}
}
Car(Engine()).drive();
The well-worn advice "favor composition over inheritance" holds in Dart too: composition keeps classes small, decoupled, and easy to test.
Putting it together
These aren't competing techniques — you use them together. Encapsulate state, define behavior behind interfaces, share cross-cutting logic with mixins, compose objects from smaller ones, and reserve inheritance for true specialization. That balance is what separates code that ages well from code that calcifies.