Abstraktions-Overkill

Marco13

Top Contributor
Hallo

Fragen wollte ich das schon länger. Der Anlass, dass ich es jetzt tue, war Icare3D Blog: The vicious circle of generalization @AltDevBlogADay bzw. Vicious circle of generalization #AltDevBlogADay - das heißt ja zumindest dass ich nicht der einzige bin ;)

Fast alles was man schreibt könnte noch allgemeiner geschrieben werden. Irgendwann merkt man dann, dass eigentlich alles, was man macht, nur Funktionen sind, und man ertappt sich dabei, nur noch einen Haufen
Java:
class SpecialFunction implements Function<Input,Output> { Output execute(Input input) { ... } }
-artige Implementierungen zu schreiben :autsch: Und dabei geht es nicht um Featuritis oder mißachtetes "YAGNI", sondern nur um eine immer weiter gehende Abstraktion der Beschreibung - schon fast im mathematisch-pursitischen Sinn: Man braucht eigentlich nur Mengen, der Bequemlichkeit halber nimmt man vielleicht noch Tupel/Listen und Funktionen dazu, aber das reicht dann auch. Gerade Java macht es einem da ja auch schrecklich einfach: Man kann wo immer man will schnell eine { Implementierung() } in Form einer anonymen Klasse reinschreiben, und theoretisch könnte man ja das meiste mit einer [c]class Function<S,T> {T evaluate(S s)};[/c] abbilden. Für alles weitere kämen dann die funktionalen Aspekte höherer Ordnung hinzu, und man landet bei Methodensignaturen wie
public static <A,B,C,D,E,F$,G,H,I> F<A,F<B,F<C,F<D,F<E,F<F$,F<G,I>>>>>>> partialApply8(F<A,F<B,F<C,F<D,F<E,F<F$,F<G,F<H,I>>>>>>>> f, H h). Toll, mächtig, aber für die public API einer Bibliothek vielleicht nicht sooo perfekt geeignet.

Man könnte das abwertend als Symptom für "Over-Engineering" bezeichnen, aber nachdem es keine harten Kriterien gibt, wann etwas Over- Under- oder einfach nur Engineered ist, wäre das doch eher subjektiv. Vielleicht gibt es ja ein paar Gedanken dazu, wie man einen objektiv sinnvollen "Jetzt ist aber auch mal gut"-Punkt verläßlich erkennt...?
 
F

Firephoenix

Gast
Hi,
Ich würde sagen ein Programm, das aus einer Klasse besteht ist
-nicht lesbar
-nicht wartbar
Ein Programm, das bis zum gehtnichtmehr abstrahiert wurde ist
-nicht lesbar
-wartbar
Ein Programm das (in der Theorie) perfekt abstrahiert ist, ist
-wartbar
-lesbar
Es gilt also die goldene Mitte zu finden, bei der man sich noch flott im Quellcode bewegen kann, gleichzeitig aber trotzdem den Code versteht.
Wenn in jeder Methode und Klasse nur noch Untermethoden sind ist das nicht mehr übersichtlich ;)
Gruß
 

Marco13

Top Contributor
@mvitz: Die Links sehen ganz interessant aus, werde ich mir mal näher ansehen - auf den ersten Blick scheint das aber "nur" eine gezielte Anwendung dessen, was ich mit der "Function" andeuten wollte: Es gibt "fast nichts" was man nicht durch Funktionen (die auf Mengen und Teilmengen operieren) beschreiben könnte...

@Firephoenix: Der goldene Mittelweg, ja... Aber... etwas plakativ formuliert: An vielen Stellen ist lediglich die Benennung das, was die Lesbarkeit (vermeintlich?) erhöht, aber die Allgemeinheit (vermeintlich?) einschänkt.

Um irgendein Beispiel rauszupicken, dass zu den "Klassischen" Datenstrukturen gehört: Man könnte für einen Graphen ganz straightforward und "unüberlegt" ein paar Klassen (bzw. Interfaces) erstellen, wie "Vertex", "Edge", "Graph"... Aber der Definition nach ist ein Graph einfach ein Tupel bestehend aus einer Menge von Knoten und einer Menge von Kanten, und eine (gerichtete) Kante ist ein Tupel aus Knoten - also könnte man das ganze auch auf Klassen wie "Tuple" und "Set" aufbauen. Es spricht in bezug auf die Flexibilität theoretisch viel dafür, schon allein weil man z.B. auf die "Set" dann die gesamte Maschinerie des Collections-Frameworks anwenden könnte, und mit ein paar Erweiterungen (wie z.B. den angedeuteten "Functions") herrlich elegante und allgemeine Algorithmen schreiben könnte, die bei spezifischen Klassen krampfigst für die spezifischen Strukturen neu- und mehrfach implementiert werden müßten...
 

FArt

Top Contributor
Ich fange in der Regel mit konkreten Implementierungen an und abstrahiere bei Bedarf. Ich stecke viel Energie in das (sinnvolle) Refaktorieren.

Und das hier, finde ich einen sehr guten Ansatz: Clean Code Developer ( Clean Code Developer - Clean Code Developer )
Mit diesen Grundgedanken und Ideen wird auch dein "Problem" eingefangen... ;-)
 
Zuletzt bearbeitet:

Landei

Top Contributor
Ein Beispiel für eine verpasste Gelegenheit für Abstraktion in Java ist die String-Klasse. In wieweit unterscheidet sich ein StringBuilder von einer List<Character> und ein String von der unmodifiable-Version? Wohlgemerkt spreche ich nicht davon, die interne Repräsentation zu ändern, nur davon, dem Nutzer eine möglichst allgemeine API anzubieten. Was hat man stattdessen? Nachträglich hineingemurkste Krücken wie CharSequence.

Hier ein wenig Haskell, wo Strings Listen von Chars sind (Haskell-Listen sind unveränderlich):
Code:
-- Länge
length [1,2,3,4,5]
length "abcde"

--drittes Element
[1,2,3,4,5] !! 3
"abcde" !! 3

--Verkettung
[1,2] ++ [3,4,5] 
"ab" ++  "cde"

--Umdrehen
reverse [1,2,3,4,5]
reverse "abcde"

--Testen auf Element
elem 3 [1,2,3,4,5]
elem 'c' "abcde"

--Filtern
filter (/= 3) [1,2,3,4,5]
filter (/='c') "abcde"

--Zusammenarbeit mit anderen Listen
concat $ zipWith replicate [1,2,3,4,5] "abcde"
--> "abbcccddddeeeee"

Wäre es nicht schön, wenn sowas in Java auch möglich wäre?

Ich finde jedenfalls, dass hier ein wenig mehr Abstraktion gut getan hätte.
 
M

maki

Gast
Stimme FArt zu, refactoring ist eines der wichtigsten Werkzeuge.

Anonsten git es noch hier einen imho passenden Artikel, da geht es darum, APIs/Module/Code etc. einfacher zu verwenden sind, wenn sie ganz konkrete Probleme lösen und nicht allgemein gehalten sind, auf der anderen Seite sind allgemein gehaltene APIs/Module/Code einfacher wiederzuverwenden.

The Use/Reuse Paradox | Javalobby
 

mvitz

Top Contributor
Wobei ich noch darauf hinweisen möchte, dass sowohl die Clean Code Initiative, als auch das Flow-Design von Ralf Westphal ausgeht, scheint sich also zumindest vermeintlich nicht ganz zu widersprechen ;)
 

Marco13

Top Contributor
@mvitz: Habe mir die Links nochmal angesehen: Erschreckende Ähnlichkeiten zu einem Projekt sind deutlich geworden, das ich vor... 1,2 Jahren mal angefangen hatte, und das ebenso verblüffenderweise "JFlow" heißt - Join, Source, Sink, Fork... - vermutlich ist es zu naheliegend. Von weiter oben betrachetet könnte man den Unterschied zwischen diesem Flow-Design und dem "normalen" Design vielleicht darüber charakterisieren, dass bei ersterem der Datenfluss im Vordergund steht, während es ansonsten üblicherweise (eher, erstmal) die Datenstrukturen (d.h. Klassen) sind.

@FArt: Die Seite ist ja allgemein bekannt. Und natürlich ist es (wenn man nicht schon einen konkreten Feinplan hat) (zumindest für mich) "üblich", erstmal grob anzuskizzieren, was man wie machen will, das dann abenso grob an-zu-implementieren, und zu schauen, wo man was besser und allgemeiner machen könnte - aber spätestens da stellt sich die Frage in analoger Form: Wann hört man mit den Refactorings auf, die der Verallgemeinerung dienen? Wieder als vordergründiges Beispiel mit der "Graph"-Klasse: Natürlich kann man erstmal eine Klasse "VertexSet" schreiben, die ähnlich zu einem "Set" ist, und darin implementiert man dann "contains", "retainAll", "removeAll"... und hätte sich das beim allgemeineren Ansatz (mit der Set) eigentlich sparen können. (Nicht immer ist es so offensichtlich - das sollte nur ein verdeutlichendes Beispiel sein).

@Landei: Ja, die Frage, inwieweit mein "unerfüllter Wunsch nach mehr Allgemeinheit" damit zusammenhängt, dass Java vielleicht einfach inhärent nicht ausreichend ausdrucksstark ist, also einfach die sprachlichen Mittel für das was ich machen will nicht ausreichen, habe ich mir auch schon gestellt. Hast du vielleicht eine Idee, welche Sprache man sich stattdessen mal ansehen könnte? :joke: (Ja, es ist schwer, dafür ausreichend Zeit zu allokieren). Speziell die Sonderbehandlung von Primitiven Typen stand mir in letzter Zeit ziemlich im Weg. Strings könnte man als eine Instanz dieses Problems ansehen (auch wenn es bei mir konkret eher um die anderen Typen ging, aber ... manchmal kommt man um einen "rohen int[]-Array" einfach nicht drumrum...)

@maki: Der Link, speziell das zweite Schaubild, beschreibt schon eine gewisse Sicht auf das, was ich meinte. Tendenziell versuche ich quasi, das durch mehrere "Schichten" zu lösen: Fine-grained "Lightweight"-Strukturen für die Flexibilität, und diese dann (wo möglich mit angemessenem Default-Verhalten oder Utility-Methoden und Klassen) zu größeren, coarse-grained "Heavyweight"-Strukturen zusammenzubauen. Letztere sollten dann einfacher zu verwenden sein, während erstere, für den, der das will, noch den Low-Level-Zugriff erlauben. Das ist nicht 100% das, was ich ursprünglich mit "Abstraktions-Overkill" meinte, aber natürlich ein Teilaspekt dessen, was aus dieser immer weiter gehenden Verallgemeinerung resultiert: Man hat viele kleine "Module" (Klassen), die sehr elementar sind, und "komplex" verschaltet werden müssen, um eine gewünschte Funktion zu erreichen. (Verschaltet werden müssen klingt schlecht - der postive Aspekt ist eben, dass sie komplex verschaltet werden können ;) )
 

Landei

Top Contributor
@Landei: Ja, die Frage, inwieweit mein "unerfüllter Wunsch nach mehr Allgemeinheit" damit zusammenhängt, dass Java vielleicht einfach inhärent nicht ausreichend ausdrucksstark ist, also einfach die sprachlichen Mittel für das was ich machen will nicht ausreichen, habe ich mir auch schon gestellt. Hast du vielleicht eine Idee, welche Sprache man sich stattdessen mal ansehen könnte?

