Skip to content

Instantly share code, notes, and snippets.

@gabsn
Created September 12, 2025 10:47
Show Gist options
  • Select an option

  • Save gabsn/d39b3f2d1cc81f60a19798f1f99c2c21 to your computer and use it in GitHub Desktop.

Select an option

Save gabsn/d39b3f2d1cc81f60a19798f1f99c2c21 to your computer and use it in GitHub Desktop.

Rob Pike's Software Engineering Philosophy

Non-Negotiables

  • 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

Workflow (Step-by-Step)

  1. Model data clearly (final, const, immutable).
  2. Define minimal I/O interfaces.
  3. Compose capabilities (avoid subclassing).
  4. Write straightforward, readable code.
  5. Test public behavior; document the why.

Essential Patterns

Single-Method Interfaces & Composition

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));
  }
}

Accept Interfaces, Return Concretes

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 { /* ... */ }

Immutable Data & Safe Exposure

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);
}

Adapter Pattern: Bridge Incompatible Interfaces

// 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;
  }
}

Decorator Pattern: Add Cross-Cutting Concerns

// 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')));

Sentinel Errors for Common Conditions

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
  }
}

Error Handling (Practical Approach)

  • 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;
}

When NOT to Abstract

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

API & Module Hygiene

  • 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

Anti-patterns (Avoid)

  • 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.

Testing (Behavior-Focused)

  • 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]);
  });
}

Naming & Documentation

  • 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) { /* ... */ }

Breaking Rules (Pragmatically)

  • 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.

Quick PR Checklist

  • 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment