refactor: adding alias management and extender support 96 pass

This commit is contained in:
Patrick Stewart 2024-12-26 22:40:34 -07:00
parent 786224caf5
commit 972c0424e1
4 changed files with 463 additions and 15 deletions

View file

@ -24,6 +24,12 @@ class Container {
final Map<Type, dynamic Function(Container)> _factories = {};
final Map<String, dynamic> _namedSingletons = {};
/// The container's type aliases
final Map<Type, Type> _aliases = {};
/// The container's service extenders
final Map<Type, List<dynamic Function(dynamic, Container)>> _extenders = {};
/// The container's contextual bindings
final Map<Type, Map<Type, dynamic>> _contextual = {};
@ -135,6 +141,9 @@ class Container {
return false;
}
// Check if the type is aliased
t2 = getAlias(t2);
Container? search = this;
while (search != null) {
if (search._singletons.containsKey(t2)) {
@ -253,6 +262,7 @@ class Container {
/// This method is central to the dependency injection mechanism, allowing for
/// flexible object creation and dependency resolution within the container hierarchy.
T make<T>([Type? type]) {
// Get the original type
Type t2 = T;
if (type != null) {
t2 = type;
@ -324,6 +334,7 @@ class Container {
}
if (instance != null) {
instance = _applyExtenders(t2, instance);
var typedInstance = instance as T;
_fireResolvingCallbacks(typedInstance);
_fireAfterResolvingCallbacks(typedInstance);
@ -354,6 +365,7 @@ class Container {
}
if (instance != null) {
instance = _applyExtenders(t2, instance);
var typedInstance = instance as T;
_fireResolvingCallbacks(typedInstance);
_fireAfterResolvingCallbacks(typedInstance);
@ -361,19 +373,22 @@ class Container {
}
}
// Check for singleton or factory
// Check for singleton or factory, resolving aliases if no contextual binding was found
Container? search = this;
var resolvedType = contextualConcrete == null ? getAlias(t2) : t2;
while (search != null) {
if (search._singletons.containsKey(t2)) {
var instance = search._singletons[t2] as T;
if (search._singletons.containsKey(resolvedType)) {
var instance = search._singletons[resolvedType];
instance = _applyExtenders(resolvedType, instance);
_fireResolvingCallbacks(instance);
_fireAfterResolvingCallbacks(instance);
return instance;
} else if (search._factories.containsKey(t2)) {
var instance = search._factories[t2]!(this) as T;
return instance as T;
} else if (search._factories.containsKey(resolvedType)) {
var instance = search._factories[resolvedType]!(this);
instance = _applyExtenders(resolvedType, instance);
_fireResolvingCallbacks(instance);
_fireAfterResolvingCallbacks(instance);
return instance;
return instance as T;
} else {
search = search._parent;
}
@ -443,11 +458,12 @@ class Container {
var instance = reflectedType.newInstance(
isDefault(constructor.name) ? '' : constructor.name,
positional,
named, []).reflectee as T;
named, []).reflectee;
instance = _applyExtenders(t2, instance);
_fireResolvingCallbacks(instance);
_fireAfterResolvingCallbacks(instance);
return instance;
return instance as T;
} else {
throw BindingResolutionException(
'$t2 is not a class, and therefore cannot be instantiated.');
@ -722,9 +738,17 @@ class Container {
while (search != null) {
var building = _buildStack.last;
var contextMap = search._contextual[building];
if (contextMap != null && contextMap.containsKey(abstract)) {
if (contextMap != null) {
// First try to find a binding for the original type
if (contextMap.containsKey(abstract)) {
return contextMap[abstract];
}
// Then try to find a binding for the aliased type
var aliasedType = getAlias(abstract);
if (aliasedType != abstract && contextMap.containsKey(aliasedType)) {
return contextMap[aliasedType];
}
}
search = search._parent;
}
@ -768,15 +792,97 @@ class Container {
while (search != null) {
var building = _buildStack.last;
var contextMap = search._contextual[building];
if (contextMap != null && contextMap.containsKey(type)) {
if (contextMap != null) {
// First check for binding of original type
if (contextMap.containsKey(type)) {
return true;
}
// Then check for binding of aliased type
var aliasedType = getAlias(type);
if (aliasedType != type && contextMap.containsKey(aliasedType)) {
return true;
}
}
search = search._parent;
}
return false;
}
/// Register an alias for an abstract type.
///
/// This allows you to alias an abstract type to a concrete implementation.
/// For example, you might alias an interface to its default implementation:
/// ```dart
/// container.alias<Logger>(ConsoleLogger);
/// ```
void alias<T>(Type concrete) {
_aliases[T] = concrete;
}
/// Get the concrete type that an abstract type is aliased to.
///
/// If the type is not aliased in this container or any parent container,
/// returns the type itself.
Type getAlias(Type abstract) {
Container? search = this;
while (search != null) {
if (search._aliases.containsKey(abstract)) {
return search._aliases[abstract]!;
}
search = search._parent;
}
return abstract;
}
/// Check if a type is aliased to another type in this container or any parent container.
bool isAlias(Type type) {
Container? search = this;
while (search != null) {
if (search._aliases.containsKey(type)) {
return true;
}
search = search._parent;
}
return false;
}
/// Extend a service after it is resolved.
///
/// This allows you to modify a service after it has been resolved from the container.
/// The callback receives the resolved instance and the container, and should return
/// the modified instance.
///
/// ```dart
/// container.extend<Logger>((logger, container) {
/// logger.level = LogLevel.debug;
/// return logger;
/// });
/// ```
void extend<T>(
dynamic Function(dynamic instance, Container container) callback) {
_extenders.putIfAbsent(T, () => []).add(callback);
}
/// Apply any registered extenders to an instance.
dynamic _applyExtenders(Type type, dynamic instance) {
// Collect all extenders from parent to child
var extenders = <dynamic Function(dynamic, Container)>[];
Container? search = this;
while (search != null) {
if (search._extenders.containsKey(type)) {
extenders.insertAll(0, search._extenders[type]!);
}
search = search._parent;
}
// Apply extenders in order (parent to child)
for (var extender in extenders) {
instance = extender(instance, this);
}
return instance;
}
/// Check if we're in danger of a circular dependency.
void _checkCircularDependency(Type type) {
if (_buildStack.contains(type)) {

View file

@ -56,8 +56,7 @@ class ContextualImplementationBuilder {
/// Specify the implementation that should be used
void give<T>() {
for (var concreteType in concrete) {
container.addContextualBinding(
concreteType, abstract, (Container c) => c.make<T>());
container.addContextualBinding(concreteType, abstract, T);
}
}

View file

@ -0,0 +1,230 @@
import 'package:platformed_container/container.dart';
import 'package:test/test.dart';
import 'common.dart';
// Test interfaces and implementations
abstract class Logger {
void log(String message);
}
class ConsoleLogger implements Logger {
@override
void log(String message) => print('Console: $message');
}
class FileLogger implements Logger {
final String filename;
FileLogger(this.filename);
@override
void log(String message) => print('File($filename): $message');
}
class LoggerClient {
final Logger logger;
LoggerClient(this.logger);
}
class MockReflector extends Reflector {
@override
String? getName(Symbol symbol) => null;
@override
ReflectedClass? reflectClass(Type clazz) => null;
@override
ReflectedType? reflectType(Type type) {
if (type == LoggerClient) {
return MockReflectedClass(
'LoggerClient',
[],
[],
[
MockConstructor([MockParameter('logger', Logger)])
],
[],
type,
(name, positional, named, typeArgs) => LoggerClient(positional[0]),
);
} else if (type == FileLogger) {
return MockReflectedClass(
'FileLogger',
[],
[],
[
MockConstructor([MockParameter('filename', String)])
],
[],
type,
(name, positional, named, typeArgs) => FileLogger(positional[0]),
);
}
return null;
}
@override
ReflectedInstance? reflectInstance(Object? instance) => null;
@override
ReflectedFunction? reflectFunction(Function function) => null;
@override
ReflectedType reflectFutureOf(Type type) => throw UnimplementedError();
}
class MockReflectedClass extends ReflectedClass {
final Function instanceBuilder;
MockReflectedClass(
String name,
List<ReflectedTypeParameter> typeParameters,
List<ReflectedInstance> annotations,
List<ReflectedFunction> constructors,
List<ReflectedDeclaration> declarations,
Type reflectedType,
this.instanceBuilder,
) : super(name, typeParameters, annotations, constructors, declarations,
reflectedType);
@override
ReflectedInstance newInstance(
String constructorName, List positionalArguments,
[Map<String, dynamic> namedArguments = const {},
List<Type> typeArguments = const []]) {
var instance = instanceBuilder(
constructorName, positionalArguments, namedArguments, typeArguments);
return MockReflectedInstance(this, instance);
}
@override
bool isAssignableTo(ReflectedType? other) {
if (other == null) return false;
return reflectedType == other.reflectedType;
}
}
class MockReflectedInstance extends ReflectedInstance {
MockReflectedInstance(ReflectedClass clazz, Object? reflectee)
: super(clazz, clazz, reflectee);
@override
ReflectedInstance getField(String name) {
throw UnimplementedError();
}
}
class MockConstructor extends ReflectedFunction {
final List<ReflectedParameter> params;
MockConstructor(this.params)
: super('', [], [], params, false, false,
returnType: MockReflectedType('void', [], dynamic));
@override
ReflectedInstance invoke(Invocation invocation) {
throw UnimplementedError();
}
}
class MockParameter extends ReflectedParameter {
MockParameter(String name, Type type)
: super(name, [], MockReflectedType(type.toString(), [], type), true,
false);
}
class MockReflectedType extends ReflectedType {
MockReflectedType(String name, List<ReflectedTypeParameter> typeParameters,
Type reflectedType)
: super(name, typeParameters, reflectedType);
@override
ReflectedInstance newInstance(
String constructorName, List positionalArguments,
[Map<String, dynamic> namedArguments = const {},
List<Type> typeArguments = const []]) {
throw UnimplementedError();
}
@override
bool isAssignableTo(ReflectedType? other) {
if (other == null) return false;
return reflectedType == other.reflectedType;
}
}
void main() {
late Container container;
setUp(() {
container = Container(MockReflector());
});
group('Alias Tests', () {
test('alias resolves to concrete type', () {
container.registerSingleton<ConsoleLogger>(ConsoleLogger());
container.alias<Logger>(ConsoleLogger);
var logger = container.make<Logger>();
expect(logger, isA<ConsoleLogger>());
});
test('isAlias returns true for aliased type', () {
container.alias<Logger>(ConsoleLogger);
expect(container.isAlias(Logger), isTrue);
});
test('isAlias returns false for non-aliased type', () {
expect(container.isAlias(ConsoleLogger), isFalse);
});
test('getAlias returns concrete type for aliased type', () {
container.alias<Logger>(ConsoleLogger);
expect(container.getAlias(Logger), equals(ConsoleLogger));
});
test('getAlias returns same type for non-aliased type', () {
expect(container.getAlias(ConsoleLogger), equals(ConsoleLogger));
});
test('alias works with contextual bindings', () {
// Register both logger implementations
container.registerSingleton<ConsoleLogger>(ConsoleLogger());
container.registerSingleton<FileLogger>(FileLogger('test.log'));
// Set up the alias
container.alias<Logger>(ConsoleLogger);
// Set up contextual binding for the interface
container.when(LoggerClient).needs<Logger>().give<FileLogger>();
var logger = container.make<Logger>();
expect(logger, isA<ConsoleLogger>());
var client = container.make<LoggerClient>();
expect(client.logger, isA<FileLogger>());
});
test('child container inherits parent aliases', () {
container.registerSingleton<ConsoleLogger>(ConsoleLogger());
container.alias<Logger>(ConsoleLogger);
var child = container.createChild();
var logger = child.make<Logger>();
expect(logger, isA<ConsoleLogger>());
});
test('child container can override parent aliases', () {
container.registerSingleton<ConsoleLogger>(ConsoleLogger());
container.registerSingleton<FileLogger>(FileLogger('test.log'));
container.alias<Logger>(ConsoleLogger);
var child = container.createChild();
child.alias<Logger>(FileLogger);
var parentLogger = container.make<Logger>();
expect(parentLogger, isA<ConsoleLogger>());
var childLogger = child.make<Logger>();
expect(childLogger, isA<FileLogger>());
});
});
}

View file

@ -0,0 +1,113 @@
import 'package:platformed_container/container.dart';
import 'package:test/test.dart';
class MockReflector extends Reflector {
@override
String? getName(Symbol symbol) => null;
@override
ReflectedClass? reflectClass(Type clazz) => null;
@override
ReflectedType? reflectType(Type type) => null;
@override
ReflectedInstance? reflectInstance(Object? instance) => null;
@override
ReflectedFunction? reflectFunction(Function function) => null;
@override
ReflectedType reflectFutureOf(Type type) => throw UnimplementedError();
}
abstract class Logger {
LogLevel get level;
set level(LogLevel value);
void log(String message);
}
enum LogLevel { debug, info, warning, error }
class ConsoleLogger implements Logger {
LogLevel _level = LogLevel.info;
@override
LogLevel get level => _level;
@override
set level(LogLevel value) => _level = value;
@override
void log(String message) => print('Console: $message');
}
void main() {
late Container container;
setUp(() {
container = Container(MockReflector());
});
group('Service Extender Tests', () {
test('can extend a service after resolution', () {
container.registerSingleton<Logger>(ConsoleLogger());
container.extend<Logger>((logger, container) {
logger.level = LogLevel.debug;
return logger;
});
var logger = container.make<Logger>();
expect(logger.level, equals(LogLevel.debug));
});
test('can apply multiple extenders in order', () {
container.registerSingleton<Logger>(ConsoleLogger());
container.extend<Logger>((logger, container) {
logger.level = LogLevel.debug;
return logger;
});
container.extend<Logger>((logger, container) {
logger.level = LogLevel.error;
return logger;
});
var logger = container.make<Logger>();
expect(logger.level, equals(LogLevel.error));
});
test('child container inherits parent extenders', () {
container.registerSingleton<Logger>(ConsoleLogger());
container.extend<Logger>((logger, container) {
logger.level = LogLevel.debug;
return logger;
});
var child = container.createChild();
var logger = child.make<Logger>();
expect(logger.level, equals(LogLevel.debug));
});
test('child container can add its own extenders', () {
container.registerSingleton<Logger>(ConsoleLogger());
container.extend<Logger>((logger, container) {
logger.level = LogLevel.debug;
return logger;
});
var child = container.createChild();
child.extend<Logger>((logger, container) {
logger.level = LogLevel.error;
return logger;
});
var parentLogger = container.make<Logger>();
expect(parentLogger.level, equals(LogLevel.debug));
var childLogger = child.make<Logger>();
expect(childLogger.level, equals(LogLevel.error));
});
});
}