refactor: adding alias management and extender support 96 pass
This commit is contained in:
parent
786224caf5
commit
972c0424e1
4 changed files with 463 additions and 15 deletions
|
@ -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,8 +738,16 @@ class Container {
|
|||
while (search != null) {
|
||||
var building = _buildStack.last;
|
||||
var contextMap = search._contextual[building];
|
||||
if (contextMap != null && contextMap.containsKey(abstract)) {
|
||||
return contextMap[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,8 +792,16 @@ class Container {
|
|||
while (search != null) {
|
||||
var building = _buildStack.last;
|
||||
var contextMap = search._contextual[building];
|
||||
if (contextMap != null && contextMap.containsKey(type)) {
|
||||
return true;
|
||||
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;
|
||||
}
|
||||
|
@ -777,6 +809,80 @@ class Container {
|
|||
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)) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
230
incubation/container/container/test/alias_test.dart
Normal file
230
incubation/container/container/test/alias_test.dart
Normal 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>());
|
||||
});
|
||||
});
|
||||
}
|
113
incubation/container/container/test/extend_test.dart
Normal file
113
incubation/container/container/test/extend_test.dart
Normal 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));
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue