Startseite
Amiforce 2.1     Amiforce-News Amiforce-News Amiforce-Forum Amiforce-Forum Amiforce-Chat/IRC-Chat Amiforce-Chat/IRC-Chat Gästebuch Gästebuch Kontakt mit dem Webmaster aufnehmen Kontakt mit dem Webmaster aufnehmen

Amiblitz3
Amiblitz2(alt)
Storm Wizard
Abakus-Design
Helpguide
Toolsguide
Tipps&Tricks
Gamesfun
Links
Download
Musik

Bugfixes am Forum
Subdomains aktiviert
Counterscript entfernt
  Navigation:   Index / 
Amiforce Forum - Wie programmiert man eigentlich...?
Registrierung Häufig gestellte Fragen Suche Mitgliederliste Moderatoren und Administratoren Startseite Bugtracker Chat Irc
Amiforce Forum » Laberforum (Allgemein) » Wie programmiert man eigentlich...? » Hallo Gast [registrieren|anmelden]
« Vorheriges Thema | Nächstes Thema » Druckvorschau | An Freund senden | Thema zu Favoriten hinzufügen
Neues Thema erstellen Antwort erstellen
Autor
Beitrag
Der_Wanderer
Foren Gott




Dabei seit: März 2006
Herkunft: Karlsruhe, Baden-Württemberg
Beiträge: 3564
  Wie programmiert man eigentlich...?Antwort mit Zitat Beitrag editieren/löschen Nach weiteren Beiträgen von Der_Wanderer suchen Diesen Beitrag einem Moderator melden        IP Adresse Zum Anfang der Seite springen

In diesem Thread würde ich gerne in loser Folge meine Gedanken zur Softwareentwicklung loswerden (so etwas wie "Joel on Software" im kleinen Stil). Ich schreibe solche Sachen gerne zusammen einfach nur so für mich um Gedanken zu ordnen, aber so können auch andere davon profitieren.

Anfangen möchte ich mit einem Überblick und einer Visualisierung von Unterschiedlichen Graden der Strukturierung.

Ein Programmieranfänger versteht oft unter "wie programmiere ich xyz" oder "ich lerne Programmiersprache xyz" das bloße Auswendiglernen einer API (also Funktionsbibliothek) und danach das Aneinanderreihen derer Funktionen. Das ist aber nicht einmal die halbe Miete. Im Gegenteil. Die Kenntnis einer API ist für einen guten Programmierer zweitrangig. Ein gutes Referenzhandbuch oder eine Online Hilfe reichen. Damit der Code (und somit das Programm) gut wird, ist die Struktur wie dieser angelegt wird ausschlaggebend.
"Gut" soll hier bedeuten, das Programm läuft stabil und hat, gegeben der Arbeitszeit, die investiert wurde, einen möglichst großen Funktionsumfang bzw. Komfort.

Dabei spielt natürlich die Größe eines Projektes eine entscheidende Rolle. Je größer ein Projekt, je wichtiger wird eine gute Strukturierung.


Bei sehr kleinen Projekten wie z.B. bei einem Script das auf eine Bildschirmseite passt, benötigt man keine Struktur und man legt am besten "drauf los". Die Komplexität (~ Anzahl der Aufgaben die das Programm bewältigt) ist niedrig genug dass man das gesamte Skript im Kopf erfassen kann. Deshalb haben viele Scriptsprachen auch z.B. keine Variablentypen, sondern erlauben alles durch implizite (also nicht sichtbare, automatische) Typenumwandlung. So kommt man sehr schnell zum Ziel, muss sich nicht um Deklarationen kümmern usw.

Dass man in dem Stil aber ein größeres Projekt schneller aufgebaut hat ist ein Trugschluss. Denn sobald die Komplexität steigt, verschlingt die Fehlersuche (die bei Skripten fast immer zur Laufzeit stattfindet) immer mehr Zeit und man wird immer unproduktiver. Irgendwann werden 100% der Zeit nur noch in die Wartung des Programms gesteckt.
Die Strukturierung, die am Anfang mehr Zeit kostet und eine gewisse Planung und Vorrausschau erfordert, zahlt sich dann ab einer gewissen Größe immer aus.

