- Simplicity > cleverness
- Do one thing well per unit (fn/class/module)
- Data first; make invalid states unrepresentable
- Composition > inheritance
- Accept minimal interfaces, return concrete types
- Tiny public API
- Communicate, don't share mutable state
- Test behavior; keep tests fast & reliable
- Dart: throw exceptions for exceptional cases
- Model data clearly (
final,const, immutable). - Define minimal I/O interfaces.
- Compose capabilities (avoid subclassing).
- Write straightforward, readable code.
- Test public behavior; document the why.
abstract class Reader { Future<int> read(List<int> buffer); }
abstract class Writer { Future<int> write(List<int> data); }
abstract class Closer { Future<void> close(); }
// Composition
abstract class ReadWriter implements Reader, Writer {}
abstract class ReadWriteCloser implements Reader, Writer, Closer {}
// Minimal Interface Usage
Future<void> copyData(Reader src, Writer dst) async {
final buf = List<int>.filled(1024, 0);
while (true) {
final n = await src.read(buf);
if (n == 0) break;
await dst.write(buf.sublist(0, n));
}
}class FileWriter implements Writer, Closer {
@override Future<int> write(List<int> data) async { /* ... */ }
@override Future<void> close() async { /* ... */ }
}
FileWriter openFile(String path) => FileWriter(path);
Future<void> processData(Reader input) async { /* ... */ }class User {
final String id;
final String name;
const User({required this.id, required this.name});
}
class Cart {
final List<String> _items = [];
List<String> get items => List.unmodifiable(_items);
}// Existing interface you can't change
abstract class LegacyDataSource {
String getData();
}
// Adapter bridges the gap
class LegacyReaderAdapter implements Reader {
final LegacyDataSource _source;
LegacyReaderAdapter(this._source);
@override
Future<int> read(List<int> buffer) async {
final data = _source.getData();
final bytes = utf8.encode(data);
final length = math.min(buffer.length, bytes.length);
buffer.setRange(0, length, bytes);
return length;
}
}// Add logging without modifying original
class LoggingReader implements Reader {
final Reader _reader;
LoggingReader(this._reader);
@override
Future<int> read(List<int> buffer) async {
print('Reading ${buffer.length} bytes');
final result = await _reader.read(buffer);
print('Read $result bytes');
return result;
}
}
// Stack decorators: file -> buffered -> logged
final reader = LoggingReader(BufferedReader(FileReader('data.txt')));class IoError extends Error {
final String message;
const IoError(this.message);
static const endOfFile = IoError('End of file');
static const unexpectedEof = IoError('Unexpected end of file');
static const shortWrite = IoError('Short write');
}
// Usage: Check for specific known errors
Future<int> readData(Reader reader, List<int> buffer) async {
try {
return await reader.read(buffer);
} catch (e) {
if (e == IoError.endOfFile) {
return 0; // Normal end of file
}
rethrow; // Other errors are unexpected
}
}- Throw exceptions, catch at boundaries (API/UI/isolate entry).
- Rethrow with added context; never silently swallow exceptions.
try {
await operation();
} catch (e, stack) {
log('Operation failed: $e', stackTrace: stack);
rethrow;
}Rule of 3: Don't abstract until you have 3+ similar cases.
- 1 instance: Write concrete code
- 2 instances: Consider duplication - it might be cheaper than wrong abstraction
- 3+ instances: Now you can see the real pattern - abstract it
// Wait until you have multiple real use cases
// Don't create BaseService<T> for one concrete service- Keep public API small; hide internals (
_private). - Prefer defaults over extensive configuration.
// lib/library.dart
export 'src/public_api.dart';
// Hide internal implementations in lib/src/_internal.dart- Premature abstraction: Wait until you have multiple real cases.
- Framework-itis: Build tools/libraries, not large frameworks.
- Mock theater: Prefer small fakes and integration tests.
- Unnecessary indirection: Avoid layers without clear purpose.
- Black-box testing (public APIs).
- Test edge cases clearly and realistically.
- Fast, reliable tests with minimal mocks.
class FakeReader implements Reader {
final List<int> _data;
int _index = 0;
FakeReader(this._data);
@override
Future<int> read(List<int> buffer) async {
if (_index >= _data.length) return 0;
final available = math.min(buffer.length, _data.length - _index);
for (int i = 0; i < available; i++) {
buffer[i] = _data[_index + i];
}
_index += available;
return available;
}
}
void main() {
test('copyData copies all data', () async {
final src = FakeReader([1, 2, 3]);
final dst = CollectingWriter();
await copyData(src, dst);
expect(dst.data, [1, 2, 3]);
});
}- Names show what; comments explain why.
- Use clear, consistent naming; include brief usage examples.
/// Buffer size chosen to balance speed and memory usage
const int bufferSize = 1024;
/// Returns the next card with lowest recall probability.
/// This implements the "hardest first" strategy for effective learning.
Card? selectHardestCard(List<Card> dueCards) { /* ... */ }- Intentionally break rules only when necessary.
- Clearly document why and plan to revisit.
// EXCEPTION: Used inheritance due to external API constraints (#45); revisit after update.- Clean data models; immutable defaults.
- Minimal, composable interfaces.
- APIs accept interfaces, return concretes.
- No shared mutable state.
- Boundary-based exception handling.
- No premature abstractions (wait for 3+ cases).
- Behavior-driven tests (fast, clear).
- Clear naming; docs explain why.
- Simpler after this PR than before.