Wer die Wahl hat, hat die Qual: Umgang mit Abhängigkeiten in der Softwareentwicklung

Make-or-buy? Entscheidungen für die Verwendung von Frameworks oder Third Party Software führen oft zu Diskussionen über Umfang, Qualität und damit sinnvollen Einsatz von Zeit und Geld. Wir zeigen, warum es nicht immer sinnvoll ist, auf fertige Lösungen zu setzen und wann sich eine Eigenentwicklung lohnt.

Wie wägt man nun ab?

Bei der Entscheidung für eine Eigenentwicklung oder die Benutzung einer fertigen Software liegen gewöhnlich wenig Erfahrungswerte zu Grunde. Zudem weiß man unter Umständen erst spät, ob es funktionale Unterschiede gibt, die sich auf den ersten Blick nicht erkennen lassen. Sie treten erst später in der Implementierung auf – ein Problem, das man nicht ganz beseitigen kann.

Will man also eine gute Entscheidung treffen, braucht es Faktoren, an denen man mit wenig Wissen ableiten kann, ob make oder buy die bessere Wahl ist. Folgende Kniffe können die Entscheidung vereinfachen.

Umfang der Lösung

Das bietet ja soviel, da sind wir sicher gut aufgestellt für die Zukunft.

So oder so ähnlich könnte die Argumentation lauten, wenn man von der Fremdsoftware nur einen kleinen Teil benötigt. Was ist aber, wenn nur der kleine Teil benötigt wird? Ist es dann nicht einfacher, die Funktionalität selbst zu schreiben? Brauche ich wirklich eine Komponente wie leftpad?

Es ist sinnvoll, den Umfang der benötigten Lösung einmal zu betrachten. Manchmal sind reale Argumente für die komplexere Lösung von der Stange gar nicht vorhanden und man entscheidet sich für trotzdem für ein Potential, das man gar nicht ausschöpfen wird.

Zukunftssicher

Externe Komponenten werden unabhängig in Stand gehalten und aktualisiert

Es ist sicher richtig, dass Komponenten anderer Hersteller oft über Aktualisierungen und Sicherheitsupdates verfügen, die die Funktionalität erhöhen und Stabilität verbessern. Gerade die erlangte Maturität einer fremden Komponente gibt Aufschluss über deren Zukunftssicherheit. Faktoren beinhalten:

  • Engagement, Beliebtheit (bei Open Source Software)

  • Software hat ein stabiles Geschäftsmodell, mit dem der Hersteller Geld verdient

  • Durchsetzung im Markt

Diese Faktoren wirken einfach auszuwerten, geben zum Beispiel die Sterne auf GitHub eine Indikation der Beliebtheit der Software. Die Durchsetzung im Markt oder die Frage nach dem Geschäftsmodell bleibt oft nur qualitativ auswertbar.

Der Integrationsaufwand bleibt

Ungeachtet der vorherigen Faktoren bleibt immer ein gewisser Integrationsaufwand. Bei der Eigenentwicklung ist dieser inklusive. Bei der Fremdsoftware besteht die Integration oft aus Verständnisaufbau durch Dokumentation und Trial and Error. Dieser Aufbau ist bei umfangreicheren Lösungen tendenziell größer. Bei kleineren Lösungen kann es deswegen sinnvoll sein, diese selbst zukunftssicher zu entwickeln, denn bei der Eigenentwicklung kennt das eigene Entwicklungsteam die Lösung sehr genau und kann sie stabil halten.

Nichts ist umsonst

Kostenlos ist beides nicht, denn die Aktualität kostet in jedem Fall immer Integrationsaufwand, auch wenn durch automatisierte Versionsverwaltung in den Paketmanagern und Ansätzen wie Semantic Versioning versucht wird, der Dependency Hell (z.B. durch viele Abhängigkeiten und Abhängigkeiten-Kaskaden) zu entkommen.

Ein Praxisbeispiel – Caliburn.Micro

Wie einfach eine Implementierung sein kann sieht man an einem Beispiel aus einem unserer Softwareteams, die mit Hilfe von WPF und .NET Software erstellen. Dafür haben sie das Framework Caliburn.Micro eingesetzt. Der primäre Grund war die Benutzung des MVVM-Patterns.