Das ist quasi ein Naturgesetz, was man überall beobachten kann. Mehr Vorbereitung kostet einen initialen Aufwand, der sich aber später auszahlt.
So ist es schneller zu Fuß zum Bäcker um die Ecke zu laufen als ins Auto zu steigen, Motor zu starten, fahren, parken usw. Sobald die Strecke aber lang genug ist, lohnt sich das Auto. Weiter geht es dann mit dem Flugzeug, wo es sich sogar lohnt in die falsche Richtung erstmal zum Flughafen zu fahren, Check-in, Wartezeiten etc. Sitzt man dann aber im Flugzeug geht die Post ab.

Das gleiche kann man bei Algorithmen beobachten. Der "naive" Algorithmus (also der sofort ersichtliche) ist meistens sehr kurz und hat pro Schritt wenig zu tun, z.B. "Insert Sort", wo man die Elemente der Reihe nach anfasst und in die passende Stelle in der sortierten Reihe einfügt. Macht man sich aber die Mühe und implementiert "Quicksort", hat man pro Schritt einen größeren Aufwand, allerdings sinkt die Anzahl der Schritte nicht nur prozentual, sondern die Komplexität wird tatsächlich von quadratisch auf n*log(n) reduziert. D.h. egal wie schlecht und umständlich man Quicksort implementiert, und wie langsam die verwendete Sprache ist, ab einer gewissen Anzahl von Elementen wird es schneller sein als die super-hand-optimierte Assembler Version von Insert Sort. Das nennt man Komplexitätstheorie und das sollte jeder Programmierer verstanden haben. Liest sich in der Literatur oft kompliziert, ist aber vom Prinzip her einfach.
Dazu vielleicht später mehr.

Schauen wir uns mal an wie man sich ein Programm vorstellen kann. Die grauen Flecken sind Programm Code, die Linien sind Daten-Abhängigkeiten, also z.B. Rechenergebnisse die woanders weiterverarbeitet werden.


Das Programm hier ist völlig unstrukturiert. Das könnte z.B. ein Assembler Programm sein. Man sieht zwar Bereiche wo sich Aufgaben einer bestimmten Art häufen, z.B. GUI Funktionen, aber im Prinzip könnte man überall Fragmente finden und muss das auch erwarten. Zwischen den einzelnen Fragmenten bestehen Anhängigkeiten, die farbigen Linien, weil sie z.B. auf die gleichen Speicherzellen zugreifen oder bei einem Sprung an eine andere Codestelle Werte in Registern "mitnehmen". Da z.B. in Assembler keinerlei Zwänge gibt wann man wo welche Register mit welchen Daten lädt, wird das natürlich bei größeren Programmen schnell zur Hölle. Der Programmierer muss sehr diszipliniert sein, um ein größeres Projekt aufzuziehen. Da theoretisch alles von allem abhängen kann, müsste man das ganze Programm verstanden und im Kopf präsent haben, um daran zu arbeiten.

Der Programmierer kann zwar sein Programm auch in Assembler struktuiert aufbauen und Datenabhängigkeiten dokumentieren, da es aber keine Kontrolle seitens des Kompilers gibt, können schnell Fehler oder Inkonsistenzen (veralteter Kommentar, Aufweichung der Disziplin) auftreten.
Menschen machen ständig Fehler (wer in C noch nie vom Kompiler auf ein fehlendes ";" hingewiesen wurde hebe die Hand!), und ein Kommentar mag zwar gut gemeint sein aber verhindert keine Fehler, weil keine "Bestrafung" stattfindet.