Nun ja, Sprachen für die JVM gibt es genug, selbst wenn man sich auf statische Sprachen beschränkt. Wer denkt, Scala sei zu kompliziert, hat immer noch Fantom, Gosu, Erjang, Jaskell und jetzt auch Ceylon zur Auswahl, und jede dieser Sprachen ist Java überlegen, und viele Konzepte finden sich in mehreren oder sogar allen Sprachen wieder (wie etwa Closures).

Ich denke, dass Scala zwar umfangreich ist, aber trotzdem eine recht flache Lernkurve hat, weil man sich neue Konzepte Schritt für Schritt aneignen kann. Damit ist es meiner Meinung nach einfacher zu erlernen als beispielsweise Haskell, das viel weniger Features hat, die aber oft nur im Zusammenhang sinnvoll einsetzbar sind (man kann ja nicht mal ein "Hello World" schreiben, ohne der ersten Monade zu begegnen), man also mehr oder weniger "alles auf einmal" lernen muss. Deshalb ist Scala auch gut zum "Nebenbei-Lernen" geeignet, auch wenn man wenig Zeit hat.
 

FArt

Top Contributor
@FArt: Die Seite ist ja allgemein bekannt. Und natürlich ist es (wenn man nicht schon einen konkreten Feinplan hat) (zumindest für mich) "üblich", erstmal grob anzuskizzieren, was man wie machen will, das dann abenso grob an-zu-implementieren, und zu schauen, wo man was besser und allgemeiner machen könnte - aber spätestens da stellt sich die Frage in analoger Form: Wann hört man mit den Refactorings auf, die der Verallgemeinerung dienen? Wieder als vordergründiges Beispiel mit der "Graph"-Klasse: Natürlich kann man erstmal eine Klasse "VertexSet" schreiben, die ähnlich zu einem "Set" ist, und darin implementiert man dann "contains", "retainAll", "removeAll"... und hätte sich das beim allgemeineren Ansatz (mit der Set) eigentlich sparen können. (Nicht immer ist es so offensichtlich - das sollte nur ein verdeutlichendes Beispiel sein).

Nun, dafür gibt es einfach keine allgemeine Regel. Mal ist es sinnvoll, mal nicht.

Eine Abwägung ist aber ein Ansatz:
- Bringt es (in dem Moment) einen Mehrwert für den Kunden, das Produkt, die Wartung?
- KISS
- don't repeat yourself

Refactoring also nur bei konkretem Bedarf, also mit Mehrwert in irgendeiner Form.
 

Landei

Top Contributor
Wenn ich eine Klasse schreibe, die von der Funktionalität her einem Set entspricht, zahlt es sich immer (von Wegwerf-Code einmal abgesehen) aus, das entsprechende Interface zu implementieren, einfach deshalb, weil der Code damit einfacher zu lesen ist: Man muss "nur noch" die Funktionalität verstehen, die es eventuell zusätzlich zu den Set-Methoden gibt, alles andere ist unmittelbar klar. Man braucht sich nicht mehr fragen, ob Duplikate erlaubt sind, und was passiert, wenn man eins einfügt. Dazu kommt die zusätzliche "operationele Power" der Standardisierung: Man kann das Objekt in erweiterten for-Schleifen verwenden, es kann in einer Zeile zu einer Liste oder in ein Array konvertiert werden u.s.w.
 
M

maki

Gast
Wenn ich eine Klasse schreibe, die von der Funktionalität her einem Set entspricht, zahlt es sich immer (von Wegwerf-Code einmal abgesehen) aus, das entsprechende Interface zu implementieren, einfach deshalb, weil der Code damit einfacher zu lesen ist: Man muss "nur noch" die Funktionalität verstehen, die es eventuell zusätzlich zu den Set-Methoden gibt, alles andere ist unmittelbar klar. Man braucht sich nicht mehr fragen, ob Duplikate erlaubt sind, und was passiert, wenn man eins einfügt. Dazu kommt die zusätzliche "operationele Power" der Standardisierung: Man kann das Objekt in erweiterten for-Schleifen verwenden, es kann in einer Zeile zu einer Liste oder in ein Array konvertiert werden u.s.w.
Was aber, wen man wirklich nur add und get braucht (keine Iteratoren, keine vollwertigen Collection etc.?) und an sehr vielen Stellen im Programm verwendet wird?

Dann lohnt es sich eine eigene Abstraktion drüberzustülpen und man verschont die nnutzer der Klasse vor unnötig großen Schnittstellen.

Ist übrigens bei Maps in der Realtität häufiger der Fall ime.
 

Marco13

Top Contributor
@Landei: Ja, habe gerade mal wieder kurz auf den Blog geschaut - bei Funktoren in Java eSCALAtion Blog hattest du ja einen Punkt angeprochen, der in eine ähnliche Richtung geht, wie meine angedeuteten Beispiele... "sperrig" (in Java) ist wohl der richtige Ausdruck...