Die Entscheidung war einfach, denn Caliburn.Micro ist kostenlos und sehr einfach zu benutzen und zu integrieren. Mit der Beschränkung auf das MVVM-Pattern hatte das Team nur einen kleinen isolierten Teil gewählt, der für sie wichtig war. Trotzdem traten relativ früh Probleme auf, die die Entwicklung unnötig kompliziert gemacht haben:

  • Im MVVM-Pattern, wie es bei Caliburn.Micro implementiert wird, passiert das Matching von View und View Model über den Namen der Komponenten.

  • Bei der zusätzlichen Integration von DevExpress konnte dieses Pattern so nicht verwendet werden. Stattdessen musste das Standard WPF-Pattern verwendet werden, bestehend aus XAML + Code-Behind.

  • Durch die Benutzung von zwei Patterns ist das Team immer wieder in das WPF-Pattern gefallen, da nicht klar war, an welcher Stelle jetzt welches Pattern verwendet wird.

Zuerst wurde also der Umfang der Lösung betrachtet. Das MVVM-Pattern selbst ist ein kleinerer isolierter Teil. Es lässt sich einfach heraustrennen und selbst lösen, ohne Abhängigkeit zu Caliburn.Micro. Die Integration und die Einarbeitung in das externe Framework war ähnlich aufwendig.

Die Umsetzung der Eigenentwicklung beinhaltete demnach:

  • Das Isolieren des MVVM Pattern Matchings über Filename-Konvention (statt Namen der Komponenten) und

  • das Einbinden in WPF

Eigene Ideen können nun einfacher und schneller umgesetzt werden und der Code ist um ein Vielfaches übersichtlicher und schlanker, da auch nur solche Funktionen umgesetzt wurden, die wir tatsächlich benötigen. Unser Framework ist damit deutlich einfacher zu verstehen und somit bereit für jegliche Wartung und Erweiterung. Die Nutzung unserer firmeneigenen Standardformate und Standardstrukturen erleichtern den Umgang mit der Software firmenintern. Somit können Probleme durch bereits auch schon vorhandene Lösungen leichter beseitigt werden.

Dass es nicht viel Programmierarbeit benötigt, zeigt der selbst erstellte Code, der die durch Caliburn.Micro erhofften Vorteile abbildet.

 

 

  /// <summary>
        /// Searches for View models and views that match the name convention and adds them to the resource dictionary
        /// </summary>
        /// <param name="typeNamespace"> The namespace in which will be searched for the view models</param>
        /// <param name="res">Reference to Dictionary in which the template should be stored</param>
        /// <param name="relTypes">Array of types that contains among other types, the view model types and view types</param>
        /// <returns>List that contains the imported templates</returns>
        public static List<String> GenerateTemplateDictionary(String typeNamespace, ResourceDictionary res, Type[] relTypes)
        {
            // get names of namespaces for VMs and Views by convention
            String vmNs = $@"{typeNamespace}.ViewModels";
            String viewNs = vmNs.Replace("Model", String.Empty);

            // getting types of VMs and Views from that namespaces
            var viewModelQuery = from t in relTypes
                          where t != null && t.IsClass && !t.IsAbstract && t.Namespace != null && t.Namespace.StartsWith(vmNs)
                          select t;

            var viewModelTypeList = viewModelQuery.Where(t => t != null).GroupBy(t => t.Name).Select(g => g.First()).ToDictionary(t => t.Name, t => t);

            var viewQuery = from t in relTypes
                            where t != null && t.IsClass && !t.IsAbstract && t.Namespace != null && t.Namespace.StartsWith(viewNs)
                            select t;
            var viewTypeList = viewQuery.Where(t => t != null).GroupBy(t => t.Name).Select(g => g.First()).ToDictionary(t => t.Name, t => t);

            var foundItems = new List<String>();

            // link the VM types to View types by convention into a datatemplate and add the datatemplate to app.resources
            foreach (var vmt in viewModelTypeList)
            {
                var viewKey = vmt.Key.Replace("Model", String.Empty);
                if (viewTypeList.ContainsKey(viewKey))
                {
                    var viewType = viewTypeList[viewKey];
                    var template = CreateTemplate(vmt.Value, viewType);
                    if (!res.Contains(template.DataTemplateKey ?? throw new InvalidOperationException()))
                    {
                        res.Add(template.DataTemplateKey ?? throw new InvalidOperationException(), template);
                        foundItems.Add($@"Found and add VM -> View: {viewType.FullName} -> {vmt.Value.FullName}");
                    }
                    else
                        foundItems.Add($@"Pair is already known VM -> View: {viewType.FullName} -> {vmt.Value.FullName}");
                }
            }

            return foundItems;
        }
        
        private static DataTemplate CreateTemplate(Type viewModelType, Type viewType)
        {
            // The only way to get around some problems with binding later is this way of creating datatemplate objects
            // see www.ikriv.com/dev/wpf/DataTemplateCreation/ 

            const String xamlTemplate = "<DataTemplate DataType=\"{{x:Type vm:{0}}}\"><v:{1} /></DataTemplate>";
            var xaml = String.Format(xamlTemplate, viewModelType.Name, viewType.Name);

            var context = new ParserContext { XamlTypeMapper = new XamlTypeMapper(new String[0]) };

            context.XamlTypeMapper.AddMappingProcessingInstruction("vm", viewModelType.Namespace ?? throw new InvalidOperationException(), viewModelType.Assembly.FullName);
            context.XamlTypeMapper.AddMappingProcessingInstruction("v", viewType.Namespace ?? throw new InvalidOperationException(), viewType.Assembly.FullName);
caliburnmicro.com
            context.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
            context.XmlnsDictionary.Add("x", "http://schemas.microsoft.com/winfx/2006/xaml");
            context.XmlnsDictionary.Add("vm", "vm");
            context.XmlnsDictionary.Add("v", "v");

            var template = (DataTemplate)XamlReader.Parse(xaml, context);
            return template;
        }

