35

Ich konfiguriere Automapper im Bootstrapper und ich rufe die Bootstrap() in Application_Start(), und mir wurde gesagt, dass dies falsch ist, weil ich meine Bootstrapper Klasse jedes Mal ändern muss, wenn ich ein neues Mapping hinzufügen muss, also verletze ich das Open-Closed-Prinzip.Das Konfigurieren von Automapper in Bootstrapper verletzt das Open-Closed-Prinzip?

Wie denkst du, verstoße ich wirklich gegen dieses Prinzip?

public static class Bootstrapper 
{ 
    public static void BootStrap() 
    { 
     ModelBinders.Binders.DefaultBinder = new MyModelBinder(); 
     InputBuilder.BootStrap(); 
     ConfigureAutoMapper(); 
    } 

    public static void ConfigureAutoMapper() 
    { 
     Mapper.CreateMap<User, UserDisplay>() 
      .ForMember(o => o.UserRolesDescription, 
         opt => opt.ResolveUsing<RoleValueResolver>()); 
     Mapper.CreateMap<Organisation, OrganisationDisplay>(); 
     Mapper.CreateMap<Organisation, OrganisationOpenDisplay>(); 
     Mapper.CreateMap<OrganisationAddress, OrganisationAddressDisplay>(); 
    }  
} 

Antwort

39

Ich würde argumentieren, dass Sie zwei Prinzipien verletzen: das Prinzip der einheitlichen Verantwortung (Single Responsibility Principle, SRP) und das offene/geschlossene Prinzip (OCP).

Sie verstoßen gegen die SRP, da die Bootstrapping-Klasse mehr als einen Grund hat, geändert zu werden: wenn Sie die Modellbindung oder die Auto-Mapper-Konfiguration ändern.

Sie würden das OCP verletzen, wenn Sie zusätzlichen Bootstrapping-Code zum Konfigurieren einer anderen Unterkomponente des Systems hinzufügen würden.

Wie ich das normalerweise handhabe ist, dass ich die folgende Schnittstelle definiere.

public interface IGlobalConfiguration 
{ 
    void Configure(); 
} 

Für jede Komponente im System, die Bootstrapping benötigt, würde ich eine Klasse erstellen, die diese Schnittstelle implementiert.

public class AutoMapperGlobalConfiguration : IGlobalConfiguration 
{ 
    private readonly IConfiguration configuration; 

    public AutoMapperGlobalConfiguration(IConfiguration configuration) 
    { 
     this.configuration = configuration; 
    } 

    public void Configure() 
    { 
     // Add AutoMapper configuration here. 
    } 
} 

public class ModelBindersGlobalConfiguration : IGlobalConfiguration 
{ 
    private readonly ModelBinderDictionary binders; 

    public ModelBindersGlobalConfiguration(ModelBinderDictionary binders) 
    { 
     this.binders = binders; 
    } 

    public void Configure() 
    { 
     // Add model binding configuration here. 
    } 
} 

Ich benutze Ninject, um die Abhängigkeiten zu injizieren.IConfiguration ist die zugrunde liegende Implementierung der statischen AutoMapper Klasse und ModelBinderDictionary ist das ModelBinders.Binder Objekt. Ich würde dann eine NinjectModule definieren, die die angegebene Assembly für jede Klasse scannen würde, die die Schnittstelle IGlobalConfiguration implementiert, und diese Klassen zu einem Verbund hinzufügen.

public class GlobalConfigurationModule : NinjectModule 
{ 
    private readonly Assembly assembly; 

    public GlobalConfigurationModule() 
     : this(Assembly.GetExecutingAssembly()) { } 

    public GlobalConfigurationModule(Assembly assembly) 
    { 
     this.assembly = assembly; 
    } 

    public override void Load() 
    { 
     GlobalConfigurationComposite composite = 
      new GlobalConfigurationComposite(); 

     IEnumerable<Type> types = 
      assembly.GetExportedTypes().GetTypeOf<IGlobalConfiguration>() 
       .SkipAnyTypeOf<IComposite<IGlobalConfiguration>>(); 

     foreach (var type in types) 
     { 
      IGlobalConfiguration configuration = 
       (IGlobalConfiguration)Kernel.Get(type); 
      composite.Add(configuration); 
     } 

     Bind<IGlobalConfiguration>().ToConstant(composite); 
    } 
} 

Ich würde dann den folgenden Code der Datei Global.asax hinzufügen.