@FArt: Vielleicht ist meine Denkweise dort in mancher Hinsicht zu ... wenig kommerziell, zu akademisch, oder, böswillig gesagt, zu wenig praxisbezogen: Es geht nicht um "Kunden", schon gar nicht um ein "Produkt", und nur in sehr spezieller Hinsicht um "Wartung" - nur darum, coole, praktische, mächtige und allgemeine Bibliotheks-artige Klassensammlungen zu schreiben. DRY steht da auf "Modulebene" im Vordergrund. Wieder ein Beispiel: Ich habe schon in mehreren Zusammenhängen ein [c]interface Predicate<T> { boolean is(T t); }[/c] geschrieben, und den "üblichen Kladderadatsch" drumherum, nämlich Implementierungen und Methoden zur Erstellung von "And-Predicates", "Or-Predicates" und "Not-Predicates" - klar. Ich wollte das dann eigentlich mal in einer JAR zusammenpacken, die ich immer wieder verwenden kann (jaja, ich kenne Apache Functors, trotzdem :oops: ). Aber da tritt dann ganz konkret das auf, was ich oben schon angedeutet hatte: Ein Predicate ist ja eigentlich nur eine Function (die halt einen boolean liefert), und "AND" und "OR" sind eigentlich nur Funktionskompositionen. Einerseits ist es "unpraktisch", wenn man nicht
Code:
Predicate<X> p = Predicates.and(p0, p1);
schreiben kann, sondern
Code:
Function<Boolean,X> p = Functions.compose(f0, f1, and);
(oder so) schreiben muss - also eigentlich ein "Verstoß gegen KISS" - aber spätestens, wenn man das ganze dann in einem (erweiterten) Collections-Framework anbieten will, und dann deswegen Methoden wie
Code:
Collections#applyToAll(Collection, Predicate, Collection);
Collections#applyToAll(Collection, Function, Collection);
schreiben muss (nur weil man Predicate nicht als Function angesehen hat) und diese Methoden dann bis auf die Typen eigentlich gleich sind, ist das nicht nur ein "Verstoß gegen DRY", sondern schlicht unschön, unpraktisch und unflexibel. Jetzt zu sagen: ~"Da muss man halt abwägen" wäre etwas zu einfach, wenn man kein vom Kunden vorgegebenes Lastenheft hat...
 

FArt

Top Contributor
@FArt: Vielleicht ist meine Denkweise dort in mancher Hinsicht zu ... wenig kommerziell, zu akademisch, oder, böswillig gesagt, zu wenig praxisbezogen: Es geht nicht um "Kunden", schon gar nicht um ein "Produkt", und nur in sehr spezieller Hinsicht um "Wartung" - nur darum, coole, praktische, mächtige und allgemeine Bibliotheks-artige Klassensammlungen zu schreiben....
Da muss man halt abwägen" wäre etwas zu einfach, wenn man kein vom Kunden vorgegebenes Lastenheft hat...

Jein... mit Frameworks oder APIs ist das ja sowieso so eine Geschichte (und nach Framework oder API hört sich das gerade an).
Wie gut oder schlecht die API ist, sieht man in der Verwendung. Dort gilt dann wieder das KISS und das Mehrwert-Prinzip. Es ist also nie verkehrt (und das ist oft auch gar nicht einfach) die Brille des Benutzers aufzusetzten (Kunde = Entwickler) und dann zu sehen, wie einfach es ist damit etwas zu erreichen, wofür das ganze gedacht ist. Auch bzgl. Wartung der API und Wartung der damit erstellten Software. Dafür braucht man eigentlich auch kein Lastenheft.

(Endlich mal wieder ein Diskussionsthread, in dem es nicht nur darum geht eine Frage zu beantworten, die man auch ergoogeln kann) :)
 
Zuletzt bearbeitet:

Marco13

Top Contributor
Pfff, denkst du ich würde eine Frage mit einer ergooglebaren Antwort stellen? :pueh: ;)

Es geht tatsächlich in erster Linie um ""Frameworks"" bzw. der Wikipedia-Definition nach nicht mal Frameworks, sondern normale Bibliotheken - im einfachsten Fall eine oder mehrere JAR-Dateien mit ein paar praktischen Klassen drin. Einerseits könnte man sagen, dass Bibliotheken, die bestimmte Funktionalitäten anbieten, "nicht allgemein genug sein können" - am schon angedeuteten Beispiel: Funktionale Aspekte in Java, im Idealfall noch in Verbindung mit Collections. Aber sowas wie FunctionalJava ist dann doch eher nichts, was man einem "normalen" (nicht-akademischen) Anwender zumuten wollte. Konkretere Beispiele sind bei mir im Moment etliche in der "Pipeline":
- Eben eine "Functions"-Lib,
- Eine Lib für Primitive Collections (gibt's schon etliche, aber noch nicht für mehrdimensionale primitive Arrays, das ganze wiederum leicht funktional angehaucht ... ich könnte mir vorstellen, dass das mit Java7 an Bedeutung gewinnen könnte),
- Auch Sachen wie eine "Rendering-Bibliothek", bei der ich dann gemerkt habe, dass sich das ganze runterbrechen läßt auf Listen von sowas ähnlichem wie "[c]interface RenderingCommand { void execute(); }[/c]"-Objekten
- Noch ein paar andere, die ich jetzt nicht ausbreiten will, aber ...
... über allen schwebt die Frage "Geht das nicht auch flexibler und allgemeiner?", die ich ("leider"?) immer mit "Ja" beantworten kann. Etwas suggestiv formuliert: Um eine "zu" flexible API kann man einfach eine (weniger flexible, aber) einfach zu verwendende Hülle drumwickeln - aber umgekehrt geht das nicht. Das ganze kombiniert mit dem API-Design-Mantra: "Wenn man Zweifel hat, sollte man es weglassen" macht die Entscheidungen so schwierig: Man hat nur eine Chance, es "richtig" zu machen.
 

Landei

Top Contributor
Was aber, wen man wirklich nur add und get braucht (keine Iteratoren, keine vollwertigen Collection etc.?) und an sehr vielen Stellen im Programm verwendet wird?

Dann lohnt es sich eine eigene Abstraktion drüberzustülpen und man verschont die nnutzer der Klasse vor unnötig großen Schnittstellen.

Ist übrigens bei Maps in der Realtität häufiger der Fall ime.

Das ist richtig, aber eher ein Problem der bestehenden Java-APIs: Die Interfaces sind oft viel zu lang und detailliert, und es fehlt die Möglichkeit, eine Default-Implementiertung für Methoden anzugeben. Abstracte Klassen (etwa AbstractSet, AbstractList oder die Adapter in Swing) können helfen, zwingen einem aber eine vorgegebene Vererbung auf, was sie oft unbrauchbar macht. Inzwischen gibt es den Defender-Methoden-Vorschlag (was im Prinzip die Funktionalität von Scala-Traits bietet, nur etwas umständlicher), der das Problem deutlich verringern würde: Man könnte dann z.B. eine Default-Implementierungen für Methoden wie size, toArray, addAll, removeAll u.s.w. angeben. Für deine Implementierung müssten dann nur noch wirklich "existenzielle" Methoden angegeben werden (add, remove, contains und iterator sollten für Sets eigentlich ausreichen).
 
Zuletzt bearbeitet:

FArt

Top Contributor
... über allen schwebt die Frage "Geht das nicht auch flexibler und allgemeiner?", die ich ("leider"?) immer mit "Ja" beantworten kann. Etwas suggestiv formuliert: Um eine "zu" flexible API kann man einfach eine (weniger flexible, aber) einfach zu verwendende Hülle drumwickeln - aber umgekehrt geht das nicht. Das ganze kombiniert mit dem API-Design-Mantra: "Wenn man Zweifel hat, sollte man es weglassen" macht die Entscheidungen so schwierig: Man hat nur eine Chance, es "richtig" zu machen.

Ich schreibe selber Tests gegen meine API und kann dann ungefähr beurteilen wie handhabbard das ganze ist bzw. wie sinnvoll (einfach) der Code ist, der damit herauskommt.
Troztdem ist dieser Wechsel zur Benutzersicht nicht einfach. Da hilft z.B. die testgetriebene Entwicklung. Ich entwerfe eine API wie ich sie verwenden möchte, bevor ich mir Gedanken über die Implementierung mache. Irgendwann steckt man natürlich zu tief drin und hat u.U. unbemerkt die "technische" Brille auf.
Da helfen dann Codereviews und "Testentwickler", bzw. den Anwendern mal über die Schulter schauen. Was ist zu kompliziert? Was geht nicht, sollte aber gehen? Kann man die API auch intuitiv verwenden (mit Abstrichen), ...

Ein guter Trick ist einfach: mach nicht zu viel für den Anfang... die Verwendung zeigt, wo noch Nachholbedarf ist... und bei OpenSource bedkommt man eine Menge Feedback... manchmal auch sehr direkt ;-)
 