Die Verwendung ist denkbar einfach.

DynamicVmTemplateFinder.GenerateTemplateDictionary(typeof(App), Current.Resources,  Assembly.GetExecutingAssembly().GetTypes())

Das Dictionary erledigt das Matching, indem dort View und View Model zusammengebunden werden.

Dass die Entscheidung richtig war, ist spätestens im Juni 2020 klar geworden. Zu diesem Zeitpunkt wurde Caliburn.Micro vom Entwickler als nicht mehr gewartet markiert.

Quelle: https://caliburnmicro.com/

Die Gründe sind oft dieselben. Es sind wenige Entwickler verantwortlich für das Projekt, die Entwickler verändern ihr privates oder berufliches Setup und können eine oft privat durchgeführte Entwicklung nicht mehr aufrecht erhalten.

Fazit

Das Beispiel zeigt, dass Entscheidungen und Abwägungen in diesem Fall dazu geführt haben, dass sich die Investition gelohnt hat. Die Freiheit, diese Entscheidung im Entwicklungsteam zu treffen, charakterisiert die Arbeit bei OHB Digital Services.

Der Gestaltungsraum für diese Entscheidungen ist groß, eigene Ideen können eingebracht werden und es wird nicht vom Projektleiter vorgegeben, welche Art von Bibliothek genutzt werden soll. Aus der Tatsache, dass im Team entschieden wird, ergeben sich somit oft die besseren Lösungen.

Für uns hat sich im Laufe der Zeit gezeigt, dass es häufiger sinnvoll ist, eigene Lösungen zu erstellen und ein einfaches kleines Framework selbst zu entwickeln, anstatt komplexere Third-Party-Frameworks zu benutzen.

Wenn man sich dennoch für eine Fremdbibliothek entscheidet, sollte man sich immer bewusst sein, dass dies kein Selbstläufer ist. Statt einfach zu integrieren und zu vergessen ist auch bei Fremdsoftware eine hohe Sorgfalt und Pflege wichtig. Steigt die Anzahl der Abhängigkeiten verliert man mitunter den Blick auf versteckte Inkompatibilitäten. Auch aufwendige Fehlerbehebungen können die Folge sein, wenn die Fremdsoftware nicht tiefgreifend verstanden ist. All das darf nicht in Vergessenheit geraten.

Am Ende soll natürlich das bestmögliche Produkt für den Kunden bereitstehen und hiernach sollte auch die Entscheidung getroffen werden.