dart-archive / di.dart

DEPRECATED
https://webdev.dartlang.org/angular/guide/dependency-injection
MIT License
65 stars 57 forks source link

named binding #195

Open jonaskello opened 10 years ago

jonaskello commented 10 years ago

Maybe this is already possible but I cannot find any way to do it. Most di frameworks I've worked with before support binding to a name. So I guess something like this:

abstract class MyInterface {}
class MyClassA implements MyInterface {}
class MyClassB implements MyInterface {}
bind(MyClassA, toName: "ImplA")
bind(MyClassB, toName: "ImplB")
MyInterface implToUse = injector.getByName("ImplB")

The name does not have to be a string, it would be better if any object could be used as a key/name.

pavelgj commented 10 years ago

What's the use case?

jonaskello commented 10 years ago

Well basically every case where you have alternative implementations of an interface and want to pick one at run-time. One example may be that you have a logger interface that have several implementations and you want to be able to select one from config. Say you have one implementation that writes to a file and another that writes to a database. Something like this:

abstract class Logger { void LogMessage(); }
class WriteToFileLogger implements Logger { ... }
class WriteToDatabaseLogger implements Logger { }
bind(WriteToFileLogger, toName: "FileLogger");
bind(WriteTODatabaseLogger, toName: "DBLogger");
var configuredLogger = GetConfiguredLoggerFromConfigFile();
var logger = injector.getByName(configuredLogger);
logger.LogMessage("Hello World!"); // Will be logged to file or database depending on config file

Another example that is my current case is when you have a messaging framework and want to get a named handler for particular message while all handlers implement the same interface.

pavelgj commented 10 years ago

Use annotations for this.

bind(Logger, toImplementation: DbLogger, withAnnonation: const UseDb());
bind(Logger, toImplementation: ConsoleLogger, withAnnonation: const UseConsole());

class MyService {
  MyService(@UseDb() Logger logger) {
    ...
  }
}

or

var loggerType = logToDb ? const UseDb() : const UseConsole();
injector.getByKey(new Key(Logger, loggerType));
jonaskello commented 10 years ago

Ok, but I was hoping to achieve this without annotations. In general I think annotations is a bad idea since they spread your framework specific DI concerns across the whole codebase. I usually want to keep the DI concerns contained in the module classes leaving all other classes ignorant of the dependency injection framework.

Is there a reason to not support keyed bindings that don't require annotations (instead keyed by string or other objects)?

jonaskello commented 10 years ago

Just to clarify. Of course you need some code to be aware of the key but I much rather encapsulate this in something like a factory than maintain annotations across all classes in my codebase.

jonaskello commented 10 years ago

From using DI in C# over the last years think this is the pattern that has emerged:

1st generation: Using XML files to specify registrations and injections (eg. early StructureMap for .NET) 2nd generation: Using annotation to specify registrations and injections (eg.Microsoft Unity for .NET) 3rd generation: Using module files while keeping the codebase ignorant of all DI concerns (eg. AutoFac for .NET)

If possible, I would like dart to have capabilities in the 3rd generation :-)

jonaskello commented 10 years ago

To be more specific about the problem with the suggested annotation solution. If you have 100 classes using the logger and you want to switch implementation you will have to edit the annotation from @UseDb to @UseFile in all 100 classes. This is if I understood the usage of correctly?

pavelgj commented 10 years ago

@jonaskello

A couple of points:

jonaskello commented 10 years ago

Well, I would not need the annotations unless the DI framework required it, which in IMHO makes it a DI framework specific concern. Compare with persistance ignorance for persistance frameworks. I like to keep things as POCO/POJO as possible or in Dart's case PODO :-).

I do like that the di package does not require you to use built-in framework specific attributes which is the case in many other annotation-based DI frameworks. But I prefer a solution that don't rely on annotations at all.

Could you elaborate on the string-based abstraction? I'm not sure how I would do that?

jonaskello commented 10 years ago

Actually if it was possible to do a non-annotation abstraction that would allow any object to be used as key that would be even better. This could help avoid magic-strings in some situations.

jonaskello commented 10 years ago

I tried your sample and did some experiments. It seems what I want to do is already possible. This code works now but not sure if you will support it in the future?

import 'package:di/di.dart';

class Logger { LogMessage(String message); }
class DbLogger implements Logger { LogMessage(String message) { print('DbLogger says: $message'); } }
class ConsoleLogger implements Logger { LogMessage(String message) { print('ConsoleLogger says: $message'); } }

void main() {
  var module = new Module()
    ..bind(Logger, toImplementation: DbLogger, withAnnotation: "UseDb")
    ..bind(Logger, toImplementation: ConsoleLogger, withAnnotation: "UseConsole");

  var injector = new ModuleInjector([module]);
  bool logToDb = true;
  var loggerType = logToDb ? "UseDb" : "UseConsole";

  var theLogger = injector.getByKey(new Key(Logger, loggerType));
  theLogger.LogMessage('Hello!');
}
jonaskello commented 10 years ago

Here is another example that uses a Message type as key to pick a handler. It also works but outputs DEPRECATED messages:

import 'package:di/di.dart';

abstract class MessageHandler<TMessage> { Handle(TMessage message); }
abstract class Message {  }

class SaveMessage implements Message {  }
class DeleteMessage implements Message {  }

class SaveMessageHandler implements MessageHandler<SaveMessage> { Handle(SaveMessage message) { print('Save!'); } }
class DeleteMessageHandler implements MessageHandler<DeleteMessage> { Handle(DeleteMessage message) { print('Delete!'); } }

void main() {
  var module = new Module()
    ..bind(MessageHandler, toImplementation: SaveMessageHandler, withAnnotation: SaveMessage)
    ..bind(MessageHandler, toImplementation: DeleteMessageHandler, withAnnotation: DeleteMessage);
  var injector = new ModuleInjector([module]);

  var message = new SaveMessage();

  MessageHandler handler = injector.getByKey(new Key(MessageHandler, SaveMessage));
  handler.Handle(message);
}

Output

DEPRECATED: Use `withAnnotation: const SaveMessage()` instead of `withAnnotation: SaveMessage`.
DEPRECATED: Use `withAnnotation: const DeleteMessage()` instead of `withAnnotation: DeleteMessage`.
Save!