Marco13

Top Contributor
@Landei: Nach einem kurzen Blick auf die Einleitung sieht das ganz interessant aus. Ganz allgemein die Möglichkeit zu haben, Methoden eines Interfaces durch Delegation (statt wie bisher durch die (schrecklich unflexible) Vererbung) zu implementieren würde die Sprache schon um einiges mächtiger machen. Täuscht mich das jetzt eigentlich, oder sieht das frappierend nach einer Sprachmittelbasierten Unterstützung dessen aus, was ich vor 2 Jahren hier http://www.java-forum.org/allgemein...en-statische-utility-methoden.html#post491847 geschrieben habe...? ???:L
Wie auch immer: An den dort in der Fußnote genannten Beispielen sieht man es nochmal: Dass Dinge wie forEach, map, reduce & Co in der Collections-API fehlen ist schlecht - sowas muss so schnell wie möglich (GUT) angeboten werden...

... und bei OpenSource bedkommt man eine Menge Feedback... manchmal auch sehr direkt ;-)

"You library do not work ;( " ;) Die Gefahr der "technischen Brille", wie du es nanntest, ist wirklich evident: Wenn man zu lange (alleine) an einer Sache entwickelt, ist man so tief drin, dass man u.U. nicht mehr bemerkt, dass man unbenutzbar-kryptischen Bullshit schreibt. Aber ich versuche schon immer, das ganze mit ein bißchen gestelzter Naivität zu sehen (als weitere Anspielung an Bloch's exzellenten Vortrag InfoQ: How to Design a Good API & Why it Matters : Einfache Dinge sollten einfach sein, komplizierte Dinge sollten möglich sein, und Fehler am besten unmöglich) : Erst in ein paar Zeilen hinschreiben, was man gerne wie machen können will, und das dann weiter ausarbeiten - quasi Unit-Test und API parallel entwickeln. Aber dann, immer wieder, der gleiche Punkt: Wenn man hier noch diese winzig kleine Änderung in der Anwendung machen würde (ein anderer Klassenname, ein <Typparameter> mehr...) dann wäre es ein bißchen komplizierter zu verwenden, aber sooo viel allgemeiner und flexibler...
 

Landei

Top Contributor
Nicht wirklich, sondern ein Problem bzw. ein Effekt von allen generell wiederverwendbar ausgelegten APIs.

Das läßt eher auf ungenügende Strukturierung schließen. Scala hat z.B. viel mehr Hierarchie-Ebenen bei den Collections: Traversable, dann Iterable, dann Seq, dann LinearSeq und IndexedSeq, dann Sets, Lists, Maps, dann SortedSet und SortedMap, und erst dann die konkreten Implementierungen (alles noch in einer veränderlichen und unveränderlichen Variante). Das macht es einfach, ein wirklich passendes Interface auszuwählen. Dank Default-Methoden bleibt eine Implementierung trotzdem sehr übersichtlich. Das ist z.B. alles, was man für eine einfache eigene Set-Implementierung braucht:

Code:
  class MySet[T](seq:Seq[T]) extends scala.collection.immutable.Set[T] {
    val s = seq.distinct
    def -(t:T) = new MySet(s.filter(_ != t))
    def +(t:T) = if (s contains t) this else new MySet(t +: s);
    def contains(t:T) = s contains t
    def iterator = s.iterator
  }
 
M

maki

Gast
Das läßt eher auf ungenügende Strukturierung schließen.
Vielleicht reden wir aneinander vorbei ;)

Grundsätzlich ist es so, dass allgemein (und damit wiederverwendbar) ausgelegte Schnittstellen/APIs eben komplexer sind als spezifische für einen Anwendungsfall (siehe dazu auch den Link von mir).