public class MvcApplication : HttpApplication 
{ 
    public void Application_Start() 
    { 
     IKernel kernel = new StandardKernel(
      new AutoMapperModule(), 
      new MvcModule(), 
      new GlobalConfigurationModule() 
     ); 

     Kernel.Get<IGlobalConfiguration>().Configure(); 
    } 
} 

Jetzt ist mein Bootstrapping-Code an SRP und OCP gebunden. Ich kann einfach zusätzlichen Bootstrapping-Code hinzufügen, indem ich eine Klasse erstelle, die die IGlobalConfiguration-Schnittstelle implementiert, und meine globalen Konfigurationsklassen haben nur einen Grund, sich zu ändern.

+3

weg und Sie müssen immer noch die Configure ändern Methode in AutoMapperGlobalConfiguration jedes Mal, wenn Sie ein neues Mapping hinzufügen müssen – Omu

+10

Aber das würde OCP nicht verletzen. OCP ist nicht schreiben einmal nie wieder berühren. OCP gibt an, dass der Benutzer des Bootstrapping-Codes, das GlobalConfigurationModule (GCM), sich auf die Abstraktion und nicht auf die Implementierung der Conrete verlassen sollte. Wenn ich Bootstrapping für log4net hinzufügen würde, würde ich eine Klasse Log4NetGlobalConfiguration erstellen, die IGlobalConfiguration implementieren würde. Ich würde jedoch keinen anderen Teil meines Codes und definitiv nicht das GCM ändern müssen, da es keine komplizierten Kenntnisse über die konkrete Implementierung der IGlobalConfiguration-Schnittstelle besitzt. – mrydengren

+0

Ich bin in Zweifel. Sobald Mapper.CreateMap <>() ausgeführt wurde, existieren die Karten bis zum Beenden der Anwendung? – JPCF

3

Um es vollständig geschlossen ist, eine statische Initialisierer pro Mapping Registrierung haben könnte, aber das zu viel des Guten wäre.

Einige Dinge sind tatsächlich nützlich, um zu einem gewissen Grad aus der Sicht der Lage sein, Reverse Engineering obwohl.

In NInject gibt es die Idee, ein Module pro Projekt oder Subsystem (Reihe von Projekten) zu haben, was ein vernünftiger Kompromiss scheint.

2

Wenn überhaupt, ist es das Prinzip der einheitlichen Verantwortung, das Sie verletzen, da die Klasse mehr als einen Grund hat, sich zu ändern.

Ich persönlich hätte eine ConfigureAutoMapper Klasse, mit der alle meine Konfiguration für AutoMapper gemacht wurde. Aber es könnte argumentiert werden, dass es an der persönlichen Entscheidung liegt.

+0

ja, und immer noch, nachdem ich es in eine andere Klasse verschoben habe, komme ich nicht von dem Open Closed Principle – Omu

2

Omu, ich ringe mit ähnlichen Fragen, wenn es um Bootstrapping eines IoC-Containers in der Startroutine meiner App geht. Für IoC weist die Anleitung, die ich erhalten habe, auf den Vorteil auf, dass Sie Ihre Konfiguration zentralisieren, anstatt sie beim Hinzufügen von Änderungen über Ihre App zu streuen. Für die Konfiguration von AutoMapper ist der Vorteil der Zentralisierung viel weniger wichtig. Wenn Sie Ihren AutoMapper-Container in Ihren IoC-Container oder Service Locator bringen können, stimme ich dem Vorschlag von Ruben Bartelink zu, die Mappings einmal pro Baugruppe oder in statischen Konstruktoren oder etwas dezentralisiert zu konfigurieren.

Grundsätzlich sehe ich es als eine Frage der Entscheidung, ob Sie das Bootstrapping zentralisieren oder dezentralisieren wollen. Wenn Sie sich in Ihrer Startroutine über das Open/Closed-Prinzip Sorgen machen, gehen Sie zur Dezentralisierung über. Aber Ihre Einhaltung von OCP kann im Austausch für den Wert all Ihrer Bootstrapping an einem Ort erfolgen. Eine andere Option wäre, dass der Bootstrapper bestimmte Assemblys für Registrys scannt, vorausgesetzt, AutoMapper hat ein solches Konzept.

3

Ich weiß, dass dies ein altes ist, aber Sie könnten interessiert sein zu wissen, dass ich eine Open-Source-Bibliothek namens erstellt habe, die genau mit diesem Problem befasst. Vielleicht möchten Sie es überprüfen. Um das OC-Prinzip zu umgehen, müssen Sie Ihre Mapper in separaten Klassen definieren, die IMapCreater implementieren. Boostrapper findet diese Klassen mit Reflektion und initialisiert alle Mapper beim Start