2012-03-27 11 views
49

Jak mogę wyciągnąć przedmioty z pojemnika, które są przejściowe? Czy muszę zarejestrować je w kontenerze i wstrzyknąć do konstruktora wymagającej klasy? Wstrzykiwanie wszystkiego do konstruktora nie jest dobre. Również dla jednej klasy nie chcę tworzyć TypedFactory i wstrzykiwać fabrykę do wymagającej klasy.Windsor - ciągnięcie obiektów przejściowych z pojemnika

Inna myśl, która przyszła mi do głowy, była "nowa". Ale ja również wstrzykuję komponent Logger (poprzez właściwość) do wszystkich moich klas. Więc jeśli je wymyślę, będę musiał ręcznie utworzyć instancję Logger w tych klasach. Jak mogę dalej używać kontenera dla WSZYSTKICH moich zajęć?

wtrysk Rejestrator: Większość moich klas mają właściwość Logger określony, chyba że jest tam łańcuch dziedziczenia (w tym przypadku tylko klasa podstawowa ma tę właściwość, a wszystkie zajęcia wynikające użyć tego). Gdy zostaną one utworzone przez kontener Windsor, dostaną do niego moją implementację ILogger.

//Install QueueMonitor as Singleton 
Container.Register(Component.For<QueueMonitor>().LifestyleSingleton()); 
//Install DataProcessor as Trnsient 
Container.Register(Component.For<DataProcessor>().LifestyleTransient()); 

Container.Register(Component.For<Data>().LifestyleScoped()); 

public class QueueMonitor 
{ 
    private dataProcessor; 

    public ILogger Logger { get; set; } 

    public void OnDataReceived(Data data) 
    { 
     //pull the dataProcessor from factory  
     dataProcessor.ProcessData(data); 
    } 
} 

public class DataProcessor 
{ 
    public ILogger Logger { get; set; } 

    public Record[] ProcessData(Data data) 
    { 
     //Data can have multiple Records 
     //Loop through the data and create new set of Records 
     //Is this the correct way to create new records? 
     //How do I use container here and avoid "new" 
     Record record = new Record(/*using the data */); 
     ... 

     //return a list of Records  
    } 
} 


public class Record 
{ 
    public ILogger Logger { get; set; } 

    private _recordNumber; 
    private _recordOwner; 

    public string GetDescription() 
    { 
     Logger.LogDebug("log something"); 
     // return the custom description 
    } 
} 

Pytania:

  1. Jak utworzyć nowy Record obiektu bez użycia "nowe"?

  2. QueueMonitor to Singleton, natomiast Data to "Scoped". Jak mogę wprowadzić Data w metodzie ?

+0

@Steven Dodałem próbkę kodu pokazującą użycie rejestratora. Czy uważasz, że to zły projekt? – user1178376

+1

Czy możesz zilustrować konkretnym kodem, jeszcze lepszym testem, co próbujesz osiągnąć? –

+0

Przepraszamy za kiepskie skonstruowanie mojego pytania. Dodałem więcej kodu, aby wyjaśnić moją sprawę. – user1178376

Odpowiedz

211

z próbek dajesz Trudno być bardzo szczegółowe, ale w ogóle, kiedy wstrzykiwać ILogger instancji do większości usług, należy zadać sobie dwie rzeczy:

  1. mogę się zalogować zbyt dużo?
  2. Czy naruszam zasady SOLID?

1. Do Loguję zbytnio

logujesz się zbyt wiele, gdy masz dużo kodu:

try 
{ 
    // some operations here. 
} 
catch (Exception ex) 
{ 
    this.logger.Log(ex); 
    throw; 
} 

pisania kodu jak to pochodzi od koncernu utraty informacji o błędzie. Powielanie tego rodzaju bloków try-catch w każdym miejscu jednak nie pomaga. Co gorsza, często widzę, jak deweloperzy logują się i kontynuują (usuwają ostatnie oświadczenie throw). To jest naprawdę złe (i pachnie starym VB ON ERROR RESUME NEXT), ponieważ w większości sytuacji po prostu brakuje wystarczającej ilości informacji, aby określić, czy jest ona bezpieczna. Często występuje błąd w kodzie, który spowodował niepowodzenie operacji. Kontynuacja oznacza, że ​​użytkownik często wpada na pomysł, że operacja zakończyła się sukcesem, podczas gdy tak się nie stało. Zadaj sobie pytanie: co jest gorsze, pokazując użytkownikowi ogólny komunikat o błędzie z informacją, że coś poszło nie tak, lub po cichu pomijając błąd i pozwalając użytkownikowi myśleć, że jego żądanie zostało pomyślnie przetworzone? Zastanów się, jak użytkownik się poczuje, jeśli dwa tygodnie później dowie się, że jego zamówienie nie zostało wysłane. Prawdopodobnie stracisz klienta. Lub gorzej, rejestracja pacjenta MRSA po cichu zawiedzie, powodując, że pacjent nie jest poddawany kwarantannie przez pielęgniarkę i powoduje skażenie innych pacjentów, powodując wysokie koszty, a nawet śmierć.

Większość tego rodzaju linii try-catch-log powinna zostać usunięta i powinieneś po prostu pozwolić wyjątkowi upuścić stos wywołań.

Nie należy się logować? Absolutnie powinieneś! Ale jeśli możesz, zdefiniuj jeden blok try-catch na górze aplikacji. W środowisku ASP.NET można wdrożyć zdarzenie Application_Error, zarejestrować numer HttpModule lub zdefiniować niestandardową stronę błędu, która wykonuje rejestrowanie. Dzięki Win Forms rozwiązanie jest inne, ale koncepcja pozostaje taka sama: Zdefiniuj jedną najwyższą wartość catch-all.

Czasami jednak nadal chcesz przechwycić i zarejestrować określony typ wyjątku. System, nad którym pracowałem w przeszłości, niech warstwa biznesu wyrzuci ValidationException s, która zostanie przechwycona przez warstwę prezentacji. Te wyjątki zawierały informacje o sprawdzaniu poprawności do wyświetlenia użytkownikowi. Ponieważ te wyjątki zostałyby przechwycone i przetworzone w warstwie prezentacji, nie pojawiłyby się w górnej części aplikacji i nie trafiłyby do kodu catch-all aplikacji. Nadal chciałem zarejestrować te informacje, aby dowiedzieć się, jak często użytkownik wprowadzał nieprawidłowe informacje i sprawdzać, czy weryfikacje zostały uruchomione z właściwego powodu. Nie było to rejestrowanie błędów; właśnie się loguję. W tym celu napisałem następujący kod:

try 
{ 
    // some operations here. 
} 
catch (ValidationException ex) 
{ 
    this.logger.Log(ex); 
    throw; 
} 

Wygląda znajomo? Tak, wygląda dokładnie tak samo jak poprzedni fragment kodu, z tą różnicą, że złapałem tylko ValidationException s. Jednak była inna różnica, której nie można zobaczyć po prostu patrząc na fragment. W aplikacji było tylko jedno miejsce, które zawierało ten kod! To był dekorator, który doprowadził mnie do następnego pytania, które powinieneś zadać sobie:

2. Czy naruszam zasady SOLID?

Rzeczy takie jak rejestrowanie, audyt i zabezpieczenia są nazywane cross-cutting concerns (lub aspekty). Są one nazywane przekrojowymi, ponieważ mogą przecinać wiele części aplikacji i często muszą być stosowane w wielu klasach w systemie. Jednakże, gdy zauważysz, że piszesz kod do wykorzystania w wielu klasach w systemie, najprawdopodobniej naruszasz zasady SOLID. Weźmy na przykład poniższy przykład:

public void MoveCustomer(int customerId, Address newAddress) 
{ 
    var watch = Stopwatch.StartNew(); 

    // Real operation 

    this.logger.Log("MoveCustomer executed in " + 
     watch.ElapsedMiliseconds + " ms."); 
} 

Tutaj mierzymy czas potrzebny na wykonanie operacji MoveCustomer i logujemy tych informacji. Jest bardzo prawdopodobne, że inne operacje w systemie wymagają tego samego przekrojowego problemu. Zaczniesz dodawać kod taki jak ten dla twoich ShipOrder, CancelOrder, CancelShipping, itp. Metody kończą się tym, że prowadzi to do wielu duplikacji kodu i ostatecznie do koszmaru utrzymania.

Problem stanowi naruszenie zasad SOLID. Zasady SOLID to zestaw zasad projektowania zorientowanych obiektowo, które pomagają w definiowaniu elastycznego i łatwego w utrzymaniu oprogramowania. Przykład MoveCustomer narusza co najmniej dwie z poniższych zasad:

  1. The Single Responsibility Principle. Klasa trzymająca metodę MoveCustomer nie tylko przenosi klienta, ale także mierzy czas potrzebny na wykonanie operacji. Innymi słowy ma wiele obowiązków. Powinieneś wyodrębnić pomiar do własnej klasy.
  2. The Open-Closed principle (OCP). Zachowanie systemu powinno być możliwe do zmiany bez zmiany istniejącej linii kodu. Jeśli potrzebujesz również obsługi wyjątków (trzecia odpowiedzialność), musisz (ponownie) zmienić metodę MoveCustomer, co stanowi naruszenie OCP.

