Gesucht: Gefunden! Ein wiederverwendbarer Extensions-Manager für .net

Die Zeiten der monströßen, schwer zu wartendenden und noch schwerer zu überarbeitenden Software-Monolithen ist glücklicherweise vorbei. Die meisten der heutzutage neu entwickelten Anwendungen werden häufig nicht nur unter den Gesichtspunkten der Übersichtlichkeit (des Quellcodes) und der logischen Modularisierung, sondern auch unter dem Aspekt der (späteren) Erweiterbarkeit bzw. der einfachen Anpassbarkeit (z.B. für underschiedliche Anwendergruppen) gestaltet. Unter dem Schlagwort „loose gekoppelt“ wird dabei häufig das beschrieben, was die meisten Anwender schlicht als „Plugin“ kennen.

Es gibt einige, teils sehr mächtige, Erweiterungen für das .net-Framework, die genau für solche Anwendungsbereiche entwickelt und optimiert wurden. Eines der Bekanntesten ist wohl Prism.
Häufig verhält es sich mit solchen Bibliotheken aber wie mit den sprichwörtlichen Kanonen, vor denen sich Spatzen in Acht nehmen sollten: Sie vergrößern nicht nur die Zahl der Anwendungsdateien oder die Gesamtgröße des Builds, sondern setzen teilweise komplizierte Gestaltungsmuster und Quellcode-Konventionen voraus.

Nicht selten, genügt das von Microsoft standardmäßig zur Verfügung gestellte Managed Extensibility Framework (MEF) völlig.
Es gibt bereits einige recht gute Tutorials zu diesem Thema, so dass ich an dieser Stelle nicht zu sehr bei den Grundlagen beginnen möchtem dennoch sei erwähnt: Grundsätzlich besteht das Laden der Extensions aus dem Erstellen von Katalogen (basierend auf einzelnen Assembly-Dateien oder ganzen Verzeichnissen), die dann vom MEF zu einer Liste an Implementierungen kompiliert werden.
Im Quellcode kann dies dann folgendermaßen aussehen:

<ImportMany(ICalculate)>
Private _allExtensions As ObservableCollection(Of Lazy(Of ICalculate, ICalculateMetadata)) = New ObservableCollection(Of Lazy(Of ICalculate, ICalculateMetadata))

Public Sub Update(ByVal paths As String())

        'assemble the catalog
        Dim catalog = New AggregateCatalog()

        For Each path As String In paths

            Select Case True

                Case File.Exists(path)
                    catalog.Catalogs.Add(New AssemblyCatalog(path))

                Case Directory.Exists(path)
                    catalog.Catalogs.Add(New DirectoryCatalog(path))

            End Select

        Next

        'compose the collection
        Dim container As CompositionContainer = New CompositionContainer(catalog)
        container.ComposeParts(Me)

    End Sub
  • ICalculate ist dabei das in einer Extension zu implementierende Interface
  • ICalculateMetadata die zur Verfügung gestellten Metadaten
  • Die Liste verfügbarer Extensions wird mit der Variable _allExtensions bereitgestellt

Stefan Henneken hat einen Artikel ein gutes Konzept beschrieben, mit dem man – basierend auf einer Basisklasse – Metadaten verwenden kann um Extensions zusätzlich zu beschreiben.

Um nun eine wiederverwendbaren (Manager-)Klasse zu erstellen, betten wir nun die oben genannte Routine in eine Klasse mit generischen Typen ein:

Public Class BasicManager(Of TInterface, TMetadata)

<ImportMany()>
Private _allExtensions As ObservableCollection(Of Lazy(Of TInterface, TMetadata)) = New ObservableCollection(Of Lazy(Of TInterface, TMetadata))

Public ReadOnly Property Extensions As Lazy(Of TInterface, TMetadata)()
        Get
            Return _validExtensions.ToArray
        End Get
End Property

Public Sub Update(ByVal paths As String())
...
End Sub

End Class

Zu beachten ist, dass neben den Datentypen in _allExtensions, auch das ImportMany-Attribute angepasst werden muss. Da MEF keine generischen Datentypen unterstützt, wird der Datentyp einfach leer gelassen. Und genau damit wird es nun problematisch:
Normalerweise wird genau über diesen Datentyp eine Filterung der gefundenen Interface-Implementierungen durchgeführt. So lange man nur ein Interface in seiner Anwendung hat, ist dies recht unproblematisch, Hat man jedoch mehrere Interfaces verfügbar, würde _allExtensions alle Extensions für alle Interfaces enthalten. Um dies zu korrigieren, wird die Update-Routine, mit dem Aufruf einer ValidateExtensions-Methode ergänzt:

Private Sub ValidateExtensions()

        _validExtensions = New ObservableCollection(Of Lazy(Of TInterface, TMetadata))

        For Each extension In _allExtensions

            Try

                If ImplementsInterface(extension) Then

                    _validExtensions.Add(extension)

                End If

            Catch ex As Exception

            End Try

        Next

    End Sub

    Private Function ImplementsInterface(ByRef extension) As Boolean

        For Each iFace In extension.Value.GetType.GetInterfaces

            If iFace.ToString = GetType(TInterface).ToString Then

                Return True

            End If

        Next

        Return False

    End Function

Alle gefundenen (und in _allExtensions gespeicherten) Extensions werden darin durchaufen und nur die in _validExtensions gespeichert, die auch tatsächlich das (gewünschte) Interface implementieren. Der Rückgabewert der öffentlichen Eigenschaft Extensions wird dann natürlich auch auf _validExtensions gesetzt.

Natürlich handelt es sich hierbei um ein stark vereinfachtes Beispiel und häufig werden weitere Zusatzfunktionen für das Laden von Plugins benötigt. Dennoch, ist es schon mit dieser übersichtlichen Menge an Code möglich eine stabile und Wartungsfreundliche Plugin-Infrastruktur aufzubauen – die zudem noch wiederverwendbar ist. Wer selbst etwas experimentieren möchte, kann dies auch gern mit dem auf Github verfügbaren Beispieldateien tun.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.