Solche Programme werden dann meistens zur Laufzeit debugged. Das nennt man dann "Trampelpfadprogrammieren". Das bedeutet, das eigene Nutzungsverhalten und häufig genutzte Funktionen werden stabil "getrampelt" durch häufiges Testen, drum herum herrscht aber weiterhin Chaos. Andere Nutzer mit anderem Verhalten werden mit dem Programm Probleme haben, bzw. es ist instabil wenn man eine ungewöhnliche/seltene Aktion auslöst. Dazu aber später mehr.

Der nächste Schritt ist, wir teilen den Code schön säuberlich in einzelne Funktionen auf, damit klare Abgrenzungen der Codefragmente entstehen.


Da hilft es natürlich sehr, wenn die Programmiersprache "Funktionen" als Konzept anbietet. So sind zusammengehörige Codefragmente unter einem Funktionsnamen zusammengefasst. Man wird also gezwungen, die einzelnen Befehle zu gruppieren und einen Namen dafür zu vergeben.

Der Vorteil, von der Übersichtlichkeit abgesehen, ist, dass sichergestellt ist dass der Programmablauf immer oben Eintritt und irgendwann rausspringt, meistens mit einem Rückgabewert. Bei einer "offenen" Subroutine (also Programmzeilen die im globalen Kontext laufen und per Jmp, Goto oder Gosub angesprungen werden) kann man nie sagen, wo sie wirklich angesprungen wird.

Desweiteren gibt es nun die Möglichkeit der lokalen Variablen, anders als bei "offenen" Subroutinen. Der riesen Vorteil von lokalen Variablen ist, dass sie nur innerhalb der Funktion existieren und daher nichts außerhalb beeinflussen können, egal wie blöd man sich anstellt. D.h. ich kann nicht innerhalb einer Funktion die Stabilität einer anderen Funktion gefährden, es sei denn... ich benutze globale Variablen, die ich z.B. in Amiblitz per "Shared" in die Funktion hole. Das torpediert das Prinzip einer Funktion, da ihr Ergbnis jetzt nicht mehr nur von den Eingabe-Parametern abhängt, die man gut kontrollieren kann und bei jedem Aufruf sieht, sondern auch von anderen, "unsichtbaren" Quellen.

Man stelle sich vor Sin(x) hänge nicht nur von x ab, sondern noch von anderen unsichtbaren Werten die "irgendwo" definiert sind. D.h. Sin(3) gibt mal 1.1, mal 1.4 zurück, mal 1.2, je nachdem.

Wenn die Funktion solche unsichtbaren Werte auch noch ändert, nennt man das einen Seiteneffekt. D.h. die Funktion gibt nicht nur einen Wert zurück, sondern ändert noch "irgendwo anders" etwas. Das "irgendwo anders" sind die globalen Variablen oder besser gesagt der Zustand der Applikation, wenn man sie als sog. "Zustandsautomaten" betrachtet. Und den sollte man tunlichst möglich simpel halten. Denn je mehr Zustände dieser Automat hat, je länger ist das Gedächtnis, wovon das Resultat einer Funktion abhängen kann. Idealerweise sind alle Unterfunktionen "gedächtnislos", d.h. ihr Aufruf macht immer exakt das selbe, egal was vorher passiert ist, so wie Sin(x).

Wenn man also versucht, auf globale Variablen zu verzichten, dann eliminiert man Seiteneffekte.
Somit kann man sie isoliert testen und die Fehlerfreiheit sicherstellen, ohne das "grosse" Programm drumherum.
Typische Fehler, die durch Seiteneffekte hervorgerufen werden, sind sowas wie: "wenn ich im Voreinsteller war, den schließe und danach auf "play" drücke crashed es...".

Das Programm sieht dann so aus:



Man kann jetzt die Abhängigkeiten zwischen den Funktionen viel besser verfolgen und dadurch auch Funktionen leichter austauschen, optimieren etc. ohne etwas anderes zu "zerbrechen", die Wartung des Programms wird deutlich einfacher, denn Fehler durch Seiteneffekte kann man nur zur Laufzeit finden, wenn man viel herumspielt mit dem Programm, was Zeit kostet. Selten durch "Draufgucken" auf den Code, aber niemals automatisch durch den Kompiler.