Collections sind naturgemäß sehr allgemein ausgelegt, oft braucht in einem Programm aber nur eine Map mit 2-3 Methoden (put, get, containsKey) für einen einzigen Typen (zB 'ne Registry ähnliche Struktur), da lohnt es sich u.U. eine eigene Abstraktion über eine normale Map zu stülpen, die eben nur diese 3 Methoden anbietet, anstatt die 14 Methoden im Map Interface inkl. Typinformationen für Generics.
 

Marco13

Top Contributor
Einschub für interessierte Mitleser auf zwei Links auf Threads, die ähnlich philosophisch und verwandt zu einigen hier angesprochenen Punkten sind:
http://www.java-forum.org/softwareentwicklung/76479-tief-darf-vererbungshierarchie.html
http://www.java-forum.org/softwareentwicklung/76540-sinn-unsupportedoperationexception.html

@maki: Das Beispiel, das du angedeutet hast, wäre ja ähnlich einer "ImmutableMap" - man hätte das natürlich als eigenes Interface definieren können, aber dazu steht im zweiten Link ja schon einiges.
Trotzdem stimmt es natürlich, dass es designtechnisch und theoretisch gut sein kann, die speziellen Anforderungen seines Programmes nochmal in einem eigenen, minimalen (!) Interface zusammenzufassen, das ja dann ggf. trivial durch Delegation mit einer Klasse implementiert werden kann, die schon ein mächtigeres Interface implementiert.

Aber auch das ist ein Aspekt meiner ursprünglichen Frage, nämlich wirklich die Verallgemeinerung aufgrund struktureller Gleichheit - die steht eben teilweise schlicht und einfach "sprechenden" Klassennamen diametral gegenüber. Wieder ein (nicht hauen :oops: ) infantil-suggestives Beispiel:
Java:
interface Mensch { void esse(Mahlzeit m); }
interface Tier { void fresse(Futter f); }
Die sind strukturell gleich. Wenn man sie anwenden will, kann es "schön" (lesbar) sein, dass sie so sprechende Namen haben. Aber wäre es nicht "besser" (eben allgemeiner), dort ein
Java:
interface Konsument<T> { void konsumiere(T t); }
zu definieren? (Und ist das nicht (schon wieder :oops: ) einfach eine [c]Function<T> { void f(T t); }[/c] ?)
 

Empire Phoenix

Top Contributor
Ich bin für eine einfache pragmatische Lösung, das interface implementieren um die vorteile mitzunehmen, und die nicht sinnvollen methoden einfach leer lassen(exeption werfen). Wenn dann doch mal jemand diese bracht kann er die ja selber einbauen und als patch zurückschicken. (Geht natürlich nur bei freiem sourcecode)

Bei closed source stellt sich die Frage viel eher, da man ja einerseits einen Kunden hat der etwas machen mit möglichs wenig aufwand und andererseits aber auch nicht zu sehr eingegrenzt sein will. Im gegensatz zu klassischen opensource projekten kann man aber an einmal fertigen code nicht ständig was änder, speziell bei der api.
 
M

maki

Gast
@Marco13
Klar ist ein konsumiere da besser, die frage ist, zumidnest wie ich sie verstanden habe, ob ich dem Nutzer einer Klasse eine Schnittstelle mit 15 generischen Methoden zumute, wenn immer nur speziell 3 davon benötigt werden.

Ein anderes Beispiel wären die geschachtelten Maps, also Maps die Maps enthalten welche wiederrum Maps enthalten welche dann schliesslich Listen enthalten. Da empfiehlt man meist auch eine eigene Struktur.

@Empire Phoenix
Muss dir da widersprechen, bei öffentlichen APIs ist es egal ob OpenSource oder ClosedSource, öffentliche APIs ändert man nie auf die schnelle. Man kann weder bei OpenSource noch bei OpenSource einfach so was auf die schnelle ändern wenn die APIs mal öffentlich gemacht wurden, die frage ist nicht Open oder Closed Source, sondern wie öffentlich/verbreitet diese Schnittstellen sind.
 
Zuletzt bearbeitet von einem Moderator:

Marco13

Top Contributor
@maki: Das war eine Frage, die auch interessant ist, sich aus der Diskussion ergeben hat, und in den verklinkten Threads schon teilweise besprochen wurde. Das letzte Beispiel war aber deutlich näher an dem, was ich ursprünglich bei diesem Thread im Kopf hatte: Wenn du jetzt sagst, dass "Konsument#konsumiere" besser wäre, dann müßte "Function#execute" doch NOCH besser sein!? Und wenn man es darauf anlegt, kann man dann eben einen nicht unerheblichen Teil seiner Programme aus Functions zusammenbauen. Klar, dann könnte (oder sollte?) man wohl gleich eine funktionale Sprache nehmen, und das hat mit Objektorientierung ja eigentlich nichts zu tun, aber selbst wenn man nicht auf einen Sprachbezogenen Paradigmenwechsel abzielt, stellt sich die Frage, wie weit man mit "solchen Dingen (zur Förderung der Abstraktion und Allgemeingültigkeit)" gehen kann oder sollte...
 

schalentier

Gesperrter Benutzer
Wenn du jetzt sagst, dass "Konsument#konsumiere" besser wäre, dann müßte "Function#execute" doch NOCH besser sein!?

Hab ich das richtig verstanden, du meinst also:
Java:
interface Mensch extends Function<Mahlzeit> {
   void execute( Mahlzeit m );
}
interface Tier extends Function<Futter> {
  void execute( Futter f );
}

Mit Java kannste so pro Klasse also nur noch eine (oeffentliche) Methode schreiben, insofern ist das erst Mal ein hypothetisches Beispiel. Auf der anderen Seite frage ich mich, wozu? So verliert man doch gewissermassen ein (imho extrem wichtiges) Sprachelement: den Namen einer Methode.

Doch selbst wenn man all das erstmal ignoriert, sehe ich keinerlei Vorteil. Man koennte jetzt z.B. ne Liste haben:
Java:
...
List<Function<?>> functions;
...

Aber was hilft das jetzt? Da kann man prinzipiell alles reinpacken und dann die execute Methode aufrufen. Damit haste doch im Grunde einfach das komplette Typsystem ausgehebelt. Vielleicht hab ich aber auch alles falsch verstanden... ;-)