Poza naruszeniem zasad SOLID zdecydowanie złamaliśmy zasadę DRY, która zasadniczo mówi, że powielanie kodu jest złe, mkay.

Rozwiązaniem tego problemu jest, aby wyodrębnić rejestrowanie do własnej klasy i pozwolić klasa zawinąć oryginalnego Klasa:

// The real thing 
public class MoveCustomerCommand 
{ 
    public virtual void MoveCustomer(int customerId, Address newAddress) 
    { 
     // Real operation 
    } 
} 

// The decorator 
public class MeasuringMoveCustomerCommandDecorator : MoveCustomerCommand 
{ 
    private readonly MoveCustomerCommand decorated; 
    private readonly ILogger logger; 

    public MeasuringMoveCustomerCommandDecorator(
     MoveCustomerCommand decorated, ILogger logger) 
    { 
     this.decorated = decorated; 
     this.logger = logger; 
    } 

    public override void MoveCustomer(int customerId, Address newAddress) 
    { 
     var watch = Stopwatch.StartNew(); 

     this.decorated.MoveCustomer(customerId, newAddress); 

     this.logger.Log("MoveCustomer executed in " + 
      watch.ElapsedMiliseconds + " ms."); 
    } 
} 

owijając dekorator wokół prawdziwego przykład, można teraz dodać ten pomiar zachowanie do klasy, bez jakiejkolwiek innej części systemu, aby zmienić:

MoveCustomerCommand command = 
    new MeasuringMoveCustomerCommandDecorator(
     new MoveCustomerCommand(), 
     new DatabaseLogger()); 

Poprzedni przykład nie rozwiąże jednak tylko część problemu (tylko część stała). Podczas pisania kodu, jak pokazano powyżej, będziesz musiał zdefiniować dekoratory dla wszystkich operacji w systemie, a skończysz z dekoratorami takimi jak MeasuringShipOrderCommandDecorator, MeasuringCancelOrderCommandDecorator i MeasuringCancelShippingCommandDecorator. To prowadzi ponownie do wielu duplikatów kodu (naruszenie zasady DRY), i nadal potrzebujemy pisać kod dla każdej operacji w systemie. To, czego tu brakuje, to powszechna abstrakcja dotycząca przypadków użycia w systemie. Brakuje interfejsu ICommandHandler<TCommand>.

Zdefiniujmy ten interfejs:

public interface ICommandHandler<TCommand> 
{ 
    void Execute(TCommand command); 
} 

I niech przechowywania argumentów metoda metody MoveCustomer do własnego (Parameter Object) klasy zwanej MoveCustomerCommand:

public class MoveCustomerCommand 
{ 
    public int CustomerId { get; set; } 
    public Address NewAddress { get; set; } 
} 

I postawmy zachowanie tego Metoda MoveCustomer w klasie implementującej ICommandHandler<MoveCustomerCommand>:

public class MoveCustomerCommandHandler : ICommandHandler<MoveCustomerCommand> 
{ 
    public void Execute(MoveCustomerCommand command) 
    { 
     int customerId = command.CustomerId; 
     var newAddress = command.NewAddress; 
     // Real operation 
    } 
} 

Może to wyglądać dziwnie, ale ponieważ teraz mamy ogólny abstrakcji dla przypadków użycia, możemy przepisać nasz dekorator następująco:

public class MeasuringCommandHandlerDecorator<TCommand> 
    : ICommandHandler<TCommand> 
{ 
    private ICommandHandler<TCommand> decorated; 
    private ILogger logger; 

    public MeasuringCommandHandlerDecorator(
     ICommandHandler<TCommand> decorated, ILogger logger) 
    { 
     this.decorated = decorated; 
     this.logger = logger; 
    } 

    public void Execute(TCommand command) 
    { 
     var watch = Stopwatch.StartNew(); 

     this.decorated.Execute(command); 

     this.logger.Log(typeof(TCommand).Name + " executed in " + 
      watch.ElapsedMiliseconds + " ms."); 
    } 
} 

Ten nowy MeasuringCommandHandlerDecorator<T> wygląda podobnie jak MeasuringMoveCustomerCommandDecorator, ale klasa ta może być ponownie wykorzystane do wszystkich wozy dowodzenia w systemie:

