Die Kombination von funktionalen und objektorientierten Sprachelementen mit F# und C# Bachelorarbeit zur Erlangung des akademischen Grades BACHELOR OF SCIENCE (B.SC.) im Studiengang Allgemeine Informatik vorgelegt an der Fachhochschule Köln Campus Gummersbach Fakultät für Informatik und Ingenieurwissenschaften ausgearbeitet von: Michael Morbach Erstprüfer/in: Prof. Dr. Erich Ehses (Fachhochschule Köln) Zweitprüfer/in: Prof. Dr. Victor (Fachhochschule Köln) Gummersbach, im Juli 2013 I Kurzfassung Kurzfassung Die vorliegende Bachelorarbeit beschäftigt sich vorwiegend mit der Fragestellung inwieweit sich die Paradigmen der funktionalen und der objektorientierten Programmierung in der heutigen Softwareentwicklung im .NET Umfeld kombinieren lassen und wie sie sich bereits darin äußern. Eine ausführliche Einführung in die funktionale Programmierung soll dazu beitragen, einen Einblick in die Konzepte und die Sichtweise des funktionalen Entwickelns und Denkens zu erlangen. Diese Einblicke werden durch den Entwicklungsprozess einer Anwendung bestärkt, der praxisnah darstellt, in welchen Bereichen sich die beiden Paradigmen ergänzen können. Abstract This bachelor thesis deals mainly with the question how far it is practically possible to combine the paradigms of functional- and object oriented programming in a .NET environment and how they do manifest themselves already. A detailed introduction to functional programming should help to gain an insight into the functional concepts and thinking. These insights are strengthened by the development process of an application, which demonstrates where functional and object oriented programming complement each other. II Inhaltsverzeichnis Inhaltsverzeichnis Kurzfassung ...................................................................................................................... I Abstract............................................................................................................................. I Inhaltsverzeichnis .......................................................................................................... II Abbildungsverzeichnis................................................................................................... V Abkürzungsverzeichnis ................................................................................................ VI 1 Einleitung ............................................................................................................. 1 2 Grundlagen der funktionalen Programmierung .............................................. 2 2.1 Was ist funktionale Programmierung? .................................................................. 2 2.2 2.2.1 2.2.2 2.2.3 2.2.4 Funktionale Programmierung produktiv einsetzen ............................................... 3 Paradigma der funktionalen Programmierung ....................................................... 3 Deklarativer Programmierstil ................................................................................ 3 Verstehen was ein Programm tut ........................................................................... 5 Nebenläufigkeit ..................................................................................................... 6 2.3 2.3.1 2.3.2 2.3.3 Beispiele funktionaler Programmierung................................................................ 7 Das Verarbeiten von Daten mit LINQ ................................................................... 7 Beschreibung einer Benutzeroberfläche mit XAML ............................................. 8 Komposition .......................................................................................................... 9 3 Kernkonzepte von F# ........................................................................................ 11 3.1 3.1.1 3.1.2 3.1.3 Auswertung funktionaler Programme ................................................................. 11 Verwenden von unveränderlichen Datenstrukturen ............................................ 11 Den Zustand eines Programms durch Rekursion verändern ............................... 12 Verwenden von Ausdrücken an Stelle von Anweisungen .................................. 13 3.2 Schreiben von deklarativem Code ....................................................................... 14 3.2.1 Funktionen als Argumente .................................................................................. 14 3.2.2 Funktionen höherer Ordnung .............................................................................. 15 3.3 Typen in der funktionalen Programmierung ....................................................... 16 4 Anwendungskonzept Pixxler ............................................................................ 18 4.1 Serviceorientierte Architektur ............................................................................. 18 4.2 WCF Service Pixxler ........................................................................................... 19 4.3 Pixxler API .......................................................................................................... 19 4.4 Clientanwendung Pixxler .................................................................................... 20 4.5 F# Bibliothek ....................................................................................................... 20 III Inhaltsverzeichnis 5 Implementierung des Pixxler Services............................................................. 21 5.1 Was ist ein WCF Service ..................................................................................... 21 5.2 Einrichten der Datenbank .................................................................................... 22 5.3 Ansteuerung der Datenbank aus dem Code......................................................... 23 5.3.1 Einsatz des Entity Frameworks zur Abbildung des Datenbankmodells im Code ..................................................................................................................... 24 5.3.2 Was ist LINQ? ..................................................................................................... 25 5.4 5.4.1 5.4.2 5.4.3 5.4.4 5.4.5 Authentifizierung eines Benutzers ...................................................................... 27 Annotation von Klassen und Methoden .............................................................. 27 Schnittstellendefinition zum Authentifizieren ..................................................... 28 Abfragen von Daten aus der Datenbank mit LINQ ............................................. 29 Das Repository Entwurfsmuster .......................................................................... 29 Validieren eines Benutzers .................................................................................. 30 6 Funktionale Bibliothek mit F# ......................................................................... 33 6.1 Parallelisierungstechniken ................................................................................... 33 6.1.1 Parallelisieren von kleinen Codesegmenten ........................................................ 33 6.2 6.2.1 6.2.2 6.2.3 Implementierung der grafischen Effekte ............................................................. 34 Berechnungen von Farben ................................................................................... 35 Implementieren und Anwenden von Farbfiltern ................................................. 36 Implementieren eines Unschärfeeffekts .............................................................. 39 6.3 Was kann F# einem C# Entwickler bieten? ........................................................ 40 7 Implementierung des Pixxler WPF Clients ..................................................... 41 7.1 Warum WPF? ...................................................................................................... 41 7.2 7.2.1 7.2.2 7.2.3 Architektur der Anwendung ................................................................................ 42 PRISM ................................................................................................................. 42 Das Architekturmuster MVVM ........................................................................... 44 Dependency Injection .......................................................................................... 45 7.3 Struktur der Anwendung (Shell) ......................................................................... 46 7.4 7.4.1 7.4.2 7.4.3 Implementierung der Module .............................................................................. 48 Anmeldung am Service ....................................................................................... 48 Übersicht über die Alben ..................................................................................... 49 Bearbeiten von Bildern ........................................................................................ 52 8 Fazit .................................................................................................................... 53 8.1 Zusammenfassung ............................................................................................... 53 8.2 8.2.1 8.2.2 8.2.3 Wie hat sich die funktionale Programmierung eingebracht ................................ 54 LINQ zur Abfrage von Datenstrukturen.............................................................. 54 WPF & XAML zur Gestaltung von Oberflächen ................................................ 55 F# zur Implementierung von Kernfunktionalitäten ............................................. 56 IV Inhaltsverzeichnis 8.3 Persönliches Fazit ................................................................................................ 56 9 Literaturverzeichnis .......................................................................................... 58 V Abbildungsverzeichnis Abbildungsverzeichnis Abbildung 1: SOA (Crofton, 2010) ................................................................................ 18 Abbildung 2: WCF - ABC Prinzip ................................................................................ 22 Abbildung 3: ER Modell der Datenbank ........................................................................ 23 Abbildung 4: EF Modell der Datenbank im Code .......................................................... 25 Abbildung 5: Definition des Interfaces für den BenutzerService ................................... 28 Abbildung 6: Das Repository Entwurfsmuster .............................................................. 30 Abbildung 7: Das BenutzerRepository ........................................................................... 30 Abbildung 8: Die DataContracts..................................................................................... 32 Abbildung 9: Modell des PRISM Frameworks ............................................................. 44 Abbildung 10: Modell des MVVM Entwurfsmusters ................................................... 45 Abbildung 11: Struktureller Aufbau des WPF Clients ................................................... 46 Abbildung 12: Login am Service .................................................................................... 48 Abbildung 13: Login Sequenzdiagramm ........................................................................ 49 Abbildung 14: Inhalte eines Albums darstellen.............................................................. 50 Abbildung 15: Ein Bild im Detail aufrufen .................................................................... 51 Abbildung 16: Ein Bild bearbeiten ................................................................................. 52 VI Abkürzungsverzeichnis Abkürzungsverzeichnis API Application Programming Interface CLR Common Language Runtime DI Dependency Injection EF Entity Framework (5.0) GUID Globally Unique Identifier IoC Inversion of Control LINQ Language Integrated Query MVVM Model-View-ViewModel SOAP Simple Object Access Protocol SoC Separation of Concerns UI User Interface WCF Windows Communication Foundation WPF Windows Presentation Foundation XAML Extensible Application Markup Language 1 Einleitung 1 Einleitung Die letzten Jahrzehnte hat die objektorientierte Programmierung den Markt in der Softwareentwicklung dominiert, indem sie es erreichte, die Komplexität einer Anwendung mit Struktur zu versehen und Sprachen für Entwickler intuitiv einsetzbar zu machte. Doch nicht jede Art von Komplexität lässt sich mit Hilfe von Objekten abbilden. Einige Bereiche, die z.B. Berechnungen, Analysen oder Transformationen betreffen, ziehen keinen Nutzen aus Objekten. Somit stehen Entwickler in der heutigen Zeit neuen Herausforderungen gegenüber. Es müssen Programme entwickelt werden, die eine große Menge an Daten schnell und sparsam verarbeiten können und gleichzeitig auf mehrere Prozessoren zugeschnitten sind. Aus dem Schatten der wissenschaftlichen Institutionen haben sich die funktionalen und deklarativen Techniken der Softwareentwicklung in den vergangenen Jahren mehr und mehr auch in der Wirtschaft bewährt. In C# wurden generics, anonyme Funktionen und Ausdrücke zur Abfrage von Daten eingebaut. Eine ausgereifte kommerzielle funktionale Sprache blieb allerdings bisher aus. Mit F# entwickelte Microsoft seit 2002 eine Sprache die sowohl funktional ist, zudem aber über das .NET Objektmodell ebenso Zugriff auf objektorientierte Bereiche besitzt. Programmiersprachen in der heutigen Zeit sind multiparadigmatisch. Objektorientierte und funktionale Programmierung geht mehr und mehr in einander über und bietet somit eine weitläufige Liste von einsetzbaren Möglichkeiten bei der Entwicklung von Anwendungen. So kann bereits jetzt in C# auf viele funktionale Techniken zurückgegriffen werden.. Funktionale Programmierung jedoch beschränkt sich nicht nur auf die Art und Weise wie Code geschrieben wird. Mehr ist es eine andere Sichtweise wie Probleme gelöst werden können. In dieser Arbeit sollen diese Betrachtungsweise und einige anwendbare Techniken bei der Entwicklung einer Software, die sowohl das objektorientierte als auch das funktionale Paradigma nutzt, aufgezeigt werden. 2 Grundlagen der funktionalen Programmierung 2 Grundlagen der funktionalen Programmierung Funktionale Sprachen sind Ausdrucksstart und erledigen sogar große Aufgaben, obwohl der dazu geschriebene Code meist minimalistisch ist. Funktionaler Code wird ergebnisorientierter verfasst und die Art und Weise wie Anweisungen ausgeführt werden, kann besser vor den Entwicklern versteckt werden. Es ist insgesamt unwichtiger worden wie die Ergebnisse erreicht werden, denn dieser Weg zur Ergebnisfindung muss nur ein einziges Mal spezifiziert werden. 2.1 Was ist funktionale Programmierung? Es existiert eine ganze Reihe von unterschiedlichen funktionalen Sprachen und es gibt keine Funktionalitäten die eben jede dieser Sprachen besitzen müsste. Daher ist es schwierig eine allgemeingültige Definition, die auf die funktionale Programmierung zutrifft, zu finden. Was die Sprachen miteinander verbindet sind eher allgemeine Aspekte. Dennoch hat jede dieser Sprachen einen eigenen Stil, um Lösungen für Probleme zu schaffen. Funktionale Programmierung lässt sich daher wohl am besten im Vergleich zur imperativen Programmierung beschreiben. „Functional programming is a style of programming that emphasizes the evaluation of expressions, rather than execution of commands. The expressions in these languages are formed by using functions to combine basic values“ 1 Der funktionale Ansatz wird aus dem ersten Satz gut ersichtlich. Die „Auswertung von Ausdrücken“ repräsentiert den funktionalen Aspekt und die „Ausführung von Anweisungen“ den imperativen Aspekt. Ausführung von Anweisungen – Ein Programm wird als eine Verkettung von Befehlen ausgedrückt. Die Befehle beschreiben wie das Ende des Programms erreicht wird und was für Objekte während dieses Prozesses erstellt und verändert werden. Auswertung von Ausdrücken – Ein funktionales Programm entspricht eher einem Ausdruck der beschreibt wie z.B. die Eigenschaften von einem Objekt aussehen 1 (Hutton, 2002) http://www.cs.nott.ac.uk/~gmh/faq.html 3 Grundlagen der funktionalen Programmierung sollen nachdem es erstellt wurde. Es werden keine einzelnen Schritte vorgegeben, die besagen wie das Objekt zusammengebaut wird und es besteht keine Möglichkeit die Erzeugung eines Objektes zu beeinflussen, bevor es nicht erstellt wurde. Sollte also z.B. eine Tasse Kaffee mit Zucker erstellen werden, so könnte der Kaffee nicht getrunken werden bevor der Zucker enthalten ist, denn wenn das Objekt erstellt ist enthält es den Zucker bereits. Der wesentliche Unterschied ist also der, dass bei der funktionalen Programmierung Code in Form von Ausdrücken anstatt in einer Reihe von Anweisungen geschrieben wird. Aus diesem Grund wird Code in der funktionalen Programmierung auf eine ganz andere Art und Weise verfasst und gekapselt. 2.2 Funktionale Programmierung produktiv einsetzen Entwickler schätzen die funktionale Programmierung oft durch ihre ausdrucksstarke und lesbare Art. Dies ist jedoch noch lange kein Grund es auch wirklich zu praktizieren. In diesem Kapitel soll der Nutzen und die Vorteile der funktionalen Programmierung herausgestellt werden. 2.2.1 Paradigma der funktionalen Programmierung Funktionales Programmieren ist ein Programmierparadigma. Das bedeutet, dass es Konzepte definiert, die genutzt werden können um bestimmte Probleme zu lösen. In der Objektorientierten Programmierung wird über Problemlösungen in Form von Objekten nachgedacht. Jede Sprache ist im speziellen etwas anders. So bietet C++ die mehrfache Vererbung und JavaScript die Prototypen. Funktional zu programmieren bedeutet jedoch nicht gleichzeitig auf das jeweils andere Paradigma zu verzichten. So bietet die Sprache C#, welche eine im Kern objektorientierte Sprache ist, seit der Version 3.0 auch einige funktionale Möglichkeiten die ausgenutzt werden können. Auf der anderen Seite ist F# eine funktionale Sprache, die jedoch das vollständige .NET Objektmodell unterstützt. Es kann also in beiden Fällen genau das Paradigma für ein Problem gewählt werden, welches für die Lösung am besten geeignet ist. 2.2.2 Deklarativer Programmierstil Beim deklarativen Programmierstil wird die Logik eines Programmes ausgedrückt, ohne auf konkrete Implementierungsdetails näher einzugehen. Diese Beschreibung könnte 4 Grundlagen der funktionalen Programmierung zwar auch auf die Definition der funktionalen Programmierung passen, jedoch ist die Idee der deklarativen Programmierung sehr viel weitläufiger und kann mit vielen unterschiedlichen Technologien umgesetzt werden. Da sich diese Arbeit unter anderem auf die funktionale Programmierung konzentriert, wird im Folgenden demonstriert wie anhand einer funktionalen Sprache deklarativer Code geschrieben werden könnte. Anwendungen werden implementiert, indem einem Computer die Ziele anhand eines vorgegebenen Vokabulars, welches dieser interpretieren kann, beschrieben werden. Bei der imperativen Programmierung besteht dieses Vokabular aus einzelnen Befehlen. Es besteht allerdings immer die Möglichkeit neue Befehle, wie z.B. „Zeige Kundendetails“, hinzuzufügen. Das gesamte Programm ist jedoch lediglich eine Aneinanderreihung von einzelnen Schritten, die beschreiben wie eine Gesamtaufgabe erledigt werden soll. Eine Aufgabe könnte zum Beispiel wie folgt lauten: „Nimm den nächsten Kunden aus der Liste. Wenn der Kunde in Deutschland wohnt, dann zeige seine Details. Wenn mehrere Kunden in der Liste vorhanden sind gehe zum Anfang“ Sobald ein Programm wächst, wird die Anzahl an Befehlen in dem Vokabular schnell größer und es wird immer schwieriger dieses zu verwenden. Der objektorientierte Ansatz erleichtert dies, weil dieser eine Ordnung und Organisation in die anlaufenden Befehle bringt. So könnten alle Befehle, die mit Kunden zu tun haben, in einer Klassenstruktur unterbringen. Das Programm selbst wäre jedoch immer noch eine Sequenz aus Befehlen. Funktionale Programmierung bietet einen ganz anderen Ansatz dieses Vokabular zu erweitern. Anstatt sich darauf zu beschränken nur neue Befehle zu erstellen, wird darauf gesetzt neue Kontrollstrukturen zu erzeugen – Primitive die beschreiben wie Befehle miteinander verknüpft werden können. In imperativen Programmen wird eine Sequenz von Befehlen zusammengestellt, die eine beschränkte Anzahl von eingebauten Kontrollstrukturen wie z.B. Schleifen nutzt. Wenn jedoch ein typisches Programm betrachtet wird, so lässt sich beobachten, dass sich die meisten Strukturen wiederholen. Einige dieser Strukturen sind bekannt und werden Entwurfsmuster genannt. In dem Kundenbeispiel lässt sich ebenfalls ein Muster erkennen. „Führe den ersten Befehl für jeden Kunden aus bei dem der Zweite Befehl ‚wahr‘ zurückgibt“. Diesen Ausdruck kann man auch simplifizieren zu „Zeige die Kundendetails für jeden Kunden 5 Grundlagen der funktionalen Programmierung aus Deutschland“. In diesem Satz ist „Zeige Kundendetails“ das erste Argument und „aus Deutschland“ das Zweite. Beide Sätze zur Lösung desselben Problems werden nun gegenübergestellt: Nimm den nächsten Kunden aus der Liste. Wenn der Kunde in Deutschland wohnt, dann zeige seine Details. Wenn mehrere Kunden in der Liste vorhanden sind gehe zum Anfang Zeige die Kundendetails für jeden Kunden aus Deutschland Es lässt sich klar erkennen, dass im ersten Satz genau beschreiben wird wie das Ziel erreicht werden kann, während der zweite Satz beschreibt was genau erreicht werden soll. Dies ist auch gleichzeitig der essentielle Unterschied zwischen imperativer und deklarativer Programmierung. 2.2.3 Verstehen was ein Programm tut Ein gewöhnliches Programm besteht aus allerhand Objekten die einen internen Zustand haben, der entweder direkt oder durch Methodenaufrufe verändert werden kann. Das bedeutet, dass es sehr schwierig sein kann zu erkennen welche Eigenschaften sich durch einen Methodenaufruf verändert haben. Folgendes Beispiel verdeutlicht dies: Ellipse und Rechteck (C#) Ellipse el = new Ellipse( new Rectangle( 0, 0, 100, 100 ) ); Rectangle rc = el.BoundingBox; rc.Inflate( 10, 10 ); return el; Bei diesem Ausschnitt kann man nicht wissen, welchen Zustand die zurückgegebene Ellipse haben wird, ohne den Code auszuführen. Besitzt die BoundingBox eine Rückreferenz zur Ellipse und verändert die Methode Inflate(x,y) das Rechteck oder gibt es ein neues Rechteck zurück? Wäre letzteres der Fall, so hätte die dritte Zeile keinerlei Effekt. Bei der funktionalen Programmierung sind die meisten Datenstrukturen nicht veränderlich. Somit kann eine erzeugte Ellipse nach ihrer Erzeugung nicht mehr verändert werden. Das einzige was getan werden könnte, wäre eine neue Ellipse zu erzeugen und mit anderen Parametern zu initialisieren. Das erleichtert das Lesen von Quelltext erheblich wie im nächsten Beispiel leicht zu erkennen ist. 6 Grundlagen der funktionalen Programmierung Ellipse und Rechteck (C#) Ellipse el = new Ellipse( new Rectangle( 0, 0, 100, 100 ) ); Rectangle rc = el.BoundingBox; Rectangle rcNew = rc.Inflate( 10, 10 ); return new Ellipse( rcNew ); Beim Schreiben von funktionalen Programmen mit unveränderlichen Typen ist das Einzige, was eine Methode tun kann, ein Rückgabewert liefern. Wie man im Beispiel erkennen kann gibt die Methode Inflate(x,y) ein neues Rechteck zurück. Dies ist keine neue Idee in der .NET Welt, denn es existieren auch andere Typen die genauso funktionieren, wie z.B. der Typ DateTime oder String. Die Lesbarkeit des Quellcodes wird durch diese Variante maßgeblich verbessert. Die Lesbarkeit ist jedoch nicht der einzige Grund für diesen Ansatz. Ein weiterer Grund ist die Entwicklung von Anwendungen mit Unterstützung für mehrere Kerne. 2.2.4 Nebenläufigkeit Möchte man in der traditionellen imperativen Programmierung eine nebenläufige Anwendung entwickeln, so stößt man auf folgende Probleme: Es ist schwer, existierenden sequenziellen Code in parallel ablaufenden Code umzuwandeln, da eine große Menge an Code explizit so umgeschrieben werden muss, dass Threads verwendet werden. Es muss sehr genau geprüft werden, dass beim Zugriff auf Objekte keine Deadlocks auftreten können und dennoch muss genügend Spielraum zur parallelen Ausführung bestehen bleiben. Die Funktionale Programmierung hat darauf folgende Antworten: Bei Verwendung des deklarativen Programmierstils kann Nebenläufigkeit in bestehenden Code eingeführt werden. Durch Ersetzen der Primitive, die bestimmen wie Befehle kombiniert werden, ist dies jederzeit möglich. Dank der Unveränderlichkeit von Objekten ist es unmöglich, dass race conditions2 oder deadlocks3 auftreten. Es ist außerdem direkt ersichtlich, welche Teile des Programms unabhängig laufen. 2 3 Eine Konstellation in der das Ergebnis einer Operation durch andere Einzeloperationen beeinflusst wird Ein Zustand, bei dem mehrere Prozesse auf die Zustandsänderung eines anderen warten und sich gegenseitig behindern 7 Grundlagen der funktionalen Programmierung Durch diese beiden Aspekte wird das Design einer Anwendung maßgeblich beeinflusst, was zu einer Erleichterung beim Schreiben von parallel ausführbarem Code führt wodurch auch der Vorteil von Mehrkernprozessoren ausgenutzt werden kann. Es sollte jedoch beachtet werden, dass die Parallelisierung von Code nicht allein durch den Einsatz von nicht veränderlichem Code eintritt, sondern lediglich erleichtert wird. 2.3 Beispiele funktionaler Programmierung Im Folgenden soll anhand einiger kleinerer Beispiele gezeigt werden, dass funktionale Programmierung nicht nur in der Theorie einsetzbar ist. Es wird außerdem auf die deklarative Programmierung sowie ihre Vorteile anhand von LINQ und XAML eingegangen. 2.3.1 Das Verarbeiten von Daten mit LINQ LINQ wurde von Microsoft entwickelt, um ein einheitliches Verfahren für den Zugriff auf Datenstrukturen verschiedener Arten bereitzustellen. Im Folgenden soll anhand eines Codebeispiels gezeigt werden, welche Vorteile sich durch den deklarativen Programmierstil ergeben. Imperative Datenverarbeitung (C#) public IEnumerable<string> GetExpensiveProducts() { List<string> filteredInfos = new List<string>(); foreach ( Product product in this.Products ) { if ( product.UnitPrice > 75.0M ) { filteredInfos.Add( String.Format( "{0} - €{1}", product.ProductName, product.UnitPrice ) ); } } } Dieses Codebeispiel ist als eine Sequenz von Befehlen im imperativen Stil geschrieben. Es wird eine neue Liste erzeugt, danach über alle bestehenden Produkte iteriert und anschließend werden die Produkte, die einen bestimmten Preis übersteigen, in eine neue Liste eingefügt. Beschreiben wir das Problem etwas abstrakter, so wird klar, dass lediglich eine Auflistung von Produkten gefiltert werden soll. 8 Grundlagen der funktionalen Programmierung Mit C#, ab der Version 3.0, kann dieses Problem auch mit Hilfe von LINQ gelöst werden. Das folgende Codebeispiel beschreibt wesentlich anschaulicher und lesbarer, welches eigentliche Ziel erreicht werden soll. Deklarative Datenverarbeitung mit LINQ (C#) public IEnumerable<string> GetExpensiveProducts() { return from product in this.Products where product.UnitPrice > 75.0M select String.Format( "{0} - €{1}", product.ProductName, product.UnitPrice ); } Dieser einzelne zusammenhängende Ausdruck besteht aus elementaren Operatoren wie z.B. select und where. Diese Operatoren nehmen wiederum andere Ausdrücke als Argumente, um zu ermitteln was gefiltert werden soll und welches Ergebnis zurückgegeben werden soll. Diese Operatoren geben dem Entwickler also eine Möglichkeit, verschiedene Teile des Codes zu kombinieren, ohne dabei mehr Code schreiben zu müssen. Ein weiterer interessanter Aspekt ist, dass dieser Ausdruck wesentlich einfacher zu lesen ist, da die konkrete Implementierung der elementaren Operatoren vor dem Entwickler versteckt wird. Es wurde eine Lösung geschaffen, die einerseits einfacher von der Implementierung, als auch flexibler in der Anwendung ist. In Kapitel 6.1 wird außerdem ersichtlich, dass es so einfacher ist, Code zu parallelisieren. 2.3.2 Beschreibung einer Benutzeroberfläche mit XAML Die Gestaltung von Benutzeroberflächen hat sich in den vergangenen Jahren gerade durch den Einsatz von Designern innerhalb den Entwicklungsumgebungen stark verbessert. Code wird immer häufiger von Generatoren automatisiert erzeugt, da dieser, gerade bei komplexen Oberflächen, für Entwickler kaum noch überschaubar ist. Die Windows Presentation Foundation (WPF) und ihre enthaltene deklarative Sprache XAML gehen dabei einen Schritt weiter. Seit der Version 3.0 sind beide ein fester Bestandteil des .NET Frameworks und bringen neben dem Compiler auch einen Laufzeitparser für XAML mit. XAML ist eine allgemein einsetzbare deklarative Sprache und besonders geeignet um Benutzeroberflächen zu gestalten. Sie ermöglicht die Trennung zwischen dem Teil, der die Ansicht der Oberfläche beschreibt und dem Teil, der die konkrete Programmlogik 9 Grundlagen der funktionalen Programmierung enthält. Genau wie bei XML wird die Beschreibung der Oberfläche in einer Baumstruktur repräsentiert. Dadurch werden eine wesentlich schlankere Form der Oberfläche sowie eine bessere Lesbarkeit gewährleistet. Einen Designer gibt es nach wie vor, jedoch sind Anpassungen an automatisch generierten Code einfacher möglich. Erzeugung eines UI im deklarativen Stil (XAML) <!-- Deklarativ geschriebene Benutzeroberfläche mit XAML --> <Canvas Background="Black"> <Ellipse x:Name="greenEllipse" Width="75" Height="75" Canvas.Left="0" Canvas.Top="0" Fill="LightGreen"> </Ellipse> </Canvas> Der XAML Code beschreibt die Benutzeroberfläche durch den Einsatz elementarer Schlüsselworte und das Festlegen von bestimmten Eigenschaften, so dass der gesamte Code zu einem einzigen Ausdruck wird. Auch hier wird wieder sehr deutlich, dass lediglich festgelegt wird was das Ergebnis sein soll und das wie in einer Blackbox vor den Entwicklern versteckt wird. 2.3.3 Komposition Es wurde bereits gezeigt, dass es möglich ist, viele Probleme durch den deklarativen Stil einfacher zu lösen, wenn zu diesem Zweck zuvor die nötigen Bibliotheken erstellt wurden. Datenstrukturen können mit Hilfe von LINQ einfach abgefragt und Benutzeroberflächen mit XAML leicht verständlich erstellt werden. Für die spezifischeren Probleme, die einen eigenen Bereich betreffen, gibt es jedoch meist keine vorgefertigten Bibliotheken. Die Funktionalität, die vor den Entwicklern in einer Blackbox versteckt wird, entsteht jedoch nicht auf magische Weise, sondern muss zunächst einmal implementiert und in einer Bibliothek gekapselt werden. Im Folgenden wird beispielhaft gezeigt wie solch eine Bibliothek entstehen könnte. Erstellen einer funktionalen Animation (C#) var green = Anims.Circle(Brushes.OliveDrab, 100.0f.Anim()); var blue = Anims.Circle(Brushes.SteelBlue, 100.0f.Anim()); var animatedPos = Time.Wiggle * 100.0f.Anim(); var greenMove = green.MoveXY(animatedPos, 0.0f.Const()); var blueMove = blue.MoveXY(0.0f.Const(), animatedPos); var animation = Anims.Compose(greenMove, blueMove); 10 Grundlagen der funktionalen Programmierung Es ist nicht wichtig das Beispiel im Detail zu verstehen. Im Grunde genommen werden lediglich 2 Kreise erzeugt, eine Bewegung mit Hilfe der MoveXY Methode erzeugt und durch die Compose Methode in eine einzelne Animation gekapselt. Ein wichtiger Aspekt von deklarativen Bibliotheken ist die Tatsache, dass sie kombinierbar eingesetzt werden können. In diesem Beispiel ist eine solche Komposition z.B. durch das Erstellen einer einzelnen Animation aus mehreren einzelnen Bewegungen deutlich geworden. Ein anderes Beispiel wäre LINQ, wo komplexe Abfragen aus mehreren einzelnen kleineren Abfragen erstellt werden können. Somit könnten z.B. auch eigene Primitive erzeugt werden, um damit Animationen zu erstellen. 11 Kernkonzepte von F# 3 Kernkonzepte von F# Der Fokus in diesem Kapitel liegt darauf, die Kernkonzepte der Sprache F# kennenzulernen und einen Einblick in allgemeine Ideen sowie elementare Techniken der funktionalen Programmierung zu bekommen. 3.1 Auswertung funktionaler Programme Im vorherigen Kapitel wurde bereits angedeutet, dass funktionale Programme unveränderliche Datenstrukturen nutzen. Man mag sich nun fragen, wie ein Programm überhaupt etwas tun kann, wenn alles unveränderlich ist. Die Antwort ist kurz wie auch einfach. Ein funktionales Programm wird nicht als eine Verkettung von Ausdrücken beschrieben die den Zustand verändern sondern mehr als eine systematische Anwendung von Regeln.4 3.1.1 Verwenden von unveränderlichen Datenstrukturen Wird innerhalb eines funktionalen Programmes von Datenstrukturen gesprochen, so sind im allgemeinen Werttypen oder Klassen, wie sie aus C# bekannt sind, gemeint. Diese Datenstrukturen sind in F# unveränderlich, sobald sie zum ersten Mal initialisiert wurden. In C# können einzelne Daten mit Hilfe des Schlüsselwortes readonly ebenfalls als unveränderlich gekennzeichnet werden. Ebenso ist es auch möglich in F# veränderliche Typen zu erzeugen, jedoch ist der Standard, dass Typen nicht veränderlich sind. Das bedeutet auch, dass Methodenaufrufe nicht den Zustand einer Struktur verändern, sondern stets einen Rückgabewert besitzen der weiter verarbeitet werden muss. Das kann bei der Initialisierung einer Liste z.B. wie folgt aussehen. Erstellen einer Liste var list = ImmutableList.Empty<int>().Add(1).Add(3).Add(5).Add(7); Dieses Beispiel ist jedoch extrem simpel und sobald darüber nachgedacht wird ein komplexeres Problem auf dieselbe Weise anzugehen, kann es schwierig werden. 4 Vgl. computation by calculation (Hudak, 2000) 12 Kernkonzepte von F# 3.1.2 Den Zustand eines Programms durch Rekursion verändern Um aufzuzeigen wie komplexere Probleme gelöst werden können, wird im Folgenden eine Funktion implementiert, die die Zahlen x bis y aufsummiert, ohne dabei auf lokale Variablen zurückzugreifen. Im einfachsten Sinne und durch Verwendung lokaler Variablen könnte diese Funktion wie folgt aussehen: Aufsummieren von Zahlen mit lokaler Variable (C#) int SumNumbers( int from, int to ) { int res = 0; for ( int i = from; i <= to; i++ ) res = res + i; return res; } Es ist in diesem Fall nicht möglich, die lokale Variable durch eine Wertbindung zu ersetzen, da die Schleife bei jedem Durchlauf einen bestimmten Zustand festhalten muss. Das bedeutet, dass eine fundamentale Veränderung am Code durchgeführt werden muss und auf eine Technik zur Anwendung kommt, die sich Rekursion5 nennt. Aufsummieren von Zahlen durch Rekursion (C#) int SumNumbers( { if ( from > int sumRest return from } int from, int to ) to ) return 0; = SumNumbers( from + 1, to ); + sumRest; Dieses Beispiel ist funktional, da es lediglich mit Wertzuweisungen und Rückgabewerten arbeitet. Die Rekursion wird hier durch einen Selbstaufruf und eine Abbruchbedingung innerhalb der Funktion repräsentiert. Der Zustand der Berechnung wird nun nicht mehr in einer lokalen Variablen festgehalten, sondern wird durch die Rekursion selbst ausgedrückt. Den rekursiven Teil einer Berechnung jedes Mal selbst zu schreiben wäre jedoch aufwändig und auch hier gibt es wiederum einen Weg den schwierigen Teil zu verstecken und Probleme zu lösen ohne explizit auf Rekursionen zurückzugreifen. 5 Eine Technik, in der eine Funktion sich selbst über den Aufruf von selbst definiert 13 Kernkonzepte von F# 3.1.3 Verwenden von Ausdrücken an Stelle von Anweisungen Bei der imperativen Programmierung ist ein Ausdruck ein einfaches Stück Code, welches ausgewertet wird und dann ein Ergebnis liefert. So ist z.B. ein Methodenaufruf bzw. die Verwendung eines booleschen- oder Zahlenoperators ein Ausdruck. Eine Anweisung hingegen ist ein Stück Code welches den Zustand des Programms verändert. Eine Anweisung wäre somit z.B. der Aufruf einer Methode, die keinen Rückgabewert besitzt oder die Zuweisung eine Wertzuweisung zu einer Variablen. Bei funktionalen Sprachen wird der Zustand durch das Ergebnis einer Funktion repräsentiert und weitergegeben. Wird diese Logik weiter verfolgt, so lässt sich sagen, dass in der funktionalen Sprache alles als Ausdruck bezeichnet werden kann. Betrachtet man nochmals das Beispiel aus dem vorherigen Abschnitt, in welchem Zahlen aufaddiert wurden, so lässt sich feststellen, dass es nicht vollkommen funktional ist, da es in einer Folge von drei Anweisungen geschrieben ist. Mit Hilfe einer kleinen Anpassung lässt sich dieses Beispiel jedoch auf eine vollständig funktionale Version verbessern. Aufsummieren von Zahlen durch Rekursion (C#) int SumNumbers( int from, int to ) { return ( from > to ) ? 0 : { int sumRest = SumNumbers( from + 1, to ); from + sumRest; } } Der Methodenkörper ist jetzt rein funktional, da er nur noch aus einem einzelnen Ausdruck besteht. Dies ist in der Sprache C# nur in relativ seltenen Fällen möglich, da innerhalb von konditionellen Auswertungen keine lokalen Variablen verwendet werden können. Obwohl dieses Beispiel sehr minimalistisch gehalten ist, gibt es einen Hinweis darauf, was man in der funktionalen Sprache alles schreiben kann: Der Methodenkörper ist ein einzelner Ausdruck. In C# bedeutet das auch, dass der Körper mit dem Schlüsselwort return beginnt. Da If-Then-Else ebenfalls eine Anweisung ist, muss man auf den konditionellen Operator (?:) zurückgreifen. Der Ausdruck im else Block enthält eine Variablendeklaration gefolgt von einem Ausdruck. Es wird eine lokale Variable sumRest deklariert welche im restlichen Codeblock verfügbar bleibt. Variablendeklarationen in F# funktionieren auf dieselbe Art und Weise. Die Deklaration ist zwar kein Ausdruck aber ein 14 Kernkonzepte von F# syntaktisches Hilfskonstrukt welches an einen Ausdruck angehangen werden kann. 3.2 Schreiben von deklarativem Code Bisher wurde lediglich beleuchtet, welche Auswirkungen die deklarative Programmierung hat und in wie weit die Anwendung dieses Programmierstils das Entwickeln erleichtern kann. Die folgenden Kapitel sollen zeigen, wie dies auf der technischen Seite überhaupt möglich gemacht wurde. Aus der technischen Sicht deuten zwei Aspekte besonders auf den deklarativen Stil hin. Der Erste von beiden wurde zuletzt behandelt – nämlich das jedes Sprachkonstrukt ein Ausdruck ist. Es wird kein genaues Wissen mehr über einzelne Anweisungen benötigt, da sich ein Programm aus Ausdrücken zusammensetzt die miteinander kombiniert werden. Der Zweite Aspekte beschäftigt sich mit dem Problem, dass Funktionen für unterschiedliche Zwecke eingesetzt werden können. 3.2.1 Funktionen als Argumente Wird der zweite Aspekt aus Kapitel 3.2 betrachtet, so stellt kommt folgende Fragen zustanden: „Wie lässt sich der variable Teil einer Methode von dem immer gleichbleibenden Teil separieren?“. Die Antwort auf diese Frage ist relativ einfach: „Der gleichbleibende Teil bildet den Körper der Methode und die Parameter bilden den variablen Teil, den die Methode ausführen soll“. Eine Modifikation, an dem Beispiel SumNumbers aus Kapitel 3.1.2, könnte so aussehen, dass diese die Funktionalität erhält sämtliche Aggregationen durchzuführen, anstatt lediglich Zahlen aufzuaddieren. Folgendes Beispiel demonstriert dies: Aggregieren von Zahlen durch Rekursion (C#) public int AggregateNumbers( Func<int, int, int> op, int init, int from, int to ) { if ( from > to ) return init; int sumRest = AggregateNumbers( op, init, from + 1, to ); return op( from, sumRest ); } Zur ursprünglichen Funktion sind zwei weitere Parameter hinzugekommen – der Initiale Wert (init) und eine Operation die beschreibt wie die kommenden Zahlenwerte trans- 15 Kernkonzepte von F# formiert werden sollen. Die Operation wird in Form eines delegates übergeben, welcher spezifiziert, dass er eine Funktion mit zwei Parametern vom Typ int ist und auch den Typ int als Rückgabewert besitzt. Die Idee Funktionen als Argumente zu nutzen ist eines der nützlichsten funktionalen Konzepte. Wird angenommen, dass eine bestehenden Kundenliste sortiert werden soll, so wäre der klassische objektorientierte Lösungsweg die Sort Funktion einer Liste zu nutzen und eine eigene Sortierklasse, die das IComparer Interface implementiert, mit zu übergeben. Diese Klasse würde bestimmen wie zwei Elemente miteinander verglichen werden können. Ein nicht ganz unerheblicher Aufwand, wenn bedacht wird, dass eine Klasse für diesen Vorgang geschrieben, diese zum Einsatz instanziiert und abschließend als Argument übergeben werden müsste. Ganz davon abgesehen, dass die Lesbarkeit des eigentlichen Zieles nicht deutlich zu erkennen ist. 3.2.2 Funktionen höherer Ordnung Inzwischen wurde gezeigt, dass Funktionen wie Werte gehandhabt werden können und sogar als Parameter für andere Funktionen dienen können. Es gibt zwei wichtige Begriffe die oft genutzt werden, wenn über diese Art von Funktionen gesprochen wird: Funktion erster Klasse (First-class function) ist eine Funktion in Form eines Wertes. Man kann diese also beispielsweise als Argument für eine andere Funktion weiter nutzen. Als Ergebnis haben diese Funktionen auch einen Typ, der in C# als delegate ausgedrückt wird. Beim Aufruf der Funktion greift diese auf ihren Erstellungskontext zurück. 6 Funktion höher Ordnung (Higher-order function) bezieht sich auf eine Funktion, die wiederum eine Funktion als Argument nimmt oder als Rückgabewert besitzt. Das Beispiel aus Kapitel 3.2.1 ist beispielsweise eine Funktion höherer Ordnung. Diese Art der Parametrisierung wird in funktionalen Sprachen sehr häufig eingesetzt und hilft dabei Quellcode deklarativer zu gestalten. 7 Mit einem weiteren Beispiel in der Sprache F# sollen diese Aspekte verdeutlicht werden. Zunächst werden aus einer Liste die ungeraden Zahlen herausgefiltert und anschließend das Quadrat der jeweiligen Zahlen gebildet. 6 7 Vgl. (Microsoft, 2012) Vgl. (Lorenz, 2009) 16 Kernkonzepte von F# Verarbeiten einer Zahlenliste mit Funktionen höherer Ordnung (F#) > let numbers = [1..10] let isOrdd(n) = n % 2 = 1 let square(n) = n * n ;; val numbers : int list val isOdd : int -> bool val square : int -> int > List.filter isOdd number;; val it : int list = [1; 3; 5; 7; 9] > List.map square (List.filter isOdd numbers);; val it : int list = [1; 9; 25; 49; 81] Nachdem eine Liste mit den Zahlen von 1 bis 10 erzeugt wurde, werden zwei weitere Funktionen gestellt. Die Erste Funktion gibt zurück, ob der übergebene Wert (n) durch zwei teilbar und somit gerade oder ungerade ist. Die zweite Funktion bildet aus dem übergebenen Wert das Quadrat. Nach der Definition der Funktionen, wird eine Funktion höherer Ordnung, List.filter, genutzt, die wiederum als erstes Argument eine Funktion und als zweites Argument eine Liste einfordert. Wie anhand der folgenden Zeile zu erkennen ist, wird als Rückgabewert eine Liste mit ungeraden Zahlen geliefert. Das darauffolgende Beispiel gibt noch eine weitgehendere Idee. Es wird die Funktion List.map aufgerufen und als erstes Argument die zuvor geschriebene Funktion square übergeben. Als zweites Argument wird aber nicht etwa eine gewöhnliche vorgefilterte Liste übergeben, sondern wiederum eine Funktion die als Ergebnis eine Liste als Rückgabewert besitzt, nämlich selbige Funktion aus dem ersten Beispiel. 3.3 Typen in der funktionalen Programmierung Da funktionale Sprachen jedes Stück Code als Ausdruck werten, ist es eine eher gewagte Aussage zu sagen, dass jeder Ausdruck einen Typ besitzt. Es bedeutet, dass jedes syntaktisch korrekte Stück Code einen bestimmten Typ hat. Dieser sagt aus was für eine Art von Ergebnis erwartet wird sobald ein Ausdruck ausgewertet wird und gibt somit eine wertvolle Information über einen Ausdruck. Typen können als eine Art grammatikalische Regeln für die Zusammensetzung von primitiven Ausdrücken gesehen werden. In funktionalen Sprachen haben Funktionen 17 Kernkonzepte von F# einen Typ, genauso wie die square Funktion aus dem vorherigen Beispiel. Dieser Typ bestimmt auch gleichzeitig wie eine Funktion verwendet werden kann. Wird sie mit einem Argument vom Typ int aufgerufen, so lautet der Typ des Rückgabewertes auch int. Wichtiger jedoch ist jedoch die Tatsache, dass der Typ auch vorgibt in wie weit eine Funktion mit einer anderen Funktion genutzt werden kann. So kann die Funktion square nicht mit der Funktion List.filter verwendet werden, da die Filterung eine Funk- tion mit dem Typ boolean erwartet. Genau das beschreibt auch eine grammatikalische Regel – die Typen stellen sicher, dass Funktionen stets in einem sinnvollen Kontext verwendet werden. 18 Anwendungskonzept Pixxler 4 Anwendungskonzept Pixxler Um die Kombination von funktionalen und objektorientierten Sprachelementen möglichst praxisnah zu erproben, wurde im Rahmen dieser Bachelorarbeit ein Konzept für eine Anwendung erstellt, welche für den Rest dieser Arbeit den Arbeitstitel „Pixxler“ trägt. Die Idee dahinter ist eine multiuserfähige Anwendung die Bilder durch verschiedene Farb- und Bildfilter nachbearbeitet und in der Cloud speichert. Die Bilder werden dabei in zuvor, vom Benutzer, angelegten Alben gespeichert. Da die zustande kommenden Daten jedoch nicht lokal beim Benutzer gespeichert werden sollen, sondern in einer zentralen Datenbank, korrespondiert die Anwendung mit einem Service innerhalb eines Netzwerks. Wichtig bei der Konzeption des Projektes war auch der Gedanke der Erweiterbarkeit. Eine absolute Erweiterbarkeit kann allerdings nicht garantiert werden, denn diese ist für einen Entwickler immer mit zusätzlichem Aufwand und erhöhter Komplexität des Programms verbunden. Bei der Abwägung einen Service als Schnittstelle für den Datenverkehr und andere Dienstleistungen einzusetzen überwogen die Vorteile jedoch dem Aufwand, besonders in Hinsicht der Multiuserfähigkeit und Erweiterbarkeit der gesamten Plattform. Weiterhin soll auch die Praxistauglichkeit sowie der moderne Ansatz der serviceorientierten Architektur in Hinsicht auf funktionale Eigenschaften näher untersucht werden. 4.1 Serviceorientierte Architektur Die Idee, die hinter diesem Anwendungskonzept steckt, nennt sich serviceorientierte Architektur (SOA). Erreicht wird dadurch, dass ein oder gar mehrere unabhängige verteilte Services einen Bestandteil eines Programmes übernehmen. Eine pragmatische Standardisierung von Schnittstellen sorgt, in Verbindung mit XML als ein- Abbildung 1: SOA (Crofton, 2010) heitlichem Datenformat, für die notwendige Interoperabilität. Dadurch wird außerdem ge- währleistet, dass unterschiedliche Endanwendungen mit Hilfe dieser externen Schnittstellen auf gleiche Weise funktionieren. 19 Anwendungskonzept Pixxler 4.2 WCF Service Pixxler Das Speichern von allen gesammelten Daten in der Cloud ist ein zentraler Aspekt in diesem Projekt. Alle, von Clients erzeugten Daten, sollen in einer zentralen Datenbank gespeichert werden. Um die Datenbank selbst jedoch nach außen hin abzusichern und eine komfortable und neutrale Schnittstelle für Clientanwendungen zu gewährleisten wird ein Service mit Hilfe der Windows Communication Foundation (WCF) entwickelt. Die auf Nachrichten basierte Kommunikation zwischen den Klienten und dem Service wird dabei über das Simple Object Access Protokoll (SOAP) realisiert. Diese Nachrichten werden verschlüsselt über einen Datenstrom versendet und sind somit gegenüber Dritten weitestgehend geschützt. Um gespeicherte Daten aus der Datenbank abzufragen wird auf einen Objektrelationalen Mapper zurückgegriffen. Dieser Mapper erleichtert die Ansteuerung der Datenbank aus dem Quellcode des Services und ermöglicht den Einsatz der funktional orientierten .NET Erweiterung LINQ to Entities. Durch den Einsatz von LINQ soll außerdem gezeigt werden, welche Vorteile der funktionale Ansatz auch im Bereich der Abfrage von Daten aus einer Datenbank haben kann. Der Service dient somit insgesamt sowohl als Vermittlungsstelle zwischen Anwendungen und Datenbank, als auch als Dienstleister für andere Aufgaben wie die Authentifizierung von Benutzern. 4.3 Pixxler API Eine API soll den Zugriff auf den Service als weitere Schicht abstrahieren damit die Ansteuerung dessen nicht direkt im Quellcode der jeweiligen Endanwendung implementiert werden muss. Diese Bibliothek enthält alle vom Service angebotenen Schnittstellenmethoden, eine Struktur zur lokalen Datenhaltung sowie eine lokale Implementierung des Authentifizierungsmechanismus. Die Verwendung einer solchen Bibliothek hat den Vorteil, dass die Ansteuerung des Services auch von anderen Endanwendungen auf gleiche Weise wiederverwendet werden kann und nicht im Quellcode mehrerer Endanwendungen erneut implementiert werden muss. Es kann im späteren Verlauf sogar in Betracht gezogen werden, diese API öffentlich zugänglich zu machen und Drittanbietern von Software einen Zugriff auf den Service zu gewähren. Die API wird als eigenständiges Modul in die Endanwendung eingebunden. 20 Anwendungskonzept Pixxler 4.4 Clientanwendung Pixxler Da auch die Vorteile der deklarativen Programmierung näher vorgestellt werden sollen, wird die Benutzeroberfläche der Clientanwendung mit der, XML ähnlichen, deklarativen Sprache XAML gestaltet. Sie ist fester Bestandteil der Windows Presentation Foundation (WPF) und wird eingesetzt um grafische Elemente, Benutzeroberflächen, Verhaltensweisen, Animationen, Transformationen, Darstellung von Farbverläufen und weitere mediale Inhalte zu definieren. Die möglichst hohe Flexibilität der Anwendung wird durch eine modulare Architektur gewährleistet. Die Modularisierung der Endanwendung wird dabei durch den Einsatz der .NET Bibliothek PRISM, sowie den modernen Ansatz der Dependency Injection möglich gemacht, um auch in der Zukunft eine leichte Erweiterbarkeit möglich zu machen. Sogenannte Softwaremodule bilden dabei die eigentliche Funktionalität der Endanwendung. Folgende Module sind für die Anwendung vorgesehen: Modul für den Login Modul zur Navigation der Bilderalben und deren Inhaltsanzeige Modul zur Anzeige von Bildern und deren Bearbeitung Bei der Entwicklung der Clientanwendung wird C# als Sprache für die Anwendungsarchitektur sowie alle anderen objektorientierten Bereiche verwendet. 4.5 F# Bibliothek Die Anforderung ist, dass der mit F# implementierte funktional orientierte Teil als Bibliothek in der Clientanwendung bzw. im Service ansprechbar sein sollte. In dieser Bibliothek sollen sämtliche Funktionalitäten bereitgestellt werden, um Bilder mit verschiedenen Farb- und Effektfiltern neu berechnen zu lassen. Da der F# Code beim Kompilieren, genau wie C# Code, in den Zwischencode für die CLR8 umgewandelt wird, ist eine Verwendung dieser Bibliothek aus einer .NET Anwendung bzw. dem Service problemlos möglich. Voraussetzung dafür ist allerdings die Einbindung der allgemeinen F# Bibliothek aus dem .NET Framework in das jeweilige Projekt. 8 Dies ist die Laufzeitumgebung von .NET 21 Implementierung des Pixxler Services 5 Implementierung des Pixxler Services Der Pixxler Service stellt eine zentrale Anlaufstelle für ausgelagerte Aufgaben aller Art dar. Er ist ein elementarer Baustein für sämtliche Endanwendungen und muss somit auch als erstes konzipiert und implementiert werden. Folgende Aufgaben soll der Service in der ersten Version übernehmen: Authentifizierung der Benutzer Vermittlungsstelle für Daten aus der Datenbank Da es sich um eine multiuserfähige Anwendung handeln soll, wird eine Authentifizierung am Service notwendig. Die Abfrage benutzerbezogener Daten muss dabei innerhalb einer Sitzung ablaufen um sicher zu stellen, dass jedem Benutzer die passenden Daten zur Verfügung gestellt werden können. Nach der erfolgreichen Anmeldung soll dem Benutzer eine SessionID 9 vergeben werden, mit welcher dieser dann weitere Leistungen des Services nutzen kann. Die SessionID wird in Form einer GUID repräsentiert um die weltweite Einmaligkeit der Sitzung zu gewährleisten. Bei den Leistungen des Services handelt es sich sowohl um die Abfrage von Bildern und Alben die einem Benutzer zugeordnet sind als auch um Routinen, die zum Speichern neuer Bilder und Alben dienen. Um die Ansteuerung des Services nicht für jedes unterschiedliche Endgerät neu zu entwickeln und ggf. sogar später eine öffentliche Ansteuerung zu ermöglichen, wird für den Service eine API in Form einer .NET Bibliothek entwickelt um die Datenstrukturen, Konnektivität und das Handhaben der Sitzung für die Anwendungsentwicklung bereitzustellen. 5.1 Was ist ein WCF Service Die Windows Communication Foundation (WCF) ist eine von Microsoft entwickelte Technologie, die es Entwicklern ermöglicht Kommunikationsplattformen zu erstellen die mit standardisierten Protokollen (TCP, HTTP) arbeiten. Die WCF ist somit besonders dazu geeignet um serviceorientierte Anwendungen zu entwickeln. 9 Eine eindeutige alphanumerische Zeichenfolge die dazu dient den Klienten wiederzuerkennen 22 Implementierung des Pixxler Services Ein WCF Dienst ist dabei in folgende Bereiche unterteilt: Eine Adresse die den Ort der Erreichbarkeit in Form einer URI beschreibt Die Bindung, welche die Form der Kommunikation sowie eingesetzte Kommunikationsprotokoll beschreibt. Verschlüsselung fällt ebenfalls in diesen Bereich. Ein Vertrag, der beschreibt welche Dienstleistungen in Form von Methoden der Service anbietet. Abbildung 2: WCF - ABC Prinzip 10 Dieses ABC (Address, Binding, Contract) Prinzip abstrahiert somit das Konzept des Endpunktes. 5.2 Einrichten der Datenbank Die Daten der Benutzer müssen in einer Datenbank persistent gespeichert werden. Als Speicherort für diese Daten dient ein Microsoft SQL Server 2008 R2 mit einer Standarddatenbank. Die zu speichernden Daten sind zu Anfang sehr überschaubar und werden auf folgende Strukturen projiziert: Benutzer Alben und ihre Zugehörigkeit zum Benutzer Zugriffsrechte auf Alben anderer Benutzer Bilder und ihre Zugehörigkeit zum Album Da die Anwendung darauf ausgelegt ist von mehreren Benutzer genutzt zu werden, muss die Tabellenstruktur in der Datenbank ebenfalls benutzerbezogen gestaltet werden. Jeder Benutzer soll X Alben anlegen können und in diesen alle gewünschten Bilder anordnen können. Es soll außerdem berücksichtigt werden, dass in einer späteren Version 10 (Beaulieu, 2010) 23 Implementierung des Pixxler Services Alben mit anderen Benutzern geteilt werden können. Die Anforderungen lassen sich wie im Folgenden Tabellenschema auf die Datenbank abbilden: Abbildung 3: ER Modell der Datenbank Da ein Benutzer X Alben besitzen soll und diese mit bestimmten Rechten auch für andere Benutzer ausgestattet werden sollen ist hier eine Zwischentabelle notwendig. Der Besitzer eines Albums wird stets durch einen Rückverweis aus der Albumtabelle ersichtlich. Ein Bild selbst soll nur einem einzigen Album zugeordnet werden. Auffällig ist, dass die Datensatzschlüssel jeweils als uniqueidentifier gespeichert werden. Dieser entspricht keinem gewöhnlichen inkrementellem Primärschlüssel in Zahlenform, sondern einer GUID. Der Grund für den Einsatz eines solchen Schlüssels liegt darin begründet, dass die Erzeugung eines eindeutigen Schlüssels schon vor dem Eintragen in die Datenbank auf Programmebene gewährleistet werden kann und keine Rückfragen zur Datenbank mehr notwendig sind um zu erfahren welche ID der neue Datensatz erhalten hat. 5.3 Ansteuerung der Datenbank aus dem Code Um Daten aus einer Datenbank auszulesen gibt es in .NET mehrere Möglichkeiten. Der älteste Weg dafür ist das Hinterlegen von SQL Befehlen im Quellcode und das Ausführen dieser gegen die Datenbank. Die nötigen Klassen für diese Variante gibt es im .NET Framework schon seit der ersten Version. Wird davon ausgegangen, immer das best- 24 Implementierung des Pixxler Services mögliche SQL Statement zu schreiben so ist diese Methode auch zugleich die leistungsfähigste Lösung die möglich ist. Es spricht jedoch einiges gegen diesen Ansatz. Zum einen wäre da die Fehleranfälligkeit. Ein SQL Statement in Form eines einfachen Strings kann immer Fehler enthalten, die zugleich auch noch schwierig zu finden sein können, da ggf. keine Informationen beim Debuggen vorhanden sind wenn eine Ausnahme auftritt. Im schlimmsten Fall fällt der Fehler gar nicht auf, weil das Statement ausgeführt werden kann, jedoch das falsche Ergebnis zurückgeliefert wird. Ein weiterer Punkt ist die Strukturierung des Codes und die Möglichkeit der Wiederverwendung. Wohin soll eine solche Fülle an Strings ausgelagert werden und wie lassen sie sich möglichst einfach in anderen Teilen des Codes wieder- oder weiterverwenden? Um diese beiden Störquellen im Voraus auszuschließen und einen funktionalen Ansatz verfolgen zu können, wird in diesem Projekt auf einen objektrelationalen Mapper11 in Kombination mit LINQ zurückgegriffen. 5.3.1 Einsatz des Entity Frameworks zur Abbildung des Datenbankmodells im Code Um bei der Entwicklung auf die LINQ Technologie zurückgreifen zu können und den Zugriff auf die Datenbank zu erleichtern, wird das Entity Framework in der Version 5.0 eingesetzt. Das Entity Framework ist ein objektrelationaler Mapper von Microsoft mit dessen Hilfe sich das Datenbankschema direkt aus der Datenbank auslesen lässt und somit ein Abbild von diesem Modell im Quellcode erzeugt werden kann. Nicht nur erspart sich der Entwickler dadurch die manuelle Erzeugung von Datenobjekten sondern hat zusätzlich auch den Vorteil die Erweiterung LINQ to Entities zur Abfrage von Daten aus der Datenbank einsetzen zu können. Für dieses Projekt wurde mit Hilfe des EF nun folgendes Modell erzeugt. 11 Eine Abbildung der Datenbankstruktur im Quellcode 25 Implementierung des Pixxler Services Abbildung 4: EF Modell der Datenbank im Code Die Abfrage von Daten mittels gewöhnlicher T-SQL Statements ergibt in der Regel ein nicht typisiertes Objekt oder ein Array mit undefinierten Daten. Es liegt am Entwickler diese abgefragten Daten in Datenobjekte umzuwandeln und die einzelnen Eigenschaften zuzuordnen. Ein Vorgang der nicht nur umständlich anzupassen, sondern auch fehleranfällig ist. Wird bei diesem Vorgang mit Indizes gearbeitet und das Tabellenschema ändert sich auf Seiten der Datenbank so können abgefragte Informationen direkt zu Ausnahmen in der Anwendung oder zur falschen Weiterverarbeitung führen. Der Code des EF Modells wird bei der Aktualisierung automatisch durch T5 Skripte erzeugt. Das macht den Einsatz nicht nur weniger anfällig gegenüber Fehlern sondern auch einfacher in der Wartung des Modells. Jegliche Anpassungen werden nämlich über die automatische Codegenerierung direkt in die jeweiligen Objekten übertragen. 5.3.2 Was ist LINQ? LINQ ist eine Spracherweiterung des .NET Frameworks, welche die Möglichkeit bereitstellt Abfragen auf Datenstrukturen durchzuführen. LINQ erweitert C# zu diesem Zweck um einige Ausdrücke, die dazu führen, dass Daten aus Arrays, Listen aber auch aus XML Dokumenten oder gar relationalen Datenbanken abgefragt werden können. 26 Implementierung des Pixxler Services Diese Ausdrücke sind vergleichbar mit den Ausdrücken, wie sie bereits aus SQL bekannt sind und beschreiben immer genau was als Ergebnis geliefert werden soll. Dieser Aspekt ist auch genau der deklarative Gedanke, der in der Erweiterung steckt. LINQ Ausdrücke werden unmittelbar in den Quellcode eingebettet und kompiliert. Das hat den Vorteil, dass Fehler in Abfragen zur Kompilierzeit ersichtlich werden und nicht erst zur Laufzeit auffallen, wie es bei herkömmlichen SQL Abfragestrings der Fall ist. Desweiteren präsentieren sich die Ergebnisse einer LINQ Abfrage stets als streng typisierte Ergebnismenge. Ergebnisse aus einer Benutzertabelle werden somit in Form von Benutzerobjekten, mit den passenden Eigenschaften, zurückgeliefert. Dadurch wird eine Typsicherheit bereits zur Übersetzung garantiert. Neben XAML und den Parallel Extensions ist LINQ somit eine weitere deklarative Erweiterung die Microsoft ins Feld schickt. Ihnen allen ist gemeinsam, dass sie nicht im Detail beschreiben, wie eine Aufgabe, sondern was überhaupt zu lösen ist. In folgendem Beispiel wird das besonders deutlich: Statt anzugeben, wie die einzelnen Elemente zu sortieren sind, gibt man lediglich mit der Erweiterungsmethode OrderBy an, dass und nach welchem Kriterium sortiert werden soll. Beispielabfrage mit LINQ auf den Speicher var processes = from p in Process.GetProcesses() orderby p.ProcessName select p; Kurz und knackig und für die meisten Entwickler wahrscheinlich sehr gut zu lesen. Aber schon nachdem der Compiler sein Werk verrichtet hat, ist von der Query Comprehension nichts mehr übrig: Der Compiler übersetzt diese Abfrage nämlich in äquivalente Aufrufe der Extension Methods aus dem Namepsace System.Linq. Das sieht dann etwa so aus: Beispielabfrage mit Lambda Expressions var processes = Process.GetProcesses().OrderBy( p => p.ProcessName ).Select( p => p ); Diese Compilermagie ist dem C#- sowie dem VB.NET-Compiler spendiert worden. Die CLR hat also von LINQ keine Ahnung und musste dazu nicht verändert werden. Die 27 Implementierung des Pixxler Services eigentliche Funktionalität von LINQ steckt folglich in den Extension Methods. Diese haben nämlich die Aufgabe, den jeweiligen Teil der Query auszuführen. So wird die OrderBy-Anweisung einer Query in einen Aufruf der OrderBy-Methode übersetzt. Je nach Erweiterung wird in der Übersetzungsmethode eine andere Funktionalität bereitgestellt. Bei der eingesetzten LINQ to Entities Erweiterung werden die vollständigen Statements in T-SQL Befehle übersetzt und an die Datenbank geschickt. Beim Einsatz dieser deklarativen Erweiterung muss der Entwickler jedoch gar nicht darüber nachdenken wie seine Statements letzten Endes umgesetzt werden, denn das bleibt in einer Blackbox verborgen. An der Stelle wird Komfort also definitiv höher bewertet als das letzte Quäntchen Performance zu erzielen – was im Regelfall allerdings eher hilfreich als störend sein dürfte. 5.4 Authentifizierung eines Benutzers Um einen Benutzer am Service zu authentifizieren muss dieser eine Kombination aus Benutzername und Kennwort zum Service senden. Ein Benutzername kann dabei entweder der angemeldete Name oder die E-Mailadresse des Benutzers sein. Benutzername sowie E-Mailadresse sind somit jeweils eindeutig, d.h. dass sie in der Datenbank als UniqueKeys angelegt werden. Bei der Registrierung neuer Benutzer wird somit verhindert, dass mehrmals derselbe Benutzername oder dieselbe E-Mailadresse genutzt werden kann. Das eingegebene Kennwort soll jedoch nicht im Klartext in der Datenbank gespeichert werden. Aus diesem Grund wird es zuvor durch einen Verschlüsselungsalgorithmus in die passende Form gebracht. Der private Schlüssel bleibt dabei nur dem Service bekannt und der öffentliche Schlüssel ist frei zugänglich. Die verschlüsselte Übertragung der Daten wird dem Kommunikationsprotokoll des Services überlassen. Durch Nachrichtensicherheit werden Nachrichten mithilfe der WS- Sicherheitsspezifikation gesichert. Die WS-Sicherheitsspezifikation enthält Erweiterungen des SOAP-Messaging, durch die Vertraulichkeit, Integrität und Authentifizierung auf SOAP-Nachrichtenebene sichergestellt werden. 5.4.1 Annotation von Klassen und Methoden Die Verwendung von Attributen ist ein weiterer Bereich in modernen Sprachen, bei dem der deklarative Ansatz zur Geltung kommt. Attribute geben Entwicklern die Möglichkeit den Einsatz oder das Verhalten von Klassen, Methoden oder Eigenschaften für bestimmte Szenarien zu beschreiben. In der Fachsprache nennt man dies auch Annotati- 28 Implementierung des Pixxler Services on. So können Elemente z.B. für eine Designeroberfläche in einer Entwicklungsumgebung deklariert werden. Es handelt sich deshalb um einen deklarativer Ansatz, da lediglich beschrieben wird was von einem Element erwartet wird und nicht jedes Mal explizit programmiert werden muss wie es umgesetzt wird. Im Folgenden Abschnitt wird gezeigt wie dieser Vorteil besonders bei einem WCF Service zum Einsatz kommt. 5.4.2 Schnittstellendefinition zum Authentifizieren Ein WCF Service veröffentlicht sogenannte Serviceverträge nach außen um zu beschreiben in wie weit man mit ihm kommunizieren kann. Es wird ein Interface erstellt und mit dem Attribut [ServiceContract] versehen. Die einzelnen Methoden, die nach außen verfügbar gemacht werden sollen müssen mit dem Attribut [OperationContract] annotiert werden. Abbildung 5: Definition des Interfaces für den BenutzerService In diesem Fall wird zunächst lediglich eine Methode zur Authentifizierung benötigt. Falls die Authentifizierung erfolgt, wird dem aufrufenden Client eine gültige GUID zurückgegeben, welche gleichzeitig der ID seiner Session entspricht. Mit Hilfe dieser SessionID ist es dem Client dann möglich weitere Methoden am Service aufzurufen. Diese SessionID hilft dem Service auch zur erkennen welcher Benutzer welche Methode aufrufen möchte und ob dieser überhaupt dazu berechtigt ist. Um zu verhindern, dass ein Benutzer auf ewig angemeldet bleibt, soll die Session Zeit nach jedem Aufruf erneut 29 Implementierung des Pixxler Services auf eine vorkonfigurierte Anzahl an Minuten gesetzt werden. Nach dem Ablauf dieser Zeit jedoch wird ein erneuter Login Vorgang notwendig. Dieses System der Authentifizierung kann nicht nur für den zu entwickelnden WPF Client verwendet werden, sondern auch für eine spätere Anmeldung auf einer Webseite oder von einem Smartphone. 5.4.3 Abfragen von Daten aus der Datenbank mit LINQ Um einen Benutzer ordnungsgemäß zu authentifizieren muss ein Abgleich seiner übermittelten Daten mit denen in der Datenbank befindlichen durchgeführt werden. Sobald es jedoch daran geht auf Daten in einer Datenbank zuzugreifen sollte man sich Gedanken über folgende gegebenenfalls auftretende Schwierigkeiten machen: Doppelte Abfragen bzw. doppelter Code Ein hohes Potential an kritischen Fehlern Keine Typisierung von Daten Zentralisierung von Daten Keine Möglichkeit Codestellen einfach und schnell zu testen Einiger dieser Dinge wurden bereits zuvor im Kapitel 5.3 angesprochen. Durch den Einsatz des Entity Frameworks wurde bereits die Typisierung sowie die Zentralisierung von Daten erreicht. Um auch die restlichen aufkommenden Fehlerquellen möglichst gering zu halten, wird auf ein Entwurfsmuster zurückgegriffen welches als Repository Entwurfsmuster bekannt ist. 5.4.4 Das Repository Entwurfsmuster Das Repository Entwurfsmuster dient als Vermittlungsschicht zwischen Datenzugriffsschicht, in diesem Fall ist das EF unsere Datenzugriffsschicht, und der Domänenschicht in welcher Programmabläufe gesteuert werden. „Repository mediates between the domain and data mapping layers using a collectionlike interface for accessing domain objects. “12 Mit Hilfe dieses Musters wird also zwischen verschiedenen Schichten vermittelt. Daten werden somit zentralisiert aus einer Datenquelle abgefragt und in Form von Objektentitäten zurückgegeben. Änderungen an Objekten können ebenfalls wieder zurück an das Repository gesendet werden, so dass diese Änderungen persistent in der Datenquelle 12 (Fowler, Patterns of Enterprise Application Architecture, 2003) 30 Implementierung des Pixxler Services festgehalten werden. Die Abstrahierung dieser Daten und Domänenschicht hat folgende Vorteile: Datenlogik und Abfragen werden zentralisiert verwaltet Es existiert eine konkreter Zugriffspunkt um Unittests zu entwickeln Es ist ein flexibles Design, welches sogar bei der Änderung der Datenquelle noch angewandt werden kann Die Abfragen können auf zwei verschiedene Arten im Repository bereitgestellt werden. Einerseits können statische Abfragen bereitgestellt werden die immer bestimmte Ergebnisse liefern. Andererseits können auch Methoden bereitgestellt werden, die einem Entwickler durch Argumente noch die nötige Flexibilität ermöglichen die Abfragen zu beeinflussen. Im Folgenden Diagramm wird die Interaktion zwischen Anwendung, Muster und Datenquell deutlich. Abbildung 6: Das Repository Entwurfsmuster 13 5.4.5 Validieren eines Benutzers Abbildung 7: Das BenutzerRepository Mit Hilfe der Klasse BenutzerRepository stehen nun einige Methoden bereit um den jeweiligen Benutzer, der sich am Service anmelden möchte, zu authentifizieren. Das 13 (Team M. ) 31 Implementierung des Pixxler Services entscheidende Kriterium ob ein Benutzer authentifiziert wird, ist ob die Abfrage in diesem Repository einen Eintrag findet oder nicht. Zu diesem Zweck wird überprüft ob das übergebene Alias mit einem Benutzernamen oder einer E-Mailadresse übereinstimmt und darauffolgend ob das Kennwort passt. Abfrage des Benutzers mit LINQ to Entities (C#) public PixxlerUserDTO GetBenutzerByUsernameOrEmail( string userAlias, string encryptedPassword ) { var alias = userAlias.Trim().ToLower(); using ( var pixxlerContext = new DataModel.PixxlerDataContext() ) { return ( from benutzer in pixxlerContext.pxl_Benutzer where ( benutzer.Benutzername.ToLower() == alias || benutzer.EmailAdresse.ToLower() == alias ) && benutzer.Passwort == encryptedPassword select new PixxlerUserDTO() { BenutzerID = benutzer.IDBenutzer, Benutzername = benutzer.Benutzername, Emailadresse = benutzer.EmailAdresse } ).FirstOrDefault(); } } Was dieser Ausdruck zurückgeben soll entspricht genau den zuvor beschriebenen Anforderungen und sollte für jeden Entwickler auch ohne Kenntnisse von LINQ erkennbar sein. Würde eine solche Abfrage auf herkömmliche Weise implementiert werden, so käme eine unleserliche aufgeblähte Methode in der zunächst Daten abgefragt werden, diese daraufhin in Objekte umgewandelt und anschließend ausgewertet werden müssten zustande. In dieser Abfrage jedoch, kommt ein konkretes Ergebnis in Form eines typisierten Objektes zurück oder den Wert null falls kein Eintrag in der Datenbank gefunden wird. Der Rückgabewert der Methode ist ein sogenanntes Datentransferobjekt. Diese Objekte werden grade im Zusammenhang mit Services sehr häufig genutzt um den Datentransfer möglichst gering zu halten und genau zu beschreiben welche Daten übertragen werden sollen. Datentransferobjekte werden genau wie [ServiceContract] buten annotiert. Ein Objekt wird mit dem Attribut [DataContract] mit Hilfe von Attriannotiert um zu be- schreiben, dass es sich um ein Datentransferobjekt handelt. Welche Informationen von dieser Klasse, in Form von Eigenschaften, der Service übertragen soll, wird durch das 32 Implementierung des Pixxler Services Attribut [DataMember] deklariert. Folgende Datentransferobjekte sind im Zuge dieses Projektes entstanden: Abbildung 8: Die DataContracts 33 Funktionale Bibliothek mit F# 6 Funktionale Bibliothek mit F# Es hat sich bisher schon in einigen zuvor angesprochenen Themen erwiesen, dass die funktionale Programmierung gewisse Vorteile haben kann. Einer der Gründe warum sie jedoch besonders in den letzten Jahren wichtiger geworden ist, ist die Tatsache, dass die Parallelisierung von Prozessen sehr viel an Bedeutung gewonnen hat. Das Schreiben von Code, der auf mehrere Kerne zugeschnitten ist, ist im funktionalen Stil sehr viel einfacher als im imperativen Stil. 6.1 Parallelisierungstechniken Das wohl am meisten parallelisierte Konstrukt in der Programmierung ist die ForSchleife. Sobald Schleifendurchläufe unabhängig voneinander durchlaufen werden können, ist es sinnvoll diese in unterschiedlichen Prozessen unterzubringen. Unabhängig heißt ist diesem Zusammenhang, dass kein Schleifendurchlauf sich auf Ergebnisse oder Aufrufe eines anderen Schleifendurchlaufs stützt. 6.1.1 Parallelisieren von kleinen Codesegmenten In dem Folgenden Beispiel wird ein gegebenes Array von Elementen unscharf gemacht, indem die Elemente wild durcheinander vertauscht werden. Obwohl jeder Durchlauf auf das Array zugreift ist keiner der Durchläufe von einem anderen Abhängig. For Schleife mit Berechnung in C# Und F# for (int i=1; i<inp.Length-1; i++) { var sum = inp[i-1] + inp[i] + inp[i+1]; res[i] = sum / 3; } for i in 1 .. inp.Length - 2 do let sum = inp.[i-1] + inp.[i] + inp.[i+1] res.[i] <- sum/3 Seit dem .NET Framework 4.0 gibt es eine Bibliothek, die das Entwickeln von parallel laufenden Prozessen vereinfacht. Diese „Parallel Extensions“ sind jedoch leider nicht in früheren Ausgaben des .NET Frameworks verfügbar. 34 Funktionale Bibliothek mit F# Um den linken imperativen Teil nun zu parallelisieren, wird die Methode Parallel.For genutzt, die im System.Threading.Tasks Namensraum 14 gefunden werden kann. Als Argumente werden, wie bei einer gewöhnlichen Schleife, die Laufzeit und zusätzlich eine Action<T>, in diesem Fall in Form einer Lambda Funktion, übergeben. Der Typ T gibt den Typ des Arguments an, der an die Funktion übergeben wird. In F# wäre diese Variante etwas überladen. Es wurde somit vorher eine einfache Funktion definiert, die den Einsatz der Methode etwas vereinfacht. Hilfsfunktion (F#) let pfor nfrom nto f = Parallel.For(nfrom, nto+1, Action<_>(f)) |>ignore Diese einzelne Zeile verpackt denselben Aufruf wie er in C# erstellt wurde in eine Funktion. Es wird jedoch die Information ignoriert, ob die Schelife erfolgreich durchgelaufen ist oder nicht. Parallelisierte Schleife C# Und F# Parallel.For(1,inp.Length-1, i => { var sum = inp[i-1] + inp[i] + inp[i+1]; res[i] = sum / 3; }); pfor 1 (inp.Length-2) (fun i -> let sum = inp.[i-1] + inp.[i] + inp.[i+1] res.[i] <- sum / 3 ) Deutlich zu erkennen ist, dass diese Methode nahezu genauso einfach zu nutzen ist, wie das von der Sprache bereitgestellte for Sprachkonstrukt. Für Arrays oder anderen imperative Datenstrukturen ist es besonders sinnvoll auf diese Parallelisierung zurückzugreifen. Später wird diese Technik auch beim Schreiben der Farb- und Effektfilter genutzt. 6.2 Implementierung der grafischen Effekte Eines der einfachsten Beispiele für große Arrays von Daten ist die Repräsentation eines Bildes als ein zweidimensionales Array aus Farben. Die WPF Clientanwendung wurde so konzipiert, dass ein Bild vom Benutzer ausgewählt werden kann um dann einen gewünschten Filter darüberzulegen. Im Folgenden Abschnitt wird gezeigt wie die Filter entwickelt werden und wie man diesen Effekt auf einzelne Bereiche des Bildes anwenden kann. 14 Namensräume dienen in .NET der thematischen Gruppierung und Einordnung von Typen 35 Funktionale Bibliothek mit F# 6.2.1 Berechnungen von Farben Um grafische Effekte, wie z.B. Unschärfe oder Graustufen, als Filter zu implementieren, müssen verschiedene Berechnungen mit Farben durchgeführt werden. Zu diesem Zwecken kann der Color Typ im Namensraum System.Drawing genutzt werden. Die Farben Rot, Grün und Blau müssen jedoch separat gehandhabt werden, was nicht immer ganz komfortabel ist. Es gibt jedoch auch einen etwas einfacheren Weg diese Berechnungen in C# oder F# durchzuführen. In diesem Projekt wurde die Operatorenüberladung genutzt um einen eigenen Farbtyp zu implementieren. Wenn später die Unschärfe auf ein Bild angewendet wird, gibt es die Möglichkeit einfach Farben zu addieren und die herauskommende Farbe durch die Anzahl der Pixel zu dividieren. Der Typ SimpleColor in F# [<Struct>] type SimpleColor(r:int, g:int, b:int) = member x.R = r member x.G = g member x.B = b // Farbe mit Farbwerten 0-255 erstellen member x.ClipColor() = let check c = min 255 (max 0 c) SimpleColor(check r, check g, check b) // 2 Farben addieren static member (+) (c1:SimpleColor, c2:SimpleColor) = SimpleColor(c1.R + c2.R, c1.G + c2.G, c1.B + c2.B) // Farbwerte durch eine Ganzzahl dividieren static member DivideByInt (c1:SimpleColor, n) = SimpleColor(c1.R / n, c1.G / n, c1.B / n) // Farbwerte mit einer Ganzzahl multiplizieren static member (*) (c1:SimpleColor, n) = SimpleColor(c1.R * n, c1.G * n, c1.B * n) // Farbe mit nullen initialisieren static member Zero = SimpleColor(0, 0, 0) Der Typ ist mit dem .NET Attribut Struct annotiert. Diese spezielle Attribut meldet dem F# Compiler, den Typ als Wertetyp zu kompilieren. Es ist in diesem Fall wichtig einen Wertetyp zu nutzen, da große Arrays mit diesem Wertetyp erstellt werden und richtige Objekte auf dem Heap extrem ineffizient wären. 36 Funktionale Bibliothek mit F# Genau wie in C# werden überladene Operatoren durch statische Eigenschaften des jeweiligen Typs implementiert. Eine Ausnahme bildet in diesem Fall der Operator zum dividieren. Es ist jedoch eher typisch, dass beim Dividieren der Dividend und der Divisor vom selben Typ sind. Daher wurde diese Methode separiert. Außerdem wird so weiterhin die Möglichkeit geboten zwei SimpleColor durcheinander zu dividieren. Weiterhin ist zu beobachten, dass der Typ nicht veränderlich ist, so wie es in der funktionalen Programmierung üblich ist. Jede Operation des Typs gibt eine neue Farbe zurück. 6.2.2 Implementieren und Anwenden von Farbfiltern Zunächst wird einer der einfacheren Effekte näher betrachtet: der Farbfilter. Im späteren Verlauf wird in diesem Kapitel auch beschrieben wie ein Unschärfeeffekt implementiert wird. Ein Farbfilter selbst ändert lediglich die Koloration einzelner Pixel eines gesamten Bildes. Die Repräsentation eines Bildes in .NET geschieht mit Hilfe der Bitmap Klasse aus dem System.Drawing Namensraum. Diese Klasse ermöglicht den Zugriff auf einzelne Pixel durch die Methoden GetPixel() und SetPixel(). Diese zu verwenden wäre jedoch äußerst ineffektiv, da sie etwa dem grafischen Äquivalent entsprechen eine Datei bei jedem Zugriff zu öffnen. Daher ist es in diesem Fall wesentlich effektiver, das Bild als zweidimensionales Array direkt im Speicher zu verändern. Die Konvertierung einer Bitmap in ein zweidimensionales Array erfolgt durch die selbst implementierten Methoden ToArray2D() und ToBitmap(). Die Implementierung der Filter ist in C# und F# relativ ähnlich, jedoch wird die Nutzung der Filter in beiden Sprachen recht unterschiedlich sein, da in F# auf Funktionen höherer Ordnung15 zurückgegriffen werden kann um 2D Arrays zu manipulieren. 6.2.2.1 Graustufenfilter und Aufhellungsfilter Um einen Graustufenfilter zu berechnen wird ein oft verwendeter Mittelwert zur Berechnung genutzt, da das menschliche Auge das grüne Licht anders wahrnimmt als das Rote und das Blaue. Der Aufhellungsfilter ist sogar noch einfacher. Jede der Farben wird mit dem Faktor zwei multipliziert, was Farben außerhalb der Wahrnehmung des 15 Vgl. Kapitel 3.2.2 37 Funktionale Bibliothek mit F# Menschen erzeugt. Daher wird im Anschluss an die Berechnung die Methode ClipColor() aufgerufen um das Limit jeder Farbe einzuhalten. Graustufen und Aufhellungsfilter (F#) // Grayscale let Grayscale(clr:SimpleColor) = let c = (clr.R*11 + clr.G*59 + clr.B*30) / 100 SimpleColor(c, c, c) // Hellere Farben let Lighten(clr:SimpleColor) = (clr*2).ClipColor() Jetzt da die Filtermethoden implementiert wurden, geht es daran diese auf das jeweilige 2D Array, die Interpretation des eigentlichen Bildes, anzuwenden. Im Gegensatz zu C# besitzt F# bereits ein Modul Array2D um 2D Arrays, ähnlich wie eindimensionale Arrays, zu verarbeiten. Dieses enthält sogar die map Funktion, was die Implementierung in F# ganz besonders trivial macht. Zum besseren Verständnis des Ablaufs wird im Folgenden aber auch aufgezeigt, wie die Implementierung in C# aussehen könnte. Schreiben der Funktion zur Anwendung von Filtern (C#) public static SimpleColor[,] RunFilter (this SimpleColor[,] arr, Func<SimpleColor, SimpleColor> f) { int height = arr.GetLength(0), width = arr.GetLength(1); // Ergebnis ist ein neues Array var result = new SimpleColor[hgt, wid]; // Neue Farbe für jeden Pixel berechnen for (int y = 0; y < height; y++) for(int x = 0; x < width; x++) result[y, x] = f(arr[y, x]); return res; } Im Gegensatz dazu würde folgender F# Aufruf denselben Effekt haben. Beide Ansätze jedoch sind noch nicht parallelisiert. Schreiben der Funktion zur Anwendung von Filtern (F#) // Wende den übergebenen Filter 'f' auf jedes Element im Array an let runFilter f arr = Array2D.map f arr 38 Funktionale Bibliothek mit F# 6.2.2.2 Parallelisieren der RunFilter() Methode Zurzeit werden alle diese Aktionen noch in einem einzelnen Prozess ausgeführt. Da die eingebaute Array2D.map Funktion nicht von Haus aus parallelisiert ist, kommt hier eine eigene Implementierung zum Einsatz. In C# würde man dazu die notwendige Schleife, die jedes Element in dem Array ansteuert, über die aus 6.1.1 bekannte Parallel.For Methode parallelisieren. Der ursprüngliche Code enthält zwei Schleifen, jedoch wird lediglich die äußere der beiden parallelisiert, um die Erzeugung unnötig vieler Aufgaben zu vermeiden. Für die Anwendung des Filters in der C# Variante würde das bedeuten, dass die Äußere der beiden Schleifen wie folgt geschrieben werden würde: Parallelisierung des C# Filter ... // Neue Farbe für jeden Pixel berechnen Parallel.For(0, height, y => { for(int x = 0; x < width; x++) result[y, x] = f(arr[y, x]); }); ... Es zeigt sich, dass für die Parallelisierung des Codes in diesem Fall lediglich eine Änderung von zwei Zeilen im Code notwendig war. Das muss jedoch nicht immer so einfach wie in diesem Beispiel sein, da Änderungen an diesem Mechanismus auch immer Probleme nach sich ziehen können. Dies kann z.B. Änderungen an dem aktuellen Zustand eines Objektes betreffen. In diesem Fall wurde lediglich eine lokale Variable verändert, welche nach außen nicht zugreifbar ist. Um die Parallelisierung auch für unser F# Codestück zu ermöglichen, wird die map Funktion in einem anderen Namensraum erneut geschrieben. Dazu wird auch die bereits in 6.1.1 erzeugte Funktion pfor wieder eingesetzt um im inneren Parallel.For anzusprechen. Parallele map Funktion für das 2D Array (F#) module Array2D = module Parallel = // Im vorh. Modul Array2D wird ein weiteres Modul Parallel eingefügt // Die Funktion f soll auf alle Elemente von ‚arr‘ angewandt werden let map f (arr:_ [,]) = let width, height = arr.GetLength(0), arr.GetLength(1) 39 Funktionale Bibliothek mit F# // Neues Array als Ergebnis erzeugen let result = Array2D.zeroCreate width height // Parallelisieren der äußeren Schleife pfor 0 (width-1) (fun x -> for y = 0 to height - 1 do result.[x, y] <- f(arr.[x, y]) ) result Das eigentliche Ziel war das Erzeugen eines parallel ablaufenden grafischen Filters. Doch wurde nicht nur dieser Filter erzeugt, sondern es wurde sogar eine generell einsetzbare Funktion geschaffen, die es ermöglicht eine unbestimmte Funktion f auf jedes Element eines zweidimensionalen Arrays parallel ablaufen zu lassen. Desweiteren ist der Code generell lesbarer geworden, da der Name der Funktion für Entwickler verständlicher ist und mehr Aufschluss darüber gibt, wie sie eingesetzt werden kann. 6.2.3 Implementieren eines Unschärfeeffekts Der andere Bildeffekt, der für dieses Projekt entwickelt wird, soll nicht nur ein einfacher Farbfilter sein. Der sogenannte Unschärfeeffekt (Blur) ist in der Bildverarbeitung auch als Weichzeichnungsfilter bekannt, da er, durch das Vermischen von mehreren Pixeln, harte Übergänge weicher erscheinen lässt. Der Vorgang des Vermischens basiert dabei auf der Berechnung eines neuen Pixelwertes anhand von mehreren originalen Pixeln. Für den Filter kann zwar, wie zuvor bei den Farbfiltern, eine Pixelweise Transformation stattfinden, jedoch wird ebenso der Zugriff auf das gesamte Bild benötigt damit die Berechnung durchgeführt werden kann. Der Transformationsvorgang selbst ist nicht sehr schwer aber er zeigt sehr gut wie sich die funktionale von der imperativen Version abhebt. Unschärfeeffekt Funktion (F#) let blur(arr:SimpleColor[,], x, y) = // Überprüfen ob die Indizes passen let hgt, wid = arr.GetLength(0) - 1, arr.GetLength(1) - 1 let checkW x = max 0 (min wid x) let checkH y = max 0 (min hgt y) // Sammeln von naheliegenden Pixeln seq { for dy in -2 .. 2 do for dx in -2 .. 2 do yield arr.[checkH(y + dy), checkW(x + dx)] } |> Seq.average // Durschnittsfarbe berechnen 40 Funktionale Bibliothek mit F# Die Parameter der Funktion bestehen aus dem Bild selbst sowie den x,y Koordinaten der Pixel, die neu berechnet werden sollen. Würde diese Funktion in der imperativen Variante implementiert werden, so müsste eine veränderbare variable mit 0 initialisiert, alle nahestehenden Pixel addiert und das Ergebnis durch die Anzahl an Pixeln dividiert werden. Mit F# gibt es jedoch einen einfacheren deklarativen Ansatz, der es ermöglicht eine Durchschnittsfarbe zu errechnen. Zunächst werden erneut zwei Funktionen definiert, welche die Abgrenzungen sicherstellen sollen. Danach wird ein Sequenzausdruck genutzt, welcher eine Liste mit allen naheliegenden Pixeln zu erzeugt, um dann abschließend mit der List.average Funktion den Durchschnittswert zurückzugeben. Damit die Listenfunktion mit selbstdefinierten Werttypen, wie dem SimpleColor Typ, funktionieren kann, muss diese folgende Dinge wissen: Was ist der 0 Wert für diesen Typ Wie werden Werte addiert Wie kann ein Wert diesen Typs durch eine Ganzzahl dividiert werden Blickt man zurück auf die Implementierung des SimpleColor Typs in Kapitel 6.2.1 so lässt sich erkennen, dass bereits alle Vorausgesetzen Operationen bereitgestellt werden. 6.3 Was kann F# einem C# Entwickler bieten? F# ist bestens geeignet um einfache funktionale Konzepte und Bibliotheken zu Beginn eines Projektes zu erkunden, um diese dann später mehr in einer traditionell orientierten Weise zu implementieren. Als C# Entwickler entwickelt man Software, die in der Wirklichkeit zur Anwendung kommt. Dennoch könnte man die Vorteile von F# folgende Arten ausnutzen: F# kann sehr gut dazu eingesetzt werden um Prototypen von einzelnen Codesegmenten zu erstellen und zu testen. Mit Hilfe der interaktiven F# Konsole können somit erste Entwürfe erstellt und im späteren Verlauf auch auf C# übertragen werden. Wie es in diesem Projekt auch praktiziert wurde, kann eine F# Bibliothek aus einem C# Projekt heraus referenziert und eingesetzt werden. F# wird vollständig in der .NET Sprache kompiliert und somit gibt es keinen Grund C# zu bevorzugen. 41 Implementierung des Pixxler WPF Clients 7 Implementierung des Pixxler WPF Clients Die für dieses Projekt entwickelte WPF Anwendung ist eine Komposition mehrerer Entwicklungstechniken und .NET Technologien. Neben der WPF selbst, kommt in diesem Projekt auch die PRISM Bibliothek zum Einsatz. Sie ermöglicht das Aufteilen der Anwendung in mehrere kleine Module, was sie letztlich zu einer modularen Anwendung macht. Das Grundgerüst der Anwendung wurde bereits im Vorfeld dieses Projektes mit der Technik der Dependency Injection entworfen. In den folgenden Abschnitten soll näher beleuchtet werden, wie und warum einzelne Techniken zum Einsatz kamen und wie sie den Prozess der Entwicklung unterstützt haben. 7.1 Warum WPF? Die Entscheidung für den Einsatz von WPF fiel primär, weil sie eine deklarative Sprache für die Gestaltung von Oberflächen nutzt. Wie praktikabel und nützlich dieser Ansatz sein kann soll ebenfalls im Laufe dieser Arbeit geklärt werden. Weitere Aspekte die für den Einsatz dieser Technologie sprachen sind: Breit gefächerte Integration – bevor WPF entwickelt wurde, mussten Entwickler für Windowssysteme sehr viele unterschiedliche Technologien beherrschen um 3D, 2D, Video, Audio oder Textverarbeitung in ihren Programmen verwenden zu können. Mit WPF sind alle diese Bereiche, durch ein konsistentes Programmiermodel, abgedeckt Unabhängigkeit von der Bildschirmauflösung – WPF setzt einen Schwerpunkt auf Vektorgrafik und ist somit unabhängig von der Bildschirmauflösung. Zoomen ist somit innerhalb der Anwendung problemlos möglich. Hardwarebeschleunigung – WPF greift bei der Berechnung von Grafiken und Steuerungselementen auf die Direct3D Schnittstelle zurück. Das ermöglicht eine weichere Navigation und erhöhte Performance beim Bildaufbau. Grade in diesem Projekt ein Gewinn bei der Darstellung vieler Bilder. Möglichkeit der deklarativen Programmierung – Um den Nutzen der deklarativen Programmierung herauszustellen ist diese neue Technologie ideal. Der Einsatz der Sprache XAML ist ähnlich dem Erstellen einer Webseite mit Hilfe 42 Implementierung des Pixxler WPF Clients von HTML, jedoch ist mit XAML sehr viel mehr möglich. So können beispielsweise auch 3D Modelle mit XAML ausgedrückt werden. Große Vielfalt bei der Komposition von Steuerelementen – Steuerelemente wie Knöpfe, Comboboxen oder Menüs können in WPF miteinander kombiniert werden. Mit XAML können beispielsweise verschachtelte Menüs mit animierten Comboboxen erzeugt werden. 7.2 Architektur der Anwendung Beim Entwurf der Anwendung wird darauf geachtet, dass wichtige architektonische Prinzipien bei der Softwareentwicklung eingehalten werden. Die Trennung von Zuständigkeiten und die lose Kopplung von Programmelementen zählen zu den wichtigsten Prinzipien bei der Softwareentwicklung. Die Bibliothek PRISM fördert diese Entwicklung von flexiblen und leicht Anpassbaren Architekturen für WPF Desktop- oder Windows Phone Anwendungen in dem sie eine Komposition von eigenständig entwickelten Komponenten ermöglicht. 7.2.1 PRISM PRISM ist ein Framework, bereitgestellt von der Microsoft Patterns & Practices Group, dass die Entwicklung von Anwendungen unterstützen soll, die einerseits komplex sind aber auch leicht zusammen zu stellen sein sollen. Um dies zu erreichen werden Entwurfsmuster und Techniken verwendet um die Anwendung in einzelne modulare Teilstücke zu zerlegen. Diese Zerteilung führt zu einigen Vorteilen die sich nicht nur bei der Entwicklung sondern auch bei der Planung der Anwendung bemerkbar machen: Wiederverwendbarkeit – Sämtliche visuelle Komponenten können in anderen Projekten die ebenfalls WPF nutzen wiederverwendet werden. Somit könnte man einige bereits entwickelte Komponenten in einem Windows Phone oder Silverlight Projekt wiederverwenden. Komponenten ohne visuelle Darstellung können sogar in Projekten jeglicher Art wiederverwendet werden. Erweiterbarkeit – Erweiterungen lassen sich, insbesondere durch die lose Kopplung, sehr leicht in das bestehende Konstrukt einbetten. Abhängigkeiten unter den einzelnen Modulen sind nicht vorhanden und können somit nicht zu Konflikten führen. Flexibilität – Dadurch, dass bei dem Entwurfsmuster der IoC stets gegen Interfaces entwickelt wird, ist ein Ersetzen oder Erweitern von konkreten Implementierungen sehr 43 Implementierung des Pixxler WPF Clients flexibel und einfach möglich. Updates einzelner Module können individuell ausgeliefert werden. Das macht sogar das Veröffentlichen der Anwendung leichter Team-Zusammenarbeit – Das Entwickeln mehrerer Module zur gleichen Zeit ist möglich. Die Teams stehen sich bei der Entwicklung und Codeverwaltung nicht gegenseitig im Weg. Eine Versionsverwaltung des Quellcodes wird dadurch ebenfalls erleichtert. Fehler Toleranz – Die Qualität der Gesamtanwendung kann durch Komponententests verbessert werden. Das Testen einzelner Komponenten führt auch dazu, dass Testfälle einfacher konzipiert und durchgeführt werden können. Wartbarkeit – Die Wartung der Anwendung unterteilt sich auf einzelne Komponenten die wiederum an die zuständigen Teams weitergegeben werden können. Modularisierung – Die Modularisierung der Anwendung führt dazu, dass ggf. sogar Drittanbieter weitere Module entwickeln können. Natürlich steht den ganzen Vorteilen bei der Entwicklung der Nachteil gegenüber sich mit den benötigten Entwurfsmustern zu beschäftigen. Um die Bibliothek zu verstehen und nutzen zur können wurde auf folgende Entwurfsmuster zurückgegriffen: MVVM Command Dependency Injection (DI) Inversion of Control (IoC) Separation of Concerns (SoC) Um sich eine PRISM Anwendung vom Aufbau her besser vorstellen zu können sei gesagt, dass sie sich aus folgenden Teilen zusammensetzt: Shell – Die Shell ist das visuelle Grundgerüst der Anwendung. Sie bezeichnet das Fenster welches auf der obersten Ebene liegt und gleichzeitig als Container für alle Programmrelevanten Inhalte dient. In der Shell werden Regionen definiert die als Platzhalter für geladene Module fungieren. Regionen – Die in der Shell hinterlegten Regionen können jederzeit aus den einzelnen Modulen angesprochen und zur Laufzeit für die Anzeige neuer Ansichten beansprucht werden. 44 Implementierung des Pixxler WPF Clients Module – Diese Module machen die eigentliche Funktionalität der Anwendung aus. Module müssen jedoch voneinander unabhängig sein um die Trennung von Abhängigkeiten einzuhalten. Ansichten (Views) – Module enthalten ggf. eine oder mehrere Ansichten um einem Benutzer eine Interaktionsmöglichkeit bereitzustellen. Bootstrapper – Der Bootstrapper ist der Initialisierungskern der Anwendung. Durch ihn wird bestimmt, welche Module in die Anwendung geladen werden, welche DI Strategie verwendet wird und was vor dem Erzeugen der Shell noch getan werden soll. Abbildung 9: Modell des PRISM Frameworks 16 7.2.2 Das Architekturmuster MVVM Das Architekturmuster Model View ViewModel gilt als eine spezialisierte Version des Model View Presenter (MVP) Musters von Martin Fowler. MVVM wird bei der Ent- 16 (Team P. a., 2012) 45 Implementierung des Pixxler WPF Clients wicklung von ereignisgesteuerten Benutzeroberflächen wie z.B. WPF oder HTML5 genutzt. Das eigentliche Ziel des MVVM Musters ist die klare Trennung von Benutzeroberfläche, Datenstrukturen und ausführender Logik. Dazu teilt das Muster die Oberfläche in die folgenden drei konzeptionellen Teile auf: Model – Das Model entspricht etwa den Daten die in der View verarbeitet werden sollen. Es kann sich dabei z.B. um Daten eines Services oder um Geschäftsdaten aus einer Datenbank handeln. View – Die View entspricht der Benutzeroberfläche mit welcher der Benutzer interagiert. ViewModel – Das ViewModel ist die Zwischenschicht zwischen View und Model und dient als eine Art Kleber. Es bereitet einerseits die Daten aus dem Model so auf, dass sie an die View gebunden werden können, kontrolliert andererseits aber auch Ereignisse aus der View um entsprechende Handlungen anzustoßen. Abbildung 10: Modell des MVVM Entwurfsmusters 17 Wichtig beim Einsatz dieses Entwurfsmusters ist die Tatsache, dass die View etwas vom ViewModel wissen sollte, jedoch das ViewModel völlig unabhängig von der View sein muss. Ebenso verhält es sich zwischen ViewModel und Model. Das Model sollte keinerlei Rückreferenzen zum ViewModel haben, da ansonsten die Trennung der Zuständigkeiten verletzt wird. 7.2.3 Dependency Injection Die Dependency Injection ist, in der Objektorientierten Programmierung, eine Methode um die Abhängigkeiten, die ein Objekt besitzt, erst zur Laufzeit bereitzustellen. Die DI 17 (Wikipedia, Wikipedia MVVM Muster, 2012) 46 Implementierung des Pixxler WPF Clients ist eigentlich eine konkretere Ausprägung der Inversion of Control wie sie Martin Fowler 2004 getauft hat. „Inversion of Control is too generic a term, and thus people find it confusing. As a result with a lot of discussion with various [Inversion of Control] advocates we settled on the name Dependency Injection.” 18 Um nicht zu tief in die Methodik der DI einzutauchen, kann für den Rest dieser Arbeit einfach gemerkt werden, dass es eine Technik für die Entkopplung und Zerteilung der Anwendung ist Da die Abhängigkeiten einzelner Komponenten erst zur Laufzeit bereitgestellt werden, lassen sich einzelne Komponenten auch unabhängig von ihren Abhängigkeiten individuell kompilieren. 7.3 Struktur der Anwendung (Shell) Da sich die Anwendung aus mehreren unabhängigen Modulen zusammensetzt, ist eine Voraussetzung für die Funktionalität, dass innerhalb der Oberfläche bestimmte Regionen definiert werden in denen die Module ihre eigenen Inhalte präsentieren können. Abbildung 11: Struktureller Aufbau des WPF Clients 18 (Fowler, MartinFowler.com, 2004) 47 Implementierung des Pixxler WPF Clients So wurde die Shell in vier Bereiche aufgeteilt die sich wie folgt ergeben haben: Informationsleiste (1) - Auf der oberen Seite der Anwendung liegt die Region für Informationsinhalte. Sie dient als Platzhalter für ein oder mehrere Module die Informationen innerhalb einer schmalen Statusleiste darstellen möchten. Derzeit existiert dort eine Informationsangabe bezüglich der Konnektivität zum Service sowie der aktuellen Uhrzeit. Inhalt (2) – Im Fokus des Benutzers sollte stets der Inhalt stehen. Daher wurde dieser Bereich zentral ausgerichtet und der meiste Platz eingeräumt. Das aktuell dort hinterlegte Modul entspricht von der Funktionsweise in etwa der Inhaltsanzeige eines Webbrowsers. In mehreren Tabs können verschiedene Alben zeitgleich geöffnet werden und es können innerhalb eines Albums auch mehrere Bilder gleichzeitig geöffnet und verändert werden. Eine Navigationsmöglichkeit von Bild zu Bild ist auch innerhalb eines Tabs möglich. Menü (3) – Direkt links neben Inhaltspräsentation befindet sich die Region für das Menümodul. Das aktuelle Modul lässt dort durch eine Baumansicht die verschiedenen Bilderalben des Benutzers darstellen. Eine Verschachtelung von Bilderalben ist derzeit noch nicht möglich, da die Datenbank noch keine kompatible Struktur dafür abbildet. Weitere Menüs wie z.B. eine Ansicht für Favoriten könnten im späteren Verlauf hinzugefügt werden. Um mehr Platz für Inhalte zu erlangen, kann die Menüleiste komplett eingefahren werden. Suche (4) – Zwischen der Informationsleiste und der Kombination aus Inhalt und Menü befindet sich der Platzhalter für ein Suchmodul. Dieses Modul befindet sich derzeit noch in der Konzeption. Geplant ist hier ggf. später nach eigenen Bild- und Alben titeln oder denen von Freunden suchen zu können. Die Definition einer Region geschieht im XAML Code der Shell und wird durch das PRISM Framework weiter interpretiert. Eine Definition sieht z.B. wie folgt aus: Definition der ContentRegion (XAML) <ContentControl DockPanel.Dock="Left" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" prism:RegionManager.RegionName="ContentRegion" Margin="0,0,20,20" /> 48 Implementierung des Pixxler WPF Clients 7.4 Implementierung der Module Das Grundgerüst für die Anwendung ist nun gegeben. Wie die eigentlichen Module implementiert werden, in wie weit XAML als Oberflächensprache und F# zur Manipulation der Bildinformationen eingesetzt wurde, wird in den folgenden Kapiteln näher erläutert. Wie bereits bei der Konzeption der Anwendung in Kapitel 4.4 definiert wurde, stehen folgende Module besonders im Fokus: Modul für den Login Modul zur Navigation der Bilderalben und deren Inhaltsanzeige Modul zur Anzeige von Bildern und deren Bearbeitung 7.4.1 Anmeldung am Service Um auf eigene benutzerbezogene Daten zugreifen zu können, ist eine Anmeldung am Service nötig. Das erste Modul stellt somit eine Benutzeroberfläche zur Verfügung um eine solche Verbindung zum Service herstellen zu können. Abbildung 12: Login am Service 49 Implementierung des Pixxler WPF Clients Die View ist dabei relativ einfach gestaltet. Zwei Textboxen ermöglichen die Eingabe von Benutzername und Kennwort und ein Knopf löst ein Ereignis aus, dass die PixxlerAPI anspricht um die Anmeldung am Service anzustoßen. Abbildung 13: Login Sequenzdiagramm Über mehrere Abstraktionsebenen wird in der Datenbank erfragt ob der Benutzer als solcher in der Datenbank angelegt und somit autorisiert ist weitere Anfragen an den Service zu stellen. Ist dies der Fall so wird auf Seiten des Services eine Sitzung erstellt und dem Benutzer eine eindeutige ID für diese zurückgegeben. Wie die einzelnen Schritte für die Authentifizierung auf der Seite des Services ablaufen, wurde bereits im Kapitel 5.4 geklärt. 7.4.2 Übersicht über die Alben Das Albummodul soll dem Benutzer einerseits die Möglichkeit bieten eine Übersicht über vorhandene Alben zu erlangen, als auch die Bilder in einem Album zu betrachten. Es muss außerdem gewährleistet sein, dass ein Benutzer neue Alben anlegen sowie weitere Bilder hochladen kann. Nachdem ein Benutzer also am Service angemeldet ist, werden zunächst alle vorhandenen Alben vom Service erfragt und in einer lokalen Struktur im Speicher gehalten. Um bei der Darstellung der Inhalte eines Bilderalbums ein wenig auf die Vorteile von XAML eingehen zu können, wurde die View so gestaltet, dass Bilder in Form von Polaroids in verschiedenen Blickwinkeln dargestellt werden. 50 Implementierung des Pixxler WPF Clients Abbildung 14: Inhalte eines Albums darstellen Eine solche Darstellung von Inhalten, wäre mittel gewöhnlichem C# Code ein ungeheurer Aufwand. Was in diesem Fall jedoch dahinter steckt ist zunächst lediglich eine Liste von vier PixxlerPictureDTO Objekten. Diese ist an ein ListBox Steuerelement gebunden welches im XAML Code definiert wurde. Definition der ListBox (XAML) <ListBox Style="{DynamicResource ListBoxStyle}" x:Name="lb" ItemContainerStyle="{DynamicResource ListBoxItemStyle}" ItemsSource="{Binding Path=AlbumBilder}" ItemsPanel="{DynamicResource ItemsPanelTemplate1}" ScrollViewer.HorizontalScrollBarVisibility="Disabled" /> Eine der nützlichsten Eigenschaften von WPF ist die Möglichkeit, dass sich Oberflächenelemente selbst neu gestalten lassen, ohne dabei die eigentliche Funktionalität des Steuerelementes aufzugeben. Wie anhand des obigen Ausschnitts zu erkennen ist, wurde für jedes Element in der Liste ein Stil ListBoxItemStyle definiert. Diese Vorlage enthält den Stil für die Darstellung eines jeden Objekts in der gebundenen Liste. Die Darstellung eines einzelnen Elementes sieht in XAML grob wie folgt aus: 51 Implementierung des Pixxler WPF Clients Vorlage zur Darstellung eines Bildes (XAML) .... <Image x:Name="img" Source="{Binding Path=Bilddaten}" Height="200" Stretch="Uniform" RenderTransformOrigin="0,0"> <Image.RenderTransform> <TransformGroup> <ScaleTransform ScaleX="1" ScaleY="1" x:Name="scaleTrans"/> </TransformGroup> </Image.RenderTransform> </Image> <TextBlock Text="{Binding Path=Titel, XPath=Rotation}" Visibility="Collapsed" /> .... In Kapitel 2.3.3 wurde die Möglichkeit der Komposition von mehreren Elementen aufgezeigt. In diesem Ausschnitt wird diese nochmals sehr gut deutlich. Nicht nur wird beschrieben wie sich ein einfaches Bild präsentieren soll, sondern wird zugleich, durch einen geschachtelten Ausdruck, festgelegt wie dieses transformiert werden soll. Da diese übereinander liegende Betrachtungsweise von Bildern natürlich nur eine allgemeine Übersicht verschafft, wird über einen sogenannten Trigger, der ebenfalls in der Vorlage eingebettet ist, auch die Möglichkeit gegeben ein Bild in den Vordergrund zu stellen. Abbildung 15: Ein Bild im Detail aufrufen 52 Implementierung des Pixxler WPF Clients 7.4.3 Bearbeiten von Bildern Wird in der Vollansicht eines Bildes auf den Knopf „Bearbeiten“ oben rechts geklickt, so öffnet sich der Bildeditor. Dieser stellt auch gleichzeitig das zuletzt implementierte Modul in diesem Projekt dar. Verschiedene Funktionen wie der Farbfilter und der in Kapitel 6.2.3 entwickelte Unschärfeeffekt können auf der linken Seite genutzt werden um die vorhandenen Bildinformationen zu verändern. Abbildung 16: Ein Bild bearbeiten Die Anwendung der Bibliothek stellt sich als leicht und intuitiv heraus. Ein einfacher Referenzverweis bietet bereits die Möglichkeit auf sämtliche, in der Bibliothek erstellten, Typen und Klassen zuzugreifen. So steckt hinter dem Aufruf, ein Bild verschwimmen zu lassen, lediglich folgender kleiner Codeausschnitt. Anwendung der Bildbearbeitungsbibliothek (C#) Bitmap bildDaten = currentPicture.Bilddaten; var array2DBilddaten = Tww.Pixxler.BitmapUtils.ToArray2D( bildDaten ); var blurPic = Tww.Pixxler.ImageEffects.ParallelBlur(konvertierteBilddaten); currentPicture.Bilddaten = Tww.Pixxler.BitmapUtils.ToBitmap(blurPic); Wie in Kapitel 6.2.2 bereits erklärt, wird hier zunächst die Bitmap in ein Array des Typs SimpleColor konvertiert um eine leichtere Manipulation durchführen zu können. Die Anwendung des Unschärfeeffektes ist ein simpler Funktionsaufruf, der zwischenzeitlich auch parallelisiert wurde. 53 Fazit 8 Fazit Man könnte diese Arbeit mit den Worten schließen „So, das war funktionale Programmierung und einige Einsatzgebiete in einem objektorientierten Umfeld“, doch wäre das eine sehr wage Aussage. Genau wie bei allen Sprachen und Technologien gibt es keine klaren Abgrenzungen was möglich ist und wozu sie eingesetzt werden sollen. Mal ganz davon abgesehen, dass längst nicht alle Konzepte und Fähigkeiten von funktionalen Sprachen in dieser Arbeit behandelt wurden. Wichtig war, dass die fundamentalen Konzepte und Ideen, die hinter der funktionalen Programmierung stecken, vermittelt wurden und gezeigt wurde wie man sie anhand vorhandener Technologien, auch in der realen Softwareentwicklung, wiederfinden und einsetzen kann. Das folgende Kapitel dient als Zusammenfassung und Rückblick auf die behandelten Themen. 8.1 Zusammenfassung Die vorliegende Arbeit beschäftigt sich vorwiegend mit der Fragestellung inwieweit sich die Paradigmen der funktionalen und der objektorientierten Programmierung in der heutigen Softwareentwicklung im .NET Umfeld kombinieren lassen und wie sie sich bereits darin äußern. Eine ausführliche Einführung in die funktionale Programmierung soll dazu beitragen einen Einblick in die Konzepte und die Sichtweise des funktionalen Entwickelns und Denkens zu erlangen. Ein wichtiger Aspekt bei dieser Sichtweise ist auch die Erkenntnis, dass deklarative Programmierung eine zentrale Rolle spielt. Diese Einblicke werden durch den Entwicklungsprozess einer Anwendung bestärkt, die praxisnah darstellt in welchen Bereichen sich funktionale, deklarative und objektorientierte Programmierung bereits ausprägt. Einer dieser Bereiche ist die Abfrage von verschiedenen Datenstrukturen. Gezeigt wird hier das komplexe Kombinationen von Entwurfsmustern durch ein einfaches funktionales Konzept ersetzt werden können. Das Framework LINQ bietet durch seine deklarative Art eine Lösung an, die im Kern funktional ist, sich jedoch hervorragend in objektorientierte Strukturen einbetten lässt. Im Vordergrund steht hierbei nur noch das was und nicht mehr das wie. Dies entspricht auch gleichzeitig dem Kernkonzept der deklarativen Programmierung. Genutzt wird LINQ in dieser Arbeit jedoch nicht nur um Daten- 54 Fazit strukturen abzufragen, die sich im Speicher befinden, sondern auch um Anfragen an eine SQL Datenbank zu stellen. Ein weiteres Einsatzgebiet der deklarativen Programmierung ist die Gestaltung von Benutzeroberflächen. Es ist eines dieser Gebiete, bei dem sich die objektorientierte Programmierung schwer tut und deklarative Programmierung seine Stärken herausstellen kann. Komplexe Benutzeroberflächen äußern sich oft durch Codeklassen mit mehreren tausend Zeilen, die imperativ beschreiben, wie Elemente erzeugt, angeordnet und welche Eigenschaften ihnen zugeordnet werden sollen. Mit XAML wird in dieser Arbeit auf eine deklarative Sprache zurückgegriffen, die einfacher denn je beschreibt was angezeigt werden soll. Gestützt wird diese Sprache durch das WPF Framework. Im Zuge dieser Arbeit wurden alle Benutzeroberflächen mit XAML erzeugt um die Vor- und Nachteile dieser Technologie zu untersuchen. Im Zuge dieser Arbeit wird außerdem auf die funktionale Sprache F# zurückgegriffen um praxisnah und tiefergehend die wichtigsten funktionalen Konzepte zu beleuchten die unter anderem auch hinter LINQ und XAML stecken. Mit der Sprache F#, welche ebenso wie C# ein Bestandteil des .NET Frameworks ist, wird eine kleine Bibliothek geschrieben, die es ermöglicht aus der Hauptanwendung bestimmte Bilder nachzubearbeiten. Dabei wird auch auf Konzepte der Parallelisierung von Codesegmenten eingegangen. 8.2 Wie hat sich die funktionale Programmierung eingebracht Es wurden mehrere Szenarien betrachtet in denen funktionale Programmierung im .NET Umfeld bereits jetzt zum Einsatz kommt. Praktisch geäußert hat sich dies auch in dem Pixxlerprojekt. 8.2.1 LINQ zur Abfrage von Datenstrukturen LINQ nutzt die funktionale Idee um Datenstrukturen unterschiedlicher Art intuitiver und leichter durchsuchen und abfragen zu können. Der Einsatz lohnt sich nicht nur dahingehend, dass die Abfragen bereits zur Kompilierzeit auf Fehler überprüft werden, sondern auch hinsichtlich der Anpassung des Quellcodes sowie der Lesbarkeit. Nicht unterschätzt werden sollte außerdem der Aspekt, dass man stets mit typisierten Objekten arbeiten kann. 55 Fazit Da LINQ Statements bei der Kompilierung in .NET Code übersetzt werden, entsteht kein Nachteil wenn man diese an Stelle von For- oder Foreach Konstrukten nutzt. Anders sieht es aus, wenn man LINQ zur Abfrage von Daten aus einer relationalen Datenbank nutzt. Diese müssen vor dem Abschicken an die Datenbank zunächst in T-SQL Statements übersetzt werden, damit der SQL Server diese überhaupt interpretieren kann. Der Overhead für diesen Prozess ist allerdings so minimal, dass er auf Grund der gegebenen Vorteile vernachlässigbar ist. Es sollte jedoch stets ein Auge auf die entstehenden T-SQL Statements geworfen werden. Diese werden zwar durch die interne Interpretation zwar sehr gut optimiert, jedoch kann beim Entwickeln auch ein nicht optimales LINQ Statement mit verschachtelten Abfragen zustande kommen. Im Pixxlerprojekt kam LINQ sowohl zur Iteration von Listen als auch zur Abfrage von Daten aus der Datenbank zum Einsatz und hat sich als sehr komfortable Komponente erwiesen. 8.2.2 WPF & XAML zur Gestaltung von Oberflächen Über XAML ohne das WPF Framework zu sprechen ist wie über C# als Sprache zu sprechen ohne dabei das .NET Framework zu berücksichtigen. Nur in Kombination ergeben sie eine Einheit, die eine Entwicklung von hübschen als auch dynamischen Oberflächen ermöglicht. Durch den deklarativen Stil des XAML Codes ist es einfacher denn je, eigene Kompositionen von Steuerelementen zu entwerfen und gleichzeitig eine Verbesserung in den Arbeitsfluss eines Entwicklerteams zu bringen. Durch die eindeutige Trennung von Oberfläche und Anwendungslogik kann hier eine saubere Arbeitsaufteilung zwischen Gestaltern und Entwicklern erreicht werden. Durch die vielen neuen Möglichkeiten, wie z.B. Trigger, können Gestalter den Entwicklern sogar einen gewissen Implementierungsaufwand abnehmen, da bereits in der Oberfläche ein Verhalten festgelegt werden kann. Der deutliche Nachteil von WPF ist wohl gleichzeitig auch der positive Aspekt. Es ist ein sehr modernes und neues Framework. Das bedeutet im Klartext, dass es erst ab dem Betriebssystem Windows XP SP3 eingesetzt werden kann. Desweiteren wird eine halbwegs moderne Grafikkarte mit Unterstützung ab Direct3D 9 benötigt um die Hardwarebeschleunigung ausnutzen zu können. Man sollte außerdem damit rechnen, mehr Arbeitsspeicher als bei gewöhnlichen formularbasierten Anwendungen zu verbrauchen. Diese Tatsache macht außerdem den Einsatz von WPF in einer terminalen Serverumgebung schwierig. 56 Fazit Der Einsatz von WPF & XAML hat sich im Pixxlerprojekt aber durchaus als Gewinn ergeben. Nicht nur war die Gestaltung von hübschen Oberflächen nach einer gewissen Einarbeitungszeit ein leichtes, sondern es hat sich auch nochmal der Vorteil des deklarativen Konzeptes, bei der Gestaltung von Oberflächen, gegenüber des objektorientierten herausgestellt. 8.2.3 F# zur Implementierung von Kernfunktionalitäten Die Sprache F# selbst hat sich als sehr mächtiges Werkzeug zur Entwicklung von Kernfunktionalitäten herausgestellt. Gerade durch den Einsatz von Funktionen höherer Ordnung wird die Flexibilität dieser Sprache sehr deutlich. Auch hat sich dadurch die Parallelisierung von einzelnen Codesegmenten als sehr einfach herausgestellt. Es lässt sich jedoch nicht leugnen, dass eine ausgiebige Einarbeitungsphase benötigt wird um sowohl die Prinzipien der funktionalen Programmierung als auch sprachspezifische Eigenschaften von F# zu erlernen. F# wurde im Pixxlerprojekt eingesetzt um sämtliche Berechnungen von Farb- und Effektfiltern parallelisiert durchzuführen. Die Tatsache, dass die F# Bibliothek über eine einfache Referenz, auch aus einem C# Projekt, zugreifbar war hat den Nutzen nochmals bestärkt. 8.3 Persönliches Fazit Abschließend ist zu sagen, dass die funktionalen Aspekte in den einzelnen eingesetzten Technologien die Entwicklung sehr stark in eine positive Richtung gewiesen haben. Ich bin davon überzeugt, dass in den kommenden Jahren noch viele interessante Frameworks entwickelt werden, die ihre Wurzeln in den funktionalen Prinzipien wiederfinden. Der Einsatz von LINQ, XAML und F# hat mich, als Entwickler, maßgeblich produktiver gemacht und hat den gesamten Prozess der Entwicklung spannender und sympathischer gestaltet. Ich persönlich befürworte den Einsatz von LINQ in jedem Projekt. Die Vorteile überwiegen den nahezu kaum vorhandenen Nachteilen. Ich selbst werde im .NET Umfeld kaum noch davon abzubringen sein, diese Technologie für die bessere Lesbarkeit und einfachere Abfrage von Daten einzusetzen. 57 Fazit Der Einsatz von WPF und XAML ist sicherlich immer im Kontext zu sehen. Gerade bei der Betrachtung der Nachteile muss zuvor stets abgewägt werden ob sich die Umstellung auf eine modernere Technologie lohnt. Insgesamt denke ich jedoch, dass dieses Framework, in der Art und Weise der Umsetzung, zukunftsweisend ist. Oberflächen deklarativ zu beschreiben ist ein großer Gewinn in der Softwareentwicklung. F# ist die erste funktionale Sprache, die ich in einem nicht akademischen Einsatzkontext angewandt habe. Ich denke jedoch auch, dass die Einsatzgebiete für F# relativ speziell sind. Die Gestaltung von Hilfsframeworks könnte ich mir durchaus vorstellen, nicht jedoch die explizite Implementierung von Anwendungen. Ich persönlich würde nicht unbedingt dazu tendieren mein Entwicklerteam eine F# Ausbildung zu geben. Man sollte hinter dem ganzen auch die Tatsache betrachten, dass bei einer solchen, eher selten zum Einsatz kommenden, Sprache der Ersatz eines Entwicklers zu einem Problem werden kann. Dennoch halte ich es für ausgesprochen sinnvoll einem Entwicklerteam eine Weiterbildung hinsichtlich der Paradigmen der funktionalen Programmierung zu geben. Einige Aspekte finden nicht zu Letzt auch Anwendung in objektorientierten Programmen und viele Entwickler wissen vermutlich noch nicht einmal welches Paradigma sie gerade bei der Entwicklung einsetzen. 58 <Literaturverzeichnis 9 Literaturverzeichnis Beaulieu, J. (27. 12 2010). JFBEAULIEU.COM. Abgerufen am 26. 06 2013 von JFBEAULIEU.COM: http://jfbeaulieu.com/en/component/content/article/53-dotnet/67wcfbasics Crofton, G. (11 2010). Relentless Development. Abgerufen am 06 2013 von Relentless Development: http://relentlessdevelopment.wordpress.com/2010/03/30/wcf-overview/ Fowler, M. (2004). MartinFowler.com. Retrieved 06 14, 2013, from MartinFowler.com: http://www.martinfowler.com Fowler, M. (2003). Patterns of Enterprise Application Architecture. Addison Wesley. Freeman, E. F. (2004). Head First Design Patterns. O'Reilly. Hudak, P. (2000). The Haskell School of Expression. Hutton, G. (2002). FAQ - comp.lang.functional. Abgerufen am 29. 04 2013 von FAQ comp.lang.functional: http://www.cs.nott.ac.uk/~gmh/faq.html Laucher, A. (2010). F# in Action. Mannig. Lorenz, P. A. (03 2009). Higher-order Functions. .NET Pro , 119. Microsoft. (05 2012). MSDN First-Class-Function. Abgerufen am 06 2013 von MSDN First-Class-Function: http://msdn.microsoft.com/en-us/library/dd233158.aspx Nathan, A. (2007). Windows Presentation Foundation Unleashed. Sams. Seemann, M. (2012). Dependency Injection in .NET. Manning. Smith, C. (2009). Programming F#. O'Reilly Media. Team, M. (kein Datum). MSDN The Repository Pattern. Abgerufen am 18. 05 2013 von MSDN The Repository Pattern: http://msdn.microsoft.com/en-us/library/ff649690.aspx Team, P. a. (06 2012). MSDN PRISM. Abgerufen am 26. 05 2013 von MSDN PRISM: http://msdn.microsoft.com/en-us/library/gg406140.aspx Wikipedia. (12. 04 2012). Wikipedia MVVM Muster. Abgerufen am 14. 06 2013 von Wikipedia MVVM Muster: http://de.wikipedia.org/wiki/Model_View_ViewModel Erklärung Ich versichere, die von mir vorgelegte Arbeit selbständig verfasst zu haben. Alle Stellen, die wörtlich oder sinngemäß aus veröffentlichten oder nicht veröffentlichten Arbeiten anderer entnommen sind, habe ich als entnommen kenntlich gemacht. Sämtliche Quellen und Hilfsmittel, die ich für die Arbeit benutzt habe, sind angegeben. Die Arbeit hat mit gleichem bzw. in wesentlichen Teilen noch keiner anderen Prüfungsbehörde vorgelegen. Köln, 07.07.2013 Michael Morbach
© Copyright 2024 ExpyDoc