Framework Veröffentlichung
StartSeite | Neues | TestSeite | ForumSeite | Teilnehmer | Kategorien | Index | Hilfe | Einstellungen | Ändern
Wenn ein Framework veröffentlicht wird, so kann sich für die veröffentlichende Firma früher oder später das Problem darstellen, daß Fehler entstanden. Wie geht man damit um? Wie geht Ihr damit um?
Konkretes Beispiel: In einer Klasse gibt es eine Methode, weshalb die Anwender des Framework sich schon oft beschwert haben. Diese Methode handelt nicht nur falsch, sondern vollständig entgegen aller Erwartung: sie tut das Gegenteil von dem, was sie vorgibt zu tun. Es steht zwar in der Doku drin, daß sie anders implementiert wurde, aber meint Ihr, folgender Code der SpracheJava wäre sinnig?
| /* falsch angewendet laut Doku */
parent.isParent(child) // false
/*richtig angewendet laut Doku */
child.isParent(parent) // true |
|
|
Nun sind sich alle Entwickler des Frameworks einig, daß man das so nicht lassen kann (ist ziemlich peinlich). Wie aber kann man das ändern? Vielleicht mögen einige hier ein wenig Erfahrung Kund tun? Mein Dank sei jenen gewiss! -- BerndSchiffer
Wie wäre es in der Version 1.0.1 folgende Änderung vorzunehmen?
| /** @deprecated */
boolean isParent(Object parent);
boolean isParentOf(Object child);
boolean isChildOf(Object parent); |
|
|
-- gR
- Ja, diese Idee hatten wir auch. Allerdings gibt es da das Problem, daß es einen optimalen Bezeichner für eine Methode (isParent()) gibt, welche gewisse Herren des Managements nicht aufgeben möchten. Somit steht diese einfache und effektive Methode in diesem Fall nicht zur Debatte. Leider! -- bs
- Warum hast Du bei Deiner Änderung gleich aus einer Methode zwei gemacht? Bestimmter Grund? -- bs
- Eigentlich nicht, die Methode isParent wird durch die Methode isChildOf ersetzt. Optional kann man die inverse Methode isParentOf anbieten. Wenn man beide anbietet, sollte die eine einfacher Aufruf der anderen sein wie in folgendem Code. Übringens, dass der Methodenname isParent nicht optimal ist, wird schon dadurch ersichtlich, dass er oft missverstanden wird. Die Authoren verstehen sie als child.isParent(parent), die Benutzer als parent.isParent(child). Namen, die zu solchen Missverständnissen führen sind weniger als optimal. -- gR
| /** @deprecated Use #isChildOf instead.*/
boolean isParent(Object parent) {
return isChildOf(parent);
}
boolean isParentOf(Object child) {
return child.isChildOf(this);
}
boolean isChildOf(Object parent) {
// hier kommt die alte Implementierung von isParent
} |
|
|
- Super, danke Dir, das ist eine Lösung, die mir sehr gut gefällt. Bleibt noch abzuwarten, wie gut ich andere Instanzen davon überzeugen kann, das genauso zu sehen :-) --bs
- Eine Frage hab ich aber noch: Gesetzt dem Fall, daß dies nun nicht ein Problem des uneindeutigen Methodennamens gewesen wäre, sondern eine (gut benannte) Methode sich falsch verhält und die Anwender diesen Fehler akzeptiert haben und auch mit ihm rechnen. Dann könnte man doch die Methode, die gut wäre, wenn sie korrekt implementiert worden wäre, nicht einfach deprecated auszeichnen... - wie also vorgehen? --bs
- Das ist aber andere interessante Frage. Was Du beschreibst, ist eine Situation, in der ein Fehler nicht schnell genug von den Frameworkentwickler erkannt und beseitigt wird und die Benutzer sich an den Fehler so gewöhnen, dass sie sich auf das Verhalten sogar verlassen. Zuerst möchte ich sagen, dass ein Entwicklungsprozess, dar sowas zulässt, ein Problem hat und man sollte sich Gedanken machen, wie man den Prozess verbessert.
- Nun aber zu dem schon existierenden Bug aus dem Feature wurde. Ich betrachet folgende Ansätze als sinnvoll:
- Semantik neu definieren: Deklariere, dass der Bug ein Feature ist. Ändere die Dokumentation der Funktion, ändere die Tests, wenn der Name nicht passt, änderen den Namen (eventuell mit eine Deprecated-Überganszeit), wie oben besprochen. Dies geht natürlich nur dann, wenn der Bug/Feature konsistent verwendet werden kann.
- oder
- Korrigiere den Bug: Korrigiere den Bug. Die Benutzer haben Pech, sind aber selber Schuld, wenn sie ProgrammingByCoincidence? betreiben. Die Frameworkentwickler haben denen das Verhalten des Bugs nicht zugesichert, im Gegenteil. Wenn man zuläst, dass die entwickelte Software sich auf Bugs verlässt, wird sie irgendwann sowieso unter eigenem Gewicht kollabieren. Um den Benutzern des Framework das Leben zu erleichtern, immerhin sind die Frameworkleute mitverantwotlich, denn das FW hat zu lange den Bug "bereitgestellt", kann man eine Übergansphase mit Deprecations einplanen. Stell also die richtige Funktionalität zur Verfügung und mach die alte falsche Deprecated. Nachdem die SW, die das Framework verwendet, umgestellt wurde, beseitige den Bug endgültig.
- In keinem Fall sollte man den Bug unbeachtet lassen, es sei den, man ist mit einer Slumarchitektur zufrieden --gR
Ich weiß, dass folgendes nicht mit der von dir angestrebten Problemlösung zu tun hat, aber: warum verwendet ihr überhaupt solche IsParent- oder IsChild-Methoden und nicht statt
| /* Codebeispiel 1 */
if(child.isChildOf(parent)) {
...
} |
|
|
nicht einfach
| /*Codebeispiel 2 */
if(child.getParent()==parent) {
...
} |
|
|
die ohnehin zum Grundrepertoire des Objektes gehören muss? Auf den ersten Blick erscheint mir IsChild or IsParent als ein überflüssiges Interface. -- HelmutLeitner
- Stell Dir vor, aus irgendeinem Grund wollen wir die Implementierung der Knoten so ändern, dass sich nicht mehr jeder Knoten seinen Vaterknoten merkt, sondern seine Kinder. Auf einmal wird getParent() eine sehr teure Operation, da sämtliche Knoten durchsucht werden müssen, ob sie diesen Knoten als Kind besitzen. isChildOf(parent) braucht dagegen nur den parent selber zu fragen.
- Fazit: Wenn wir die Implementierung der Abfrage in einer Methode des Knotens kapseln, wird das Design flexibler und das System besser optimierbar. Deswegen gilt als Faustregel in der OOP: Möglichst viel weiter delegieren, möglichst wenig selber machen... :-)
- Nebenbei: Sobald Du Deinen Vergleich ein zweites Mal im Code stehen hast, verstößt Du bereits gegen EinmalUndNurEinmal und damit gegen die XP-Definition für EinfachesDesign.
- Ein viel subtilerer Grund: Codebeispiel 1 liest sich viel einfacherer als Codebeispiel 2 :-)
- Weiterhin: Solltest Du gegen EinfachesDesign dadurch verstossen, daß Du das Codebeispiel 2 ein zweites Mal anwendest, so erhältst Du duplizierten und somit redundanten Code - gar nicht gut ;-)
- Siehe auch im Refactoring-Katalog von MartinFowler Decompose Conditional [1]. -- bs
Ilja, dass du als XP-ler Optimierung als Argument verwendest, berührt mich merkwürdig.
- Auch als XP-ler sehe ich mich ab und zu in der Position, etwas optimieren zu müssen. Und wenn das der Fall ist, dann habe ich gerne ein System, das sich leicht optimieren lässt. Die Optimierung war aber auch nur ein Beispiel dafür, dass die Methode die Kopplung im System verringert, was generell ein erstrebenswertes Ziel ist.
- Optimierung wird nach MartinFowler (RefactoringBuch, Kapitel "2.7 Refactorisieren und Performance" der deutschen Ausgabe) in drei Varianten durchgeführt:
- Nach vorher vereinbarten Zeiten, d.h. wird ein (definiertes, nicht geschätztes) Zeitlimit überschritten, so wird optimiert. Praktisch bei Echtzeitsystemen, zu viel Aufwand bei "normaler" Software.
- Immer, d.h. der Programmierer schaut bei jeder Codezeile und jeder Methode darauf, diese möglichst performant zu gestalten. Ergebnis ist ein SpaghettiCode, der verworren, unüberschaubar und schwer bis unmöglich wartbar ist. Anekdote: ein Kollege wollte seinen Code optimieren (nicht, daß dieser Code schon funktioniert hätte oder er einen konkreten Optimierunsbedarf gehabt hätte, aber das Management meinte, es sei Zeit zu optimieren). Er hatte mal gelesen, das, in der SpracheJava, Strings zu konkatenieren schneller gehen würde, wenn er sie in einem Stringbuffer zusammenfügt. Dies machte er in seinem kompletten Code, da er feststellte, daß oft Strings mit dem + Zeichen konkateniert wurden. Fazit: das System wurde nicht schneller, aber es konnte ja nicht falsch sein, weiterhin diese Optimierung zu betreiben. Also programmierte der Kollege weiter mit den Stringbuffern anstatt mit +. Haken an der Sache: jegliche Stringkonkatenation wurde in Exceptions zwecks Informationsgewinn benutzt. D.h. wenn eine Aktion in diesem System fehl schlug, so wurde sie nicht mehr ausgeführt - und das ziemlich schnell :-)
- Wenn Funktion gewährleistet ist. Wenn es Grund dafür gibt. Nach Profiling, Messen und Refactorisieren. Wenn man genau weiss, was man tut. Und genau das ist es, was nach XP auch gemacht werden kann/darf/soll. Wenn ich refaktorisiert habe, weiss ich genau, an welchen Stellen was geschieht. Beispielsweise gibt es pro Methode nur eine Aufgabe, welche erfüllt wird, d.h. wenn ich herausgefunden habe, daß diese eine Aufgabe mein Flaschenhals bzgl. Performance ist, dann kann ich diese eine Methode optimieren (ich kennzeichne diese Methode dann mit einem entsprechenden Kommentar und verweise auf die Klasse, die ihre Optimierung gemessen hat, damit dies nicht kaputt refaktorisiert wird). --bs
Es ist auch nicht stimmig, denn wer Opimierung wirklich betreibt, würde zwar vielleicht einem Parent eine Liste seiner Children beifügen (um diese jederzeit aktuell verfügbar zu haben), sicher aber nicht die einfache Parent-Referenz beim Child (die um eine Größenordnung schneller ist, um die Beziehung festzustellen) opfern.
- Das wäre sicherlich die maximale PerformanceOptimierung, aber es hat auch deutliche Nachteile: Ich habe es auf einmal mit redundanter Information zu tun, die leicht zu Inkonsistenzen im System führen kann. Ob eine Referenz zum Parent dieses Wartungsproblem wirklich wert ist, hängt doch stark davon ab, wie die Knoten im Allgemeinen benutzt werden.
Ein langsames getParent wäre eine Absurdität, welches dem Wissen und den Erwartungen eines API-Benutzers zuwiderlaufen würde. -- hl
- Das sehe ich deutlich differenzierter. Ich kann mir auch durchaus Situationen vorstellen, in denen es gar nicht eines getParent() bedarf.
- Ich mache Euch darauf aufmerksam, daß ihr ziemlich spekuliert. Abgesehen vom Diskussionsstoff, der recht interessant ist, wäre es doch sicherlich ratsam, daß wenn dieser konkrete Fall eintritt (getParent() langsamer als isChildOf() oder ähnliches), konkrete Messungen vorzunehmen (falls, und nur dann(!), wenn es wirklich Grund gibt, hier zu optimieren) und zu schauen, was schneller und langsamer ist. Dann kann man doch immer noch durch geschicktes ausmanövrieren der Methoden und deren Bezeichnungen dem Benutzer die beste (performanteste) Anwendungsmöglichkeit in der API vorgeben. --bs
- Ja, natürlich sollte nur optimiert werden, wenn es dafür tatsächlich Bedarf gibt, und auch nur am nachgewiesenen Flaschenhals. Mein Punkt ist, dass das Benutzen der Methode isParent() an Stelle von getParent()==parent den Code besser entkoppelt und damit zum Beispiel eventuell notwendige Optimierungen vereinfacht. Ein anderes Beispiel wäre, wenn wir später für einige Knoten das Proxy- oder DekoratorMuster? einsetzen wollen - in diesem Fall könnte die Abfrage auf Identität sogar falsch sein! -- ip
Bernd, da kann ich deinem grundsätzlichen Argument schon mehr abgewinnen, obwohl in dem Beispiel der Ertrag viel größer ist und der sinnvolle Begriff "Summer" herausgearbeitet wird. Es entsteht am Ort der Verwendung eine Vereinfachung. Oben angewandt halte ich das aber für eine ideologische Position, die sich IMHO mit XP und FrameworkEntwicklung? nicht konsistent verbinden lässt: Eine solche API-Ausstattung (getParent, isParent, isChild und alle analogen Situationen) hat in einem Framework nur einen Sinn, wenn sie - unabhängig vom tatsächlichen Bedarf (XP?), den man ja nicht kennt - geplant (XP?) und konsistent durch ein ganzes API durchgezogen wird. Da sag ich lieber YAGNI. -- hl
- "grundsätzlichen Argument": sorry, welches war das denn noch gleich? Ehrlich, ich weiss nicht, worauf sich das "grundsätzlichen" bezieht. --bs
- Es bezog sich auf das Argument "gleiche Vergleiche sind als redundanter Code zu sehen". -- hl
- YAGNI, okay, bin ich auch für. Aber in diesem konkreten Fall hatte ich als Anwender ja tatsächlich das Problem, daß die API mich fehlgeleitet hatte. Das API ist eine Entwicklung meiner Firma, die Schnittstellen historisch gewachsen, und ich arbeite das erste Mal damit. Der/Mein Bedarf, das Framework zu ändern, ist da (und wie der da ist!!). Und Konsistenz hätte sich ergeben, wenn das API testgetrieben entwickelt worden wäre. Nun benutze ich das API und wenn ich auch nicht überall gleichzeitig dran arbeiten kann, so versuche ich doch die Fehler an den Stellen zu korrigieren, an denen sie auftauchen. Wenn ich aus Gründen der Konsistenz über diesen Mangel hinwegsehen würde, so stünde ich bald da wie meine Kollegen, die sich alle einig waren, daß ich einen Fehler entdeckt habe und sie alle (!) diesen Fehler kannten, allerdings nie jemand auf die Idee gekommen wäre, ihn zu beseitigen. Dieses Vorgehen nennt man doch eher DunklesYagni, oder? ;-) -- bs
- Für mich ist auch bereits der Verzicht auf eine isParent-Methode zugunsten von getParent()==parent ein Fall für DunklesYagni - wir wenden hier WerdenWirNichtBrauchen auf Design an, anstatt auf Funktionalität (die Abfrage, ob ein Knoten der Parent eines anderen ist, existiert im Code so oder so).
- Ab dem Moment wo Du obige Zeile in Deinem Code hast, verlangen die Regeln für EinfachesDesign, dass Du das in eine entsprechende Methode extrahierst - bei der Prüfung, ob ein Knoten der Vater eines anderen ist, handelt es sich um ein Konzept, das explizit im Code ausgedrückt werden sollte - und zwar genau einmal, an der Stelle, wo es konzeptionell am ehesten hingehört: im Knoten selber.
- Das mag jetzt sehr ideologisch klingen, aber es gibt dafür einen ganz pragmatischen Grund: Wenn man das konsequent durchzieht, erhält man nach meiner Erfahrung ein wesentlich besser entkoppeltes und damit erweiterbares und wartbares Design. -- IljaPreuß
StartSeite | Neues | TestSeite | ForumSeite | Teilnehmer | Kategorien | Index | Hilfe | Einstellungen | Ändern
Text dieser Seite ändern (zuletzt geändert: 24. September 2002 12:48 (diff))