Il y a ce cadre que j'aide à concevoir. Certaines tâches courantes doivent être effectuées à l'aide de composants communs: la journalisation, la mise en cache et le déclenchement d'événements en particulier.
Je ne sais pas s'il est préférable d'utiliser l'injection de dépendances et d'introduire tous ces composants dans chaque service (comme propriétés par exemple) ou dois-je placer une sorte de métadonnées sur chaque méthode de mes services et utiliser l'interception pour effectuer ces tâches courantes ?
Voici un exemple des deux:
Injection:
public class MyService
{
public ILoggingService Logger { get; set; }
public IEventBroker EventBroker { get; set; }
public ICacheService Cache { get; set; }
public void DoSomething()
{
Logger.Log(myMessage);
EventBroker.Publish<EventType>();
Cache.Add(myObject);
}
}
et voici l'autre version:
Interception:
public class MyService
{
[Log("My message")]
[PublishEvent(typeof(EventType))]
public void DoSomething()
{
}
}
Voici mes questions:
Les préoccupations transversales comme la journalisation, la mise en cache, etc. ne sont pas des dépendances, elles ne doivent donc pas être injectées dans les services. Cependant, alors que la plupart des gens semblent alors rechercher un cadre AOP entrelacé complet, il existe un joli modèle de conception pour cela: Décorateur .
Dans l'exemple ci-dessus, laissez MyService implémenter l'interface IMyService:
public interface IMyService
{
void DoSomething();
}
public class MyService : IMyService
{
public void DoSomething()
{
// Implementation goes here...
}
}
Cela maintient la classe MyService complètement exempte de préoccupations transversales, suivant ainsi le principe de responsabilité unique (SRP).
Pour appliquer la journalisation, vous pouvez ajouter un décorateur de journalisation:
public class MyLogger : IMyService
{
private readonly IMyService myService;
private readonly ILoggingService logger;
public MyLogger(IMyService myService, ILoggingService logger)
{
this.myService = myService;
this.logger = logger;
}
public void DoSomething()
{
this.myService.DoSomething();
this.logger.Log("something");
}
}
Vous pouvez implémenter la mise en cache, la mesure, les événements, etc. de la même manière. Chaque décorateur fait exactement une chose, ils suivent donc également le SRP, et vous pouvez les composer de manière arbitrairement complexe. Par exemple.
var service = new MyLogger(
new LoggingService(),
new CachingService(
new Cache(),
new MyService());
Pour une poignée de services, je pense que la réponse de Mark est bonne: vous n'aurez pas à apprendre ou à introduire de nouvelles dépendances tierces et vous suivrez toujours les bons principes SOLID.
Pour une grande quantité de services, je recommanderais un outil AOP comme PostSharp ou Castle DynamicProxy. PostSharp a une version gratuite (comme dans la bière), et ils ont récemment publié PostSharp Toolkit for Diagnostics , (gratuit comme dans la bière ET la parole) qui vous donnera quelques fonctionnalités de journalisation hors de la boîte.
Je trouve que la conception d'un cadre est en grande partie orthogonale à cette question - vous devez d'abord vous concentrer sur l'interface de votre cadre, et peut-être en tant que processus mental d'arrière-plan, pensez à la façon dont quelqu'un pourrait le consommer. Vous ne voulez pas faire quelque chose qui empêche qu'il soit utilisé de manière intelligente, mais cela ne devrait être qu'une entrée dans la conception de votre framework; un parmi tant d'autres.
J'ai rencontré ce problème à maintes reprises et je pense avoir trouvé une solution simple.
Au départ, je suis allé avec le modèle de décorateur et j'ai implémenté manuellement chaque méthode, lorsque vous avez des centaines de méthodes, cela devient très fastidieux.
J'ai alors décidé d'utiliser PostSharp mais je n'aimais pas l'idée d'inclure une bibliothèque entière juste pour faire quelque chose que je pouvais accomplir avec (beaucoup) de code simple.
J'ai ensuite emprunté la route proxy transparente qui était amusante mais impliquait l'émission dynamique d'IL au moment de l'exécution et ne serait pas quelque chose que je voudrais faire dans un environnement de production.
J'ai récemment décidé d'utiliser des modèles T4 pour implémenter automatiquement le modèle de décorateur au moment de la conception, il s'avère que les modèles T4 sont en fait assez difficiles à travailler et j'avais besoin que cela soit fait rapidement, j'ai donc créé le code ci-dessous. C'est rapide et sale (et il ne prend pas en charge les propriétés) mais j'espère que quelqu'un le trouvera utile.
Voici le code:
var linesToUse = code.Split(Environment.NewLine.ToCharArray()).Where(l => !string.IsNullOrWhiteSpace(l));
string classLine = linesToUse.First();
// Remove the first line this is just the class declaration, also remove its closing brace
linesToUse = linesToUse.Skip(1).Take(linesToUse.Count() - 2);
code = string.Join(Environment.NewLine, linesToUse).Trim()
.TrimStart("{".ToCharArray()); // Depending on the formatting this may be left over from removing the class
code = Regex.Replace(
code,
@"public\s+?(?'Type'[\w<>]+?)\s(?'Name'\w+?)\s*\((?'Args'[^\)]*?)\)\s*?\{\s*?(throw new NotImplementedException\(\);)",
new MatchEvaluator(
match =>
{
string start = string.Format(
"public {0} {1}({2})\r\n{{",
match.Groups["Type"].Value,
match.Groups["Name"].Value,
match.Groups["Args"].Value);
var args =
match.Groups["Args"].Value.Split(",".ToCharArray())
.Select(s => s.Trim().Split(" ".ToCharArray()))
.ToDictionary(s => s.Last(), s => s.First());
string call = "_decorated." + match.Groups["Name"].Value + "(" + string.Join(",", args.Keys) + ");";
if (match.Groups["Type"].Value != "void")
{
call = "return " + call;
}
string argsStr = args.Keys.Any(s => s.Length > 0) ? ("," + string.Join(",", args.Keys)) : string.Empty;
string loggedCall = string.Format(
"using (BuildLogger(\"{0}\"{1})){{\r\n{2}\r\n}}",
match.Groups["Name"].Value,
argsStr,
call);
return start + "\r\n" + loggedCall;
}));
code = classLine.Trim().TrimEnd("{".ToCharArray()) + "\n{\n" + code + "\n}\n";
Voici un exemple:
public interface ITestAdapter : IDisposable
{
string TestMethod1();
IEnumerable<string> TestMethod2(int a);
void TestMethod3(List<string[]> a, Object b);
}
Créez ensuite une classe appelée LoggingTestAdapter qui implémente ITestAdapter, obtenez Visual Studio pour implémenter automatiquement toutes les méthodes, puis exécutez-le via le code ci-dessus. Vous devriez alors avoir quelque chose comme ceci:
public class LoggingTestAdapter : ITestAdapter
{
public void Dispose()
{
using (BuildLogger("Dispose"))
{
_decorated.Dispose();
}
}
public string TestMethod1()
{
using (BuildLogger("TestMethod1"))
{
return _decorated.TestMethod1();
}
}
public IEnumerable<string> TestMethod2(int a)
{
using (BuildLogger("TestMethod2", a))
{
return _decorated.TestMethod2(a);
}
}
public void TestMethod3(List<string[]> a, object b)
{
using (BuildLogger("TestMethod3", a, b))
{
_decorated.TestMethod3(a, b);
}
}
}
C'est tout avec le code de support:
public class DebugLogger : ILogger
{
private Stopwatch _stopwatch;
public DebugLogger()
{
_stopwatch = new Stopwatch();
_stopwatch.Start();
}
public void Dispose()
{
_stopwatch.Stop();
string argsStr = string.Empty;
if (Args.FirstOrDefault() != null)
{
argsStr = string.Join(",",Args.Select(a => (a ?? (object)"null").ToString()));
}
System.Diagnostics.Debug.WriteLine(string.Format("{0}({1}) @ {2}ms", Name, argsStr, _stopwatch.ElapsedMilliseconds));
}
public string Name { get; set; }
public object[] Args { get; set; }
}
public interface ILogger : IDisposable
{
string Name { get; set; }
object[] Args { get; set; }
}
public class LoggingTestAdapter<TLogger> : ITestAdapter where TLogger : ILogger,new()
{
private readonly ITestAdapter _decorated;
public LoggingTestAdapter(ITestAdapter toDecorate)
{
_decorated = toDecorate;
}
private ILogger BuildLogger(string name, params object[] args)
{
return new TLogger { Name = name, Args = args };
}
public void Dispose()
{
_decorated.Dispose();
}
public string TestMethod1()
{
using (BuildLogger("TestMethod1"))
{
return _decorated.TestMethod1();
}
}
public IEnumerable<string> TestMethod2(int a)
{
using (BuildLogger("TestMethod2", a))
{
return _decorated.TestMethod2(a);
}
}
public void TestMethod3(List<string[]> a, object b)
{
using (BuildLogger("TestMethod3", a, b))
{
_decorated.TestMethod3(a, b);
}
}
}