ICommandHandler<MoveCustomerCommand> handler1 = 
    new MeasuringCommandHandlerDecorator<MoveCustomerCommand>(
     new MoveCustomerCommandHandler(), 
     new DatabaseLogger()); 

ICommandHandler<ShipOrderCommand> handler2 = 
    new MeasuringCommandHandlerDecorator<ShipOrderCommand>(
     new ShipOrderCommandHandler(), 
     new DatabaseLogger()); 

ten sposób będzie to o wiele łatwiejsze, aby dodać obawy przekrojowych w systemie. Łatwo jest stworzyć wygodną metodę w twoim Composition Root, która może owijać wszystkie utworzone procedury obsługi poleceń za pomocą odpowiednich procedur obsługi poleceń w systemie. Na przykład:

ICommandHandler<MoveCustomerCommand> handler1 = 
    Decorate(new MoveCustomerCommandHandler()); 

ICommandHandler<ShipOrderCommand> handler2 = 
    Decorate(new ShipOrderCommandHandler()); 

private static ICommandHandler<T> Decorate<T>(ICommandHandler<T> decoratee) 
{ 
    return 
     new MeasuringCommandHandlerDecorator<T>(
      new DatabaseLogger(), 
       new ValidationCommandHandlerDecorator<T>(
        new ValidationProvider(), 
        new AuthorizationCommandHandlerDecorator<T>(
         new AuthorizationChecker(
          new AspNetUserProvider()), 
         new TransactionCommandHandlerDecorator<T>(
          decoratee)))); 
} 

Jeśli twoja aplikacja zacznie się jednak rozrastać, bolesne może być załadowanie tego wszystkiego bez pojemnika. Zwłaszcza, gdy dekoratorzy mają ogólne ograniczenia typu.

Większość współczesnych pojemników DI dla .NET ma obecnie dość przyzwoitą obsługę dekoratorów, a zwłaszcza Autofac (example) i Simple Injector (example) ułatwiają rejestrację otwartych dekoratorów generycznych.Simple Injector pozwala nawet na warunkowe stosowanie dekoratorów w oparciu o określone predykaty lub złożone ograniczenia rodzajowe, pozwala na dekorowanie klasy na injected as a factory i pozwala na wstrzykiwanie contextual context do dekoratorów, z których wszystkie mogą być naprawdę przydatne od czasu do czasu.

Z kolei Unity and Castle mają urządzenia do przechwytywania (jak robi to Autofac). Intercepcja ma wiele wspólnego z dekoracją, ale wykorzystuje dynamiczne generowanie proxy pod osłonami. To może być bardziej elastyczne niż praca z generycznymi dekoratorami, ale zapłacisz cenę, jeśli chodzi o łatwość konserwacji, ponieważ często tracisz bezpieczeństwo typu, a przechwytywacze zawsze zmuszają cię do uzależnienia się od biblioteki przechwytywania, podczas gdy dekoratorzy są bezpieczni i można zapisać bez pobierania zależności od biblioteki zewnętrznej.

Przeczytaj ten artykuł, jeśli chcesz dowiedzieć się więcej o tym sposobie zaprojektowania aplikacji: Meanwhile... on the command side of my architecture.

Mam nadzieję, że to pomoże.

+7

Świetna odpowiedź Steven. Zgadzam się ze wszystkim, co tu powiedziałeś, nie zgadzam się również z wyjątkami, tylko po to, aby się zalogować i ponownie przeanalizować, ponieważ uważam, że powinien to być jeden centralny punkt w domenie aplikacji, jednak uważam, że są chwile, kiedy chcesz wychwycić określony wyjątek, taki jak wyjątek SQLException, aby ponowić działanie. – OutOFTouch

+1

SimpleInjector wygląda świetnie i jest na szczycie mojej listy do wykorzystania, utrzymuj dobrą pracę. Oto przykład dekoratora z Windsor http://mikehadlow.blogspot.com/2010/01/10-advanced-windsor-tricks-4-how-to.html dla każdego, kto jest zainteresowany. – OutOFTouch

+3

@OutOFTouch: Wykonanie ponownej operacji jest bardzo dobrym rozwiązaniem dla AOP, ale nie chcesz owijać takich rzeczy przy każdej operacji db, ale w granicach transakcji. W rzeczywistości pokazuje to [mój blog] (http://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=91) (spójrz na 'DeadlockRetryCommandHandlerDecorator') . – Steven

Powiązane problemy