@Marco13: Du hast weiter oben gefragt, welche Sprache du dir mal ansehen solltest, die neue, andere, interessante Sprachkonzepte bietet. Mein Tipp: Gugg dir Ruby an. Das ist sehr ausdrucksstark, bei entsprechender Programmierung gut les- und wartbar und absolut hervorragend geeignet fuer "Framework" Entwicklung. Man ertappt sich staendig dabei, Dinge zu abstrahieren. Letztlich entwickelt man fuer quasi jedes Problem eine geeignete Mini-DSL.
 

Marco13

Top Contributor
Jooah, dass diese "Über-Abstraktion" teilweise auf eine Art DSL rausläuft hatte ich auch schon gedacht.

Zu dem "konsumiere"-Beispiel: Eigentlich meinte ich das noch "extremer", nämlich dass fast(!) überall dort, wo vorher "Mensch" oder "Tier" verwendet wurde, NUR eine Function verwendet werden könnte(!). Natürlich ist es "nicht schön", dass der Methodenname (und mit dem, was ich meinte, sogar der Klassenname - also quasi der Typ) verloren geht - das hatte ich weiter oben auch schon gesagt. Und es ging natürlich auch nicht darum, dass man systematisch dazu ansetzt, sein komplettes Programm aus Function's zusammenzubasteln. Aber an vielen Stellen wäre eben das genau der Zustand, auf den eine immer weiter gehende Abstraktion und Verallgemeinerung hinauslaufen würde - wo (vor diesem Punkt) soll man also damit aufhören?

Wieder das Suggestivbeispiel ein bißchen aufgebohrt: Wenn man ein Restaurant oder einen Mastbetrieb (falls man da einen Unterschied machen will ;) ) modellieren wollte, könnte man schreiben
Java:
class Restaurant { 
    void serviereEssen(Collection<Mahlzeit> c, Collection<Mensch> p) { ... }
}

class Mastbetrieb { 
    void verfüttere(Collection<Futter> c, Collection<Tier> p) { ... }
}
Diese Beiden Methoden wären sehr ähnlich, und würden sich eigentlich nur durch den Typ unterscheiden. Mit "Konsument#konsumiere" könnte man stattdessen schreiben
Java:
class KonsumentenBediener { 
    <T> void verteile(Collection<T> c, Collection<Konsument<T>> p) { ... }
}
Und damit könnten sowohl Menschen bedient als auch Tiere gemästet werden. Sehr praktisch, sehr allgemein...

Wenn man sich das dann ansieht, erkennt man, dass das eigentlich nur so etwas ähnliches wie das (in funktionalen Sprachen übliche) "applyToAll" ist, und man eigentlich auch
Java:
class CollectionUtils { 
    <T> void applyToAll(Collection<T> c, Collection<Function<T>> p) { ... }
}
schreiben könnte. Ob dort Mahlzeiten an Menschen oder Futter an Tiere verteilt wird, oder ob die Collection 'Sprite'-Objekte enthält, die von der 'Function' auf den Bildschirm gerendert werden, spielt für den in dieser Methode steckenden Ablauf keine Rolle: Jedes Element aus der Collection wird an die Methode aus der ensprechenen 'Function' übergeben.... also ist das ja eigentlich NOCH praktischer und NOCH allgemeiner... (!?)
 

ThreadPool

Bekanntes Mitglied
@maki: Das war eine Frage, die auch interessant ist, sich aus der Diskussion ergeben hat, und in den verklinkten Threads schon teilweise besprochen wurde. Das letzte Beispiel war aber deutlich näher an dem, was ich ursprünglich bei diesem Thread im Kopf hatte: Wenn du jetzt sagst, dass "Konsument#konsumiere" besser wäre, dann müßte "Function#execute" doch NOCH besser sein!? Und wenn man es darauf anlegt, kann man dann eben einen nicht unerheblichen Teil seiner Programme aus Functions zusammenbauen. Klar, dann könnte (oder sollte?) man wohl gleich eine funktionale Sprache nehmen, und das hat mit Objektorientierung ja eigentlich nichts zu tun, aber selbst wenn man nicht auf einen Sprachbezogenen Paradigmenwechsel abzielt, stellt sich die Frage, wie weit man mit "solchen Dingen (zur Förderung der Abstraktion und Allgemeingültigkeit)" gehen kann oder sollte...

Wie weit sollte man damit gehen, interessante Frage. Dazu muss man aber erstmal Fragen klären wie, wie viel Abstraktion ist überhaupt grundsätzlich notwendig um das Problem zu lösen, bietet mir eine weitere Abstraktion einen Mehrwert, wird mir dadurch etwas vereinfacht, verbessert es die Programmstruktur, wird es leichter wartbar, ist mein Anliegen so besser verständlich formuliert (im Code) oder wäre eine höhere Abstraktion nur reiner Selbstzweck, weil ich es gerade "geil" finde und minimal Tipparbeit spare? Viele Probleme auf die man trifft verlangen erstaunlicherweise gar nicht nach übermäßiger Abstraktion und es ist oft so das nur zum reinen Selbstzwek übermäßig abstrahiert wurde, weil man es eben kann und es auch zeigen möchte. Des Weiteren sollte man zwischen den Dingen trennen, will ich ein Framework, API schreiben oder "nur" eine Aufgabe erfüllen? Entweder konzentriert man sich auf eins und macht man es richtig oder es kommt nur halbgares Zeug dabei rum.

Du schreibst selbst, dass du Struktur X schon k-mal implementiert hast und immernoch keine Libary dazu gebaut hast. Das ist ein Beispiel für "Halbgares", es fehlt an Struktur an einer vernünftigen Umsetzung als API. Du erwähntest auch man hat nur "eine Chance es richtig zu machen", es gibt kein Richtig oder Falsch, es gibt nur Entscheidungen mit denen du als API-Entwickler leben musst. Solange ein API nur dir oder einem kleinen Kreis zugänglich ist kannst du darin schalten und walten wie dir beliebt, du kannst das API aufbauen und einreißen solange es die anderen Nutzer mitmachen. Kompliziert wird es dann wenn das API einem großen Nutzerkreis zur Verfügung gestellt wird. Ein API ist, vermute ich, beim ersten Versuch nie perfekt, ein API durchläuft verschiedene Evolutionsstufen und es gilt den Übergang von einer Stufe zur nächsten so geschmeidig (wenig Schmerzen auf API-Entwickler und API-Nutzer-Seite) wie möglich zu gestalten. Es gibt Hinweise wie ein API aussehen könnte (sprachbezogen auf Java, vll. die Erlebnisse wie in [1]), wie man für Erweiterbarkeit eines API sorgen kann.

Und wenn du dich fragst wie weit du gehen kannst, stell dir einfach die Fragen: Möchte ich als "Verwender" die gebotene Funktionalität so in dieser Art und Weise benutzen? und Wenn ich jmd wäre der dieses Stück Software später Erweitern und Warten müsste, wie lange benötige ich um mich einzuarbeiten und will ich diesen Code wirklich warten oder erweitern?

Übrigens, du sprachst von Paradigmenwechsel was hält dich auf Paradigmen zu kombinieren? Ich hatte vor nicht allzulanger Zeit das Vergnügen mit einem Javaprogramm in Berührung zu kommen bei dem Teile tatsächlich in einem Lisp-Dialekt verfasst waren (Clojure), weil es so einfacher umzusetzen und verständlicher war.


[1] Practical API Design, Confessions of a Java Framework Architect.
 

Marco13

Top Contributor
Die Fragen so allgemein beantwortet wie sie gestellt wurden ;) :

Notwendig ist wenig bis gar keine Abstraktion - man könnte auch für jeden Fall eine spezielle Klasse machen. Aber die allgemeinere Verwendbarkeit von Bibliotheks- (oder abwertend klingend: ) "Utility"-Klassen und Methoden auf neue Probleme ist IMHO schon eine Verbesserung. Aber die Fragen beziehen sich teilweise wohl auf die Lösungsansätze für konkrete(re) Problemstellungen, deswegen ist insbesondere die Frage gerechtfertigt, ob es um ein Framework oder eine konkrete Aufgabe geht: Es geht um ersteres (obwohl der Begrif "Framework" eigentlich auf irgendwas mit "Inversion of Control" hindeutet - es geht grob gesagt um allgemeine Bibliotheken)

Du schreibst selbst, dass du Struktur X schon k-mal implementiert hast und immernoch keine Libary dazu gebaut hast. Das ist ein Beispiel für "Halbgares", es fehlt an Struktur an einer vernünftigen Umsetzung als API.

Nun, wenn man in einer Anwendung mit ein paar tausend Klassen "mal schnell" was einbauen will, womit man Collections auf Basis von Predicates filtern kann, dann schreibt man ein interface "Predicate" und ein paar Utility-Methoden und gut ist... es soll ja nur eine Aufgabe erfüllen - und das tut es dann. Niemand kann in seinem Arbeitsalltag bei jeden Problem, das gelöst werden muss, DIE super-ausgefeilte Universalbibliothek für das konkrete Problem und (und darum geht es) auch die gesamte Klasse von Problemen schreiben. Aber wenn man letzteres versucht oder versuchen will, stellen sich eben die Fragen, die mich zu diesem Thread veranlasst haben...

Die allgemeine Frage, wie "stabil" eine API sein kann oder muss, stellt sich ja auch nur dann. Sie stellt sich nicht, wenn man mal kurz ein, zwei package-private Hilfsklassen in ein größeres Projekt einbaut. Aber wenn man dann mal DIE allgemeine Blbliothek schreiben will, dann zielt das in letzter Konsequenz darauf ab, dass es auf einer Webseite eine JAR gibt, die sich jeder runterladen und verwenden kann. Und dann HAT man nur eine Chance: Die Benutzer werden einem was husten, wenn in der Version 2 elementare Klassen und Methoden inkompatibel zur Version 1 sind. Was einmal public ist, muss "für immer bleiben wie es ist" - oder man ärgert den Benutzer mit @deprecated und Inkompatiblen Versionen....

Und wenn du dich fragst wie weit du gehen kannst, stell dir einfach die Fragen: Möchte ich als "Verwender" die gebotene Funktionalität so in dieser Art und Weise benutzen? und Wenn ich jmd wäre der dieses Stück Software später Erweitern und Warten müsste, wie lange benötige ich um mich einzuarbeiten und will ich diesen Code wirklich warten oder erweitern?

Die allgemeine Frage der Verwendbarkeit hatte ich oben schon angesprochen: Anskizzieren, wie man die Lib verwenden will, Unit-Tests und API parallel entwickeln ... dass es benutzbar bleiben soll ist klar (aber auch, dass man das als Entwickler vielleicht manchmal nicht objektiv beurteilen kann)

Übrigens, du sprachst von Paradigmenwechsel was hält dich auf Paradigmen zu kombinieren?

In Erwägung gezogen hatte ich es - aber trotzdem muss es einen "öffentlichen" Teil geben, der von Java aus verwendet werden kann. Aber nochmal genauer durchdenken...

Das Buch sieht ganz interessant aus. Unabhägäg davon, ob darin meine "konkreten" Fragen beantwortet werden, werde ich es mir mal näher ansehen.
 

Neue Themen


Oben