Przechwytywanie a wtryskiwanie: decyzja dotycząca architektury ramowej

28

Istnieją takie ramy, które pomagam zaprojektować. Istnieje kilka typowych zadań, które należy wykonać przy użyciu niektórych typowych komponentów: w szczególności rejestrowanie, buforowanie i wywoływanie zdarzeń.

Nie jestem pewien, czy lepiej jest użyć wstrzykiwania zależności i wprowadzić wszystkie te składniki do każdej usługi (na przykład właściwości), czy też powinienem umieścić jakieś metadane nad każdą metodą moich usług i użyć przechwytywania do wykonania tych typowych zadań ?

Oto przykład obu:

Iniekcja:

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);
    }
}

a oto inna wersja:

Przechwycenie:

public class MyService
{
    [Log("My message")]
    [PublishEvent(typeof(EventType))]
    public void DoSomething()
    {

    }
}

Oto moje pytania:

  1. Które rozwiązanie jest najlepsze w przypadku skomplikowanych ram?
  2. Jeśli przechwytywanie wygra, jakie są moje opcje interakcji z wewnętrznymi wartościami metody (na przykład w przypadku usługi pamięci podręcznej?)? Czy mogę zastosować inne sposoby niż atrybuty do wdrożenia tego zachowania?
  3. A może mogą istnieć inne rozwiązania problemu?
Beatlesi 1692
źródło
2
Nie mam zdania na temat 1 i 2, ale odnośnie 3: rozważ przyjrzenie się AoP ( programowanie zorientowane na aspekty ), a konkretnie Spring.NET .
Wyjaśnij: szukasz porównania między wstrzykiwaniem zależności a programowaniem zorientowanym na aspekt, prawda?
M.Babcock,
@ M.Babcock Nie widziałem tego w ten sposób, ale to prawda

Odpowiedzi:

38

Zagadnienia przekrojowe, takie jak rejestrowanie, buforowanie itp., Nie są zależnościami, dlatego nie należy ich wprowadzać do usług. Jednak podczas gdy większość ludzi wydaje się sięgać po pełną ramkę przeplatania AOP, istnieje ładny wzór dla tego: Dekorator .

W powyższym przykładzie pozwól MyService zaimplementować interfejs IMyService:

public interface IMyService
{
    void DoSomething();
}

public class MyService : IMyService
{
    public void DoSomething()
    {
        // Implementation goes here...
    }
}

Dzięki temu klasa MyService jest całkowicie wolna od obaw przekrojowych, a zatem jest zgodna z zasadą pojedynczej odpowiedzialności (SRP).

Aby zastosować rejestrowanie, możesz dodać dekorator rejestrowania:

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");
    }
}

Możesz zaimplementować buforowanie, pomiary, zdarzenia itp. W ten sam sposób. Każdy Dekorator robi dokładnie jedną rzecz, dlatego też postępuje zgodnie z SRP i możesz komponować je w dowolnie złożone sposoby. Na przykład

var service = new MyLogger(
    new LoggingService(),
    new CachingService(
        new Cache(),
        new MyService());
Mark Seemann
źródło
5
Wzorzec dekoratora to świetny sposób na oddzielenie tych obaw, ale jeśli masz dużo usług, właśnie wtedy użyłbym narzędzia AOP, takiego jak PostSharp lub Castle.DynamicProxy, w przeciwnym razie dla każdego interfejsu klasy usług muszę kodować klasę ORAZ dekorator loggera, a każdy z tych dekoratorów może potencjalnie być bardzo podobny do kodu podstawowego (tzn. Masz ulepszoną modularyzację / enkapsulację, ale wciąż dużo się powtarzasz).
Matthew Groves
4
Zgoda. W zeszłym roku wygłosiłem przemówienie, które opisuje, jak przejść z Dekoratorów do AOP: channel9.msdn.com/Events/GOTO/GOTO-2011-Copenhagen/...
Mark Seemann
Napisałem prostą implementację opartą na tym programiegood.net/2015/09/08/DecoratorSpike.aspx
Dave Mateer
Jak możemy wstrzykiwać usługi i dekoratorów za pomocą wstrzykiwania zależności?
TIKSN,
@ TIKSN Krótka odpowiedź brzmi: jak pokazano powyżej . Ponieważ jednak pytasz, musisz szukać odpowiedzi na coś innego, ale nie mogę zgadnąć, co to jest. Czy mógłbyś rozwinąć lub zadać nowe pytanie tutaj na stronie?
Mark Seemann
6

Jeśli chodzi o garść usług, myślę, że odpowiedź Marka jest dobra: nie będziesz musiał uczyć się ani wprowadzać żadnych nowych zależności stron trzecich i nadal będziesz przestrzegać dobrych zasad SOLID.

W przypadku dużej liczby usług poleciłbym narzędzie AOP, takie jak PostSharp lub Castle DynamicProxy. PostSharp ma darmową (jak w piwie) wersję, a niedawno wypuścił PostSharp Toolkit do diagnostyki (darmowy jak w piwie ORAZ mowy), który zapewni kilka funkcji logowania po wyjęciu z pudełka.

Matthew Groves
źródło
2

Uważam, że konstrukcja frameworka jest w dużej mierze ortogonalna w stosunku do tego pytania - powinieneś najpierw skupić się na interfejsie frameworka, a być może jako proces mentalny w tle zastanowić się, w jaki sposób ktoś może go faktycznie wykorzystać. Nie chcesz robić czegoś, co uniemożliwia wykorzystanie go w sprytny sposób, ale powinno to być jedynie wkładem w twój projekt frameworka; jeden z wielu.


źródło
1

Wiele razy napotykałem ten problem i myślę, że wpadłem na proste rozwiązanie.

Początkowo poszedłem ze wzorem dekoratora i ręcznie wdrożyłem każdą metodę, gdy masz setki metod, staje się to bardzo nudne.

Potem zdecydowałem się użyć PostSharp, ale nie podobał mi się pomysł włączenia całej biblioteki tylko po to, aby zrobić coś, co można osiągnąć za pomocą (dużo) prostego kodu.

Następnie wybrałem przezroczystą ścieżkę proxy, co było zabawne, ale wymagało dynamicznego emitowania IL w czasie wykonywania i nie byłoby czymś, co chciałbym robić w środowisku produkcyjnym.

Niedawno postanowiłem użyć szablonów T4 do automatycznej implementacji wzoru dekoratora w czasie projektowania, okazuje się, że szablony T4 są naprawdę dość trudne do pracy i potrzebowałem tego szybko, więc utworzyłem poniższy kod. Jest szybki i brudny (i nie obsługuje właściwości), ale mam nadzieję, że ktoś uzna to za przydatne.

Oto kod:

        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";

Oto przykład:

public interface ITestAdapter : IDisposable
{
    string TestMethod1();

    IEnumerable<string> TestMethod2(int a);

    void TestMethod3(List<string[]>  a, Object b);
}

Następnie utwórz klasę o nazwie LoggingTestAdapter, która implementuje ITestAdapter, poproś Visual Studio, aby automatycznie zaimplementowało wszystkie metody, a następnie uruchom go przez powyższy kod. Powinieneś mieć coś takiego:

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);
        }
    }
}

To jest z kodem pomocniczym:

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);
        }
    }
}
JoeS
źródło