Das Problem was hier aber weiterhin besteht ist, dass ich nicht auf den ersten Blick alle Funktionen, die sich mit der GUI beschäftigen, sehen kann, und oft verschmelzen auch die Grenzen weil man dazu neigt, zu viele Parameter zu übergeben, z.B. ein GUI Event einfach weiter zu geben (steht ja alles drin, für den Fall der Fälle...) anstatt es zu interpretieren und nur das "Fazit" weiterzugeben. Dadurch hängen viele der Funktionen von gemeinsamen Datenstrukturen ab und lassen sich nicht so leicht in einem anderen Programm wiederverwenden.

Wenn ich z.B. einer Funktion zum Bewegen eines Cursors das GUI Event weiterreiche, statt es zu interpretieren und nur den Befehl "bewege 2 nach links", dann hängt diese Funktion von dem GUI Toolkit ab, z.b. von NTUI. Kann also nicht mehr ohne weiteres mit Gadtools, MUI, Reaction oder sonstwas wiederverwendet werden.
Damit man gezwungen ist eine klare Abgrenzung zwischen Funktionsgruppen zu schaffen, und somit auch ihr Aufgabenbereit klar definiert ist, geht man zur Objekt Orientierten Strukturierung über.
Ich sage jetzt also, alles was sich mit der Datenbank beschäftigt packe ich in einen "Datenbank-Handler". Das ist ein "Objekt". Das soll nur dazu da sein, Daten hineinzuschreiben oder wiederaufzufinden.
Jede Funktion die damit zu tun hat bekommt als ersten Parameter das Datenbank-Handler Objekt, und danach die Parameter.
Von allem anderen soll das Objekt nichts wissen. Dann kann ich es überall wiederverwenden, egal was drumherum existiert oder nicht existiert.
Wenn die Funktion nun so stark mit einem Objekt assoziiert ist, kann ich auch den ersten Parameter weglassen und die Syntax so ändern, dass der Funktionsaufruf wie ein Feld einer Datenstruktur (was ein Objekt letztendlich ist) aussieht. Dann nennt man das eine Methode, die "auf" einem Objekt aufgerufen wird.

Wiederverwendbarkeit ist ein sehr wichtiger Aspekt um die Programmteile robust zu bekommen, da der gleiche Programmcode in mehreren Programmen läuft und Fehler somit schneller auffallen, zum anderen weil jede Neuimplementierung auch wieder neue Bugs mit sich bringt.
Ein weiterer Vorteil von OO ist, dass man gezwungen ist sich Gedanken zu machen über Teilbereiche eines Programms, wo man sonst ein großen verschmolzenen Klumpen hätte. Deshweiteren ist es nun leicht, beleibig viele Instanzen eines solchen Objektes zu erzeugen.
Brauche ich also zwei Datenbank-Handler, baue ich mir einfach zwei die dann unabhängig voneinander arbeiten können. Das erspart jede Menge fehleranfälliges "If datenbankID=1 Then..."



Man sieht nun auf den ersten Blick, wie das Programm aufgeteilt ist und kann sich mit Teilen Beschäfitgen, während man die anderen teile komplett ausblenden kann und nicht kennen muss.
Das ist sehr viel freundlicher für "fremde" Leute die sich in den Sourcecode einarbeiten müssen oder für den original Author selbst, der, nachdem genügend Zeit vergangen ist, auch selbst Fremder wird.







... to be improved...


__________________
Check out http://www.hd-rec.de !

Dieser Beitrag wurde von Der_Wanderer am 16.03.2012, 08:46 Uhr editiert.

15.03.2012, 11:40 Der_Wanderer ist offline   Profil von Der_Wanderer Füge Der_Wanderer deiner Freunde-Liste hinzu Email an Der_Wanderer senden Homepage von Der_Wanderer
  « Vorheriges Thema | Nächstes Thema »
Neues Thema erstellen Antwort erstellen
Gehe zu: