Multiple Generic Abstraction in Dart
Published at Apr 19, 2025
Generic Abstraction Patterns for Scalable Dart Code
When working on larger applications, especially those that involve business logic and calculations, code reusability and maintainability become essential. Dart’s type system, with its support for generics and object-oriented programming (OOP), allows us to build flexible and type-safe abstractions.
In this blog post, we’ll explore a technique called multiple generic abstraction. This design pattern helps developers define reusable calculation structures using generics and abstract classes. We’ll break down a basic example that demonstrates this concept, understand how it works, and when to use it.
Why Use Generic Abstractions?
Generics in Dart allow you to write code that works with different data types while retaining type safety. When combined with abstract classes, generics become a powerful tool for creating APIs or modules that are extensible, testable, and adaptable.
In our case, we aim to define a flexible executor pattern for performing calculations. The executor should be able to:
- Work with different types of inputs (
Param
objects) - Produce different types of outputs (like
int
,double
, etc.) - Enforce consistent contracts using abstract classes
The Abstraction Declaration
Let’s start by looking at the core structure of our abstraction:
import 'dart:async';
abstract class A<U extends Param, T> {
A();
FutureOr<T> calculate(U a);
}
abstract class B<int> extends A<ParamImpl, int> {
B();
@override
int calculate(ParamImpl b);
}
abstract class Param {}
Explanation
abstract class A<U extends Param, T>
: This is our base generic abstract class. It defines a methodcalculate
that takes a parameter of typeU
(which must extendParam
) and returns a result of typeT
. The return type isFutureOr<T>
, allowing for both synchronous and asynchronous implementations.abstract class B<int> extends A<ParamImpl, int>
: This is a more concrete abstract class, which extendsA
by fixing the types: it always takes aParamImpl
and returns anint
. This lets us define variations of calculations while narrowing down the types.abstract class Param {}
: This is a base class for any input type we want to pass toA
. It allows us to define multiple types of parameter implementations while keeping the API consistent.
The Concrete Implementation
Now let’s look at how we would implement these abstract classes:
class ParamImpl extends Param {}
class BImpl implements B {
@override
int calculate(ParamImpl b) {
return 0;
}
}
Explanation
ParamImpl
: A concrete implementation of theParam
class. This would typically include fields and methods relevant to the calculation logic.BImpl
: This class implementsB
, which already extendsA
. By doing so,BImpl
inherits the contract to calculate something using aParamImpl
and return anint
. In this example, it simply returns0
, but in a real scenario, you’d include the actual computation here.
Benefits of This Pattern
- Code Reusability: You can define new types of calculations by simply implementing or extending the base classes.
- Type Safety: Dart’s generics ensure that the types passed around are valid, reducing runtime errors.
- Flexibility: You can define new parameter classes and result types without changing the base logic.
- Separation of Concerns: Logic for input structure (
Param
) is decoupled from the calculation logic.
Use Cases
This pattern is particularly useful in scenarios like:
- Data transformation layers
- Business rule engines
- Mathematical or financial calculators
- Workflow processors that vary by type of input and output
Final Thoughts
Multiple generic abstraction is a clean and scalable approach to managing complex logic in Dart. It leverages the power of generics and OOP to ensure code is modular, maintainable, and extensible. If you’re building a system where operations vary based on input and output types, consider adopting this design.