OOP: Abstract Classes & Interfaces
Define contracts and build pluggable architectures with abstractions.
Abstract Classes
Abstraction is the practice of hiding complex implementation details behind a simple, well-defined interface. Abstract classes are the primary tool for this in Dart. They cannot be instantiated directly — instead, they define a contract that concrete subclasses must fulfill.
Think of an abstract class as a blueprint. It tells you what methods exist, but not how they work. Each subclass provides its own implementation. This lets you write code that depends on the abstract interface without knowing or caring about the specific implementation.
abstract class DatabaseAdapter {
String get name;
// Abstract method — must be implemented
void connect();
void disconnect();
void save(String key, Map<String, dynamic> data);
Map<String, dynamic>? find(String key);
// Concrete method — shared implementation
void saveBatch(Map<String, Map<String, dynamic>> items) {
items.forEach((key, data) => save(key, data));
print('[$name] Saved ${items.length} items');
}
}
Multiple Interfaces
Unlike some languages, Dart doesn't have a separate interface keyword. Every class implicitly defines an interface. Use implements to conform to multiple interfaces:
// Define capability interfaces
abstract class Serializable {
Map<String, dynamic> toJson();
}
abstract class Validatable {
bool isValid();
List<String> errors();
}
// A class can implement BOTH interfaces
class User implements Serializable, Validatable {
String name;
String email;
User(this.name, this.email);
@override
Map<String, dynamic> toJson() => {'name': name, 'email': email};
@override
bool isValid() => email.contains('@');
@override
List<String> errors() =>
email.contains('@') ? [] : ['Invalid email format'];
}
Interfaces
In Dart, every class implicitly defines an interface. Use implements to conform to any class's API:
class SqlAdapter implements DatabaseAdapter {
String _connectionString;
SqlAdapter(this._connectionString);
@override
String get name => 'SQLite';
@override
void connect() => print('Connected to SQL: $_connectionString');
@override
void disconnect() => print('Disconnected from SQL');
@override
void save(String key, Map<String, dynamic> data) {
print('[SQL] INSERT $key: $data');
}
@override
Map<String, dynamic>? find(String key) {
print('[SQL] SELECT * WHERE key=$key');
return {'key': key, 'source': 'sql'};
}
}
Multiple Interfaces
One of Dart's unique features is that every class implicitly defines an interface. This means you can implement any class's API without inheriting from it. This is incredibly powerful for building flexible, decoupled architectures.
For example, you might have a DatabaseAdapter abstract class, and then use implements to create a Hive adapter that conforms to the same contract — without extending the abstract class. This lets you mix and match capabilities from multiple unrelated class hierarchies:
class HiveAdapter implements DatabaseAdapter {
String _boxName;
HiveAdapter(this._boxName);
@override
String get name => 'Hive';
@override
void connect() => print('Opened Hive box: $_boxName');
@override
void disconnect() => print('Closed Hive box');
@override
void save(String key, Map<String, dynamic> data) {
print('[Hive] PUT $key: $data');
}
@override
Map<String, dynamic>? find(String key) {
print('[Hive] GET $key');
return {'key': key, 'source': 'hive'};
}
}
Real-World Example: Pluggable Database Adapter
void main() {
// Works with any DatabaseAdapter — no coupling
List<DatabaseAdapter> adapters = [
SqlAdapter('sqlite:///app.db'),
HiveAdapter('cache_box'),
];
for (var db in adapters) {
db.connect();
db.save('user_1', {'name': 'Alice'});
var result = db.find('user_1');
print('Found: $result\n');
db.disconnect();
}
}
Summary
abstract classdefines contracts with optional concrete methodsimplementsrequires fulfilling all methods of a class's interface- Dart supports multiple interface implementation
- Abstract classes provide shared code; interfaces define pure contracts