Seminar: Softwareentwicklung (Programmierstil)
WS 02/03
Johannes Kepler Universität Linz, System Software Group
Betreuer: Prof. Hanspeter Mössenböck
Autor: Christine Schiestl
Von "guter Software" wird häufig erwartet, dass sie mit möglichst wenig Aufwand fast überall und
in beinahe jeder Situation stabil läuft. Portabler Code versucht diesem Wunsch nachzukommen, indem so
implementiert wird, dass ein Programm möglichst einfach an unterschiedliche Betriebssysteme,
Benutzergruppen, kulturelle Gegebenheiten (zum Beispiel Sprache) usw. angepasst werden kann.Um dieses Ziel
realisieren zu können ist bereits in der Spezifikations- und Entwurfsphase, aber vor allem bei der
Implementierung, einiges zu beachten. Legt man zusätzlich bezüglich Lesbarkeit, Überschaubarkeit,
Wartbarkeit ... des Codes auf einen "guten Programmierstil" wert, können sich diverse Konflikte ergeben.
Diese Seminararbeit behandelt daher im Wesentlichen, was unter Portabilität von Software genau verstanden
wird, welche Probleme sich dabei ergeben können und wie diese gelöst werden können ohne dass der
Programmierstil wesentlich darunter leidet.
Portabilität ist eine wünschenswerte Eigenschaft bei vielen Softwareprodukten, denn sie verlängert einerseits den Lebenszyklus und ermöglicht andererseits einen breiteren Einsatz der Software [MoJa93]. Die Definition von James Mooney gibt einen guten Einblick, warum das so ist.
"A software unit is portable (exhibits portability) across a class of environments to the degree that the effort required to transport and adapt it to a new environment in the class is less than the effort of redevelopment." [MoJa01]
Es zahlt sich also nicht aus, portable Software gänzlich neu zu entwickeln, wobei mit "Software" Subsysteme, ganze Systeme, aber vor allem Programme / Applikationen und Komponenten gemeint sind. Anstelle einer Neuentwicklung kann die vorhandene Software auch physisch in eine neue Umgebung gebracht, also transportiert, und entsprechend den geänderten Bedingungen modifiziert, bzw. adaptiert, werden. Transport und Adaption sind somit die Hauptphasen bei der Portierung, wobei ein Transport mit Hilfe von transportablen Medien wie CDs erfolgen kann, oder direkt durch eine Übertragung über ein Netzwerk. Es müssen jedoch in beiden Fällen mögliche low-level Inkompatibilitäten in den Datenformaten berücksichtigt werden. [MoJa01]
Um weitere Portabilitätsprobleme verstehen zu können, muss man die Beziehung zwischen Software und ihrer Umgebung verstehen [MoJa94], weil ja die Software-Einheit in eine neue Umgebung portiert werden soll. In der Definition ist dabei von einer Klasse von Umgebungen die Rede. "Environment" bezeichnet in diesem Zusammenhang eine Sammlung aller bedeutenden externen Elemente mit denen das Programm interagiert, wie zum Beispiel andere Software Einheiten, das Betriebssystem, die Hardware, aber auch User, Programmierer und die Dokumentation. Ein Programm wird jedoch höchstwahrscheinlich nicht nur in eine ganz spezielle Umgebung, sondern in mehrere sehr unterschiedliche Umgebungen (verschiedene Betriebssysteme, User aus unterschiedlichen Ländern usw.) portiert. Da ein portables Design nicht alle denkbar möglichen Umgebungen abdecken kann, sagt Portabilität nur in Hinblick auf eine Klasse von Umgebungen, innerhalb der portiert wird, etwas aus, wobei eine Klasse durch eine Menge von gemeinsamen Eigenschaften charakterisiert ist. [MoJa01]
Bedenkt man, wie viele verschiedene Technologien im Bereich von Hardware und Software es gibt und wie schnell sich diese entwickeln, ist die Wahrscheinlichkeit einer Portierung der Software sehr hoch. Plant man Portabilität von Anfang an ein, können die Kosten einer späteren Portierung und eventuell auch die Gesamtkosten, die entstehen solange die Software im Einsatz ist, reduziert werden.
Die Entwicklung neuer Software startet für gewöhnlich mit einer eingehenden Analyse der Anforderungen. In weiterer Folge werden ein oder mehrere Spezifikationen festgelegt. Auf diese Weise können Funktionalität und andere Eigenschaften der Software identifiziert werden. Es gibt viele unterschiedliche Methoden um zu einer Spezifikation zu gelangen. Unabhängig davon sollten in Hinblick auf Portabilität vor allem folgende Richtlinien beachtet werden:
So sollten keine unnötigen Systemabhängigkeiten eingebaut werden. Ein Beispiel wäre:
"Ein Objekt wird durch zweimal klicken mit der rechten Maustaste innerhalb von 0,5 Sekunden innerhalb eines Kreises mit 10 Pixel Radius um den Mittelpunkt ausgewählt. ..."
Die Abhängigkeiten, dass ein Bildschirm eine gewisse Auflösung oder eine Maus mehrere Tasten haben muss, sind nicht notwendig. Besser und ohne unwichtige Details ist die Aussage:"Ein Objekt kann durch die Wahl eines passenden Werkzeugs ausgewählt werden."
Die meisten Systemabhängigkeiten entstehen in Zusammenhang mit User Interfaces, aber nicht alle. So besteht beispielsweise keine Notwendigkeit dafür, dass Integer durch exakt 32 Bits repräsentiert werden, wenn sich nur einige Werte zwischen 0 und einer Million bewegen.
Es ist sinnlos zu spezifizieren, dass die Portierung eines Programms in jede neue Umgebung einfach sein soll,
da dieses Ziel weder testbar, geschweige denn realisierbar ist. Denn wie in der Einleitung erwähnt, macht
Portabilität nur in Hinblick auf eine Klasse von Umgebungen Sinn. [MoJa97]
Eine Schwierigkeit besteht darin, dass ein wirklich portables Design Portierbarkeit für Umgebungen
erahnen muss, deren genaue Charakteristiken momentan noch gar nicht bekannt sind!
[MoJa01]
Da Portabilität häufig in Widerspruch mit anderen Zielen wie hohe Performance, effizienter
Ressourcenverbrauch oder geringe Entwicklungskosten steht, sollte in der Spezifikation zudem auch festgestellt
werden, wie viel Portabilität überhaupt notwendig ist. [MoJa97]
Portabilität bedeutet zusätzlichen Aufwand. Um die Frage zu klären, ob es sich vielleicht
gar nicht lohnt in Portabilität zu investieren, ist eine Metrik hilfreich, wie sie in einem eigenen
Kapitel ansatzweise beschrieben wird.
Bei der Anforderungsanalyse und der Spezifikation stehen vor allem der Benutzer, seine Bedürfnisse und seine Sicht im Mittelpunkt. [ChGe00] Bei Portabilität in Zusammenhang mit unterschiedlichen Benutzergruppen ergeben sich Probleme, die deshalb bereits in diesen Phasen der Entwicklung Beachtung finden sollten. Beispiele dafür gibt es viele. [MoJa94]
|
01.02.2002 $ / € / ... garçon |
In dieser Phase identifiziert man eine Sammlung von Elementen (Prozeduren, Datenstrukturen, Objekte etc.) die bei der Implementierung verwendet werden und definiert eine passende Einteilung der Elemente in Module. Das Ergebnis des Designs ist eine Sammlung von interagierenden Modulen die über Schnittstellen miteinander kommunizieren.
Da ein Softwareprodukt immer in eine spezielle Umgebung eingebettet ist, müssen nicht nur die
softwareeigenen Schnittstellen zwischen den Modulen, sondern auch jene, über die die Software mit ihrer
Umgebung interagiert, reflektiert werden. [MoJa97]
Sind viele Schnittstellen innerhalb der einzelnen Umgebungen der Klasse gleich, kann ein hoher Grad von
Portabilität erreicht werden, da dort kein Aufwand zum Anpassen der Software notwendig ist. Allgemein
akzeptierte, eindeutige und vollständige Standards für betroffene Schnittstellen helfen in Folge
dessen viele Probleme in Zusammenhang mit Portabilität zu lösen. [MoJa01]
Sollten Zugriffe auf Schnittstellen zu diversen Zielumgebungen variieren kann es notwendig sein, diese
Zugriffe in wenigen Modulen zu isolieren. Operationen auf spezielle I/O Geräte werden beispielsweise
häufig in einem Modul gekapselt. [MoJa97]
Mit entsprechenden Modellen können die jeweiligen Schnittstellen dargestellt werden. Auf diese Weise können Möglichkeiten gefunden werden um eine bessere Portabilität der Software zu erreichen.
|
Dieses Modell zeigt beispielsweise die wichtigsten Schnittstellen eines Programms oder einer Komponente die üblicherweise während der Ausführung eine Rolle spielen [MoJa01]. Es lässt aber einen wesentlichen Aspekt unberücksichtigt, nämlich wie sich die Schnittstellen mit der Zeit verändern [MoJa94]. |
Soll die Software auf unterschiedlichen Zielsystemen laufen, ist für das Design entscheidend, ob man entweder
|
|
| Denn durch #ifdef Statements mixt man Kompilezeit-Kontrollfluss und Laufzeit-Kontrollfluss, wodurch der Code schwer zu lesen und damit auch komplizierter zu warten wird. Modifikationen am Programm gestalten sich dadurch ebenfalls schwieriger. Schließlich müssen Änderungen in einem #ifdef - Block auch in den restlichen Blöcken nachgezogen werden. Es ist nicht leicht ein solches Programm konsistent zu halten und fast unmöglich vollständig zu testen, da es in separat kompilierte Programme geteilt wird. [KePi99] |
#ifdef MAC |
Benötigen verschiedene Systeme doch unterschiedlichen Code, sollten die Unterschiede in getrennten Files verwaltet werden. Dadurch entsteht ein übersichtlicheres Design.
| Interfaces können portablen von nicht-portablem Code abgrenzen und Systemabhängigkeiten verstecken. So liefern zum Beispiel die I/O - Bibliotheken häufig eine Abstraktion des Speichers in Form von Files, die geöffnet und geschlossen werden. Die wahre Realisierung (wo liegen die Werte im Speicher, wie ist ihre Struktur aufgebaut usw.) bleibt jedoch verborgen und kann je nach System variieren. [KePi99] |
|
int open(const char *Pfad,int OModus[unsigned SModus])
int read(int Fd, void *Puffer, unsigned n)
int close(int Fd) [WeMa95]
Die meisten Entwickler setzten bei der Implementierungsphase an um ihre Software portabel zu machen. Folgende Prinzipien sollten dabei beachtet werden:
Das bedeutet, dass die Sprache möglichst gut standardisiert und weit verbreitet sein sollte. Trotzdem können sich eventuelle Konflikte mit anderen Standards, die während der Design-Phase identifiziert wurden, ergeben. Jede Sprache enthält aber auch selbst Elemente, die zu Schwierigkeiten bezüglich Portabilität führen können. Denn ein Standard kann eine Sprache nie vollständig definieren. Selbst wenn man sich bei der Implementierung immer an den Standard hält, kann es zu Portabilitätsproblemen kommen. So können unterschiedliche Implementierungen zwar richtige aber inkompatible Ergebnisse liefern. [MoJa97]
Die Byte-Ordnung ist in C und C++ innerhalb von short, int oder long beispielsweise nicht definiert. Das Byte mit der niedrigsten Adresse kann das meist signifikante oder am wenigsten signifikante Byte enthalten. Das hängt stark von der Hardware ab. Probleme treten daher vor allem dann auf, wenn Werte von einem Computer geschrieben und vom nächsten gelesen werden.
|
Für den Datenaustausch sollte deshalb eine fixe Byte-Ordnung verwendet werden, wobei folgende
Vorgehensweise denkbar wäre: Mit dem Rechts-Shift wird das Byte höherer Ordnung geschrieben, mit dem Bitweisen-UND das Byte niedrigerer Ordnung. Das Zurücklesen funktioniert analog. Allerdings müssen sich Sender und Empfänger der Daten über die Anzahl der Bytes pro Objekt und die Byte-Ordnung einig sein. Die Kosten, die dabei entstehen, sind im Verhältnis zum I/O Prozess selbst relativ gering. Außerdem kann so auf #ifdef verzichtet werden. |
unsigned short x;
unsigned short x; |
Java ist eine höhere Programmiersprache als C oder C++ und versteckt die Byte-Ordnung völlig. Die Bibliothek enthält für diesen Zweck das Interface "Serializable".
In C und C++ muss die Reihenfolge der Auswertung von Operanden von Ausdrücken nicht immer gleich sein. Das zeigen folgende Beispiele:
n = (getchar() << 8) | getchar();
ptr[count] = name[++count];
printf("%c %c\n", getchar(), getchar());
Java ist da wesentlich strikter in der Reihenfolge der Auswertung. Ausdrücke, die Seiteneffekte hervorrufen können, müssen in einer bestimmten Reihenfolge (von links nach rechts) ausgewertet werden. Trotzdem soll man das Anwenden dieser Regel vermeiden, zumindest, wenn die Möglichkeit besteht, dass das Programm in eine andere Sprache konvertiert werden soll. Die Portierung in eine andere Programmiersprache ist zwar ein extremer Test von Portabilität, kann aber unter Umständen Sinn machen. [KePi99]
Die Größe von Basistypen ist in C und C++ nicht genau definiert, sondern nur, wie viele Bits sie mindestens haben müssen und in welcher Relation sie zueinander stehen. Dadurch kann es passieren, dass man möglicherweise in einer falschen Annahme zu wenig Speicher reserviert. [ScHe01] Nicht so Java - dort sind die Größen aller Basistypen definiert.
Folgendes C Programm auf mehreren Compilern ausprobiert ergab sowohl " 1 2 4 4" als auch "1 2 2 4" als Ergebnis:
int main(void){
printf("%d %d %d %d", sizeof(char),
sizeof(short), sizeof(int), sizeof(long));
return 0;
}
Der Basistyp char kann in C und C++ signed oder unsigned sein. Das hat ua. Auswirkungen in Zusammenhang mit der Methode "getchar()", die einen Integer-Wert liefert. "getchar()" liefert bei EOF den Wert -1. Ist char unsigned, wird im auskommentierten Fall der Wert von EOF in unsigned char konvertiert und man erhält als Ergebnis 255. Der Vergleich in unserem Beispiel muss dann also immer "false" sein. Besser ist daher für Vergleiche mit EOF den Wert wie im nicht auskommentierten Teil in einer int-Variable zwischenzuspeichern.
Int i;
Char s[MAX];
for (i=0; i<MAX-1; i++) {
//if ((s[i]=getchar()) == EOF) break;
if ((c=getchar()) == EOF) break;
s[i] = c;
}
s[i] = '\0';
Die Reihenfolge, in der die Elemente eines Structs im Speicher liegen, ist definiert. Die einzelnen Elemente können jedoch zum Beispiel je nach Compiler verschieden groß sein. Man erhält daher unterschiedliche Ergebnisse, wenn man versucht die Größe des Structs (links) auf diese zwei Arten zu ermitteln. Die erste Form ist wahrscheinlich die Gewollte.
|
Struct X{ char c; int i; }; |
sizeof(Struct X) sizeof(char)+sizeof(int) |
Grundsätzlich sollte man mit dem Strom schwimmen und nur jene Features der Sprache verwenden, wo die Sprachdefinition eindeutig ist und gut verstanden wird. Sobald es sehr schwierig ist ein Konstrukt zu verstehen, sollte man davon ablassen.
Portabilität ist auf die Verwendung in der Zukunft ausgelegt. Daher sollte man "veraltete" Funktionen vermeiden, auch wenn jetzige Compiler sie noch verstehen müssen. Das erhöht die Sicherheit des Codes und die Wahrscheinlichkeit auch in Zukunft eine lauffähige Anwendung zu haben. [KePi99] Ein Beispiel aus der Java-API zeigt eine so gekennzeichnete Funktion.
java.util.Date
int getDay()
Deprecated. As of JDK version 1.1, replaced by
Calendar.get(Calendar.DAY_OF_WEEK)
Header und Libraries erweitern die Basissprache. Genau genommen sind sie jedoch nicht Teil der Sprache. Aber
sie werden gemeinsam mit der Sprache entwickelt und man erwartet, dass sie von Programmierumgebungen, die
diese Sprache unterstützen, bereitgestellt werden. Auch Bibliotheken können nicht-Portabilität
enthalten, wenn sie zum Beispiel Features verwenden, die nicht im Standard definiert sind.
Trotzdem kann man in der Regel davon ausgehen, dass ein Programm, das nur Standard-Library-Funktionen benutzt,
auch auf anderen Systemen das gleiche Verhalten zeigt. [KePi99]
Portabel zu denken ist wohl der wichtigste Grundsatz wenn portable Software entwickelt werden soll. Bei der Implementierung sollte man sich stets der Problemantik bewusst sein und darauf achten, ob sich vielleicht diverse Konflikte ergeben können.
Metriken sind hilfreich,
Ein mögliches Maß dafür, wie portabel eine bestimmte Software ist, ergibt sich (entsprechend der Definition), indem man die Kosten für die Portierung in Relation zu den Kosten für eine Neuentwicklung setzt. In einer Formel wird der Grad der Portabilität für eine Software-Einheit su folgendermaßen ausgedrückt:
DP(su) = 1 - (Cport(su,e2) / Crdev(req,e2))
Auf diese Weise erhält man einen Wert, dessen Maximum bei 1 liegend perfekte Portabilität repräsentiert. Kosteneffizient ist Portabilität nur, wenn der errechnete Wert größer 0 ist.
Die Kosten für Softwareentwicklung allgemein, für eine bestimmte Umgebung e1 und eine Spezifikation req, setzen sich (vereinfacht) aus den Kosten für Design (Cdes), Codierung (Ccod), Test und Debugging (Ctd) sowie der Dokumentation (Cdoc) der Software zusammen. Bei portablem Code kommen noch die Kosten für die Portabilitätsanalyse (Cpa) hinzu.
Cdev(req,e1) = Cdes(req) + Ccod(req,e1) + Ctd(req,e1) + Cdoc(req,e1)
Cdevp(req,e1) = Cdev(req,e1) + Cpa(req)
Die Kosten für Portierung sollen gegen die Kosten für Neuentwicklung abgewogen werden. Die
Berechnung dafür erfolgt analog zur vorhergehenden Formel.
Durch Erfahrung, Wiederverwendung von Komponenten etc. sind diese Kosten in der Regel kleiner als
ursprünglich.
Crdev(req,e2) <= Cdev(req,e1)
Um eine Software zu portieren wird sie soweit als nötig modifiziert (Cmod), in der neuen Umgebung getestet und Fehler entfernt (Cptd), sowie die Dokumentation (Cpdoc) ergänzt. [MoJa93]
Cport(su,e2) = Cmod(su,e2) + Cptd(req,e2) + Cpdoc(req,e2)
Perfekte Portabiliät wäre gegeben, wenn das Programm gänzlich ohne neue Kosten portiert werden kann. In der Praxis ist das aber kaum der Fall. [MoJa97]
Portierbarkeit ist nicht stets der beste Weg in der Softwareentwicklung. Es ist auch nicht immer leicht alle
Aspekte der einzelnen Umgebungen sauber unter einen Hut zu bekommen. Erschwerend kommt hinzu, dass kaum
Methoden, Referenzen, Metriken etc. existieren, die dabei helfen können. Selbst in einschlägiger
Literatur findet man oft nur Empfehlungen. In der Praxis wird Portabilität daher meist durch
"ad hoc"-Lösungen eingebaut.
Zudem verursacht, Portablilität in Software einzubauen, Kosten. Zur Entwicklung ist mehr Aufwand
nötig und wahrscheinlich sinkt die Qualität der Software durch:
Doch auch wenn die Vorteile von portabler Software erst in der Zukunft [MoJa94] und nicht bereits bei der Implementierung offensichtlich sind, sind sie nicht von der Hand zu weisen:
Die Entscheidung ob und vor allem wie portiert werden soll, ist daher sorgfältig zu überlegen.
| [BlLi01] | Blair, Liz: "Build to Spec!", Reprinted from Java Developer‘s Journal, 2001 |
| [KePi99] | Brian W. Kernighan, Rob Pike: "The Practice of Programming", Addison Wesley, 1999 |
| [ChGe00] | Chroust, Gerhard: "Software Engineering 1; Methoden, Vorgehen, Werkzeuge", Skriptum, 2000 |
| [MoJa93] | Mooney, James D.: "Issues in the Specification and Measurement of Software", Technikal Report, 1993 |
| [MoJa94] | Mooney, James D.: "Portability and Reusability: Common Issues and Differences", Technikal Report, 1994 |
| [MoJa97] | Mooney, James D.: "Bringing Portability to the Software Process", Technikal Report, 1997 |
| [MoJa01] | Mooney, James D.: "Developing Portable Software", Tutorial, 2001 |
| [ScHe01] | Schildt, Herbert: "C ent-packt", mitp-Verlag, 2001 |
| [WeMa95] | Weissenböck, Martin: "Borland C++ (Version 3.1)", Schriftenreihe der ADIM, Band 50, 1995 |
| [WiDa98] | Williams, David: "C++ portability guide", Version 0.8, 1998 |