Hallo,
mir war langweilig. Habe mal einige Tipps/Vorschläge zusammengefasst, die ich beim Programmieren beherzige. Diese sind sicherlich nicht der Weisheit letzter Schluss. Daher bitte ich um eure Gedanken dazu. Einige Tipps/Vorschläge sind sicherlich trivial. Aber hier treiben sich ja auch einige Anfänger rum.
1. Möglichst wenige "Strings" im Quellcode
Zur Verdeutlichung ein einfaches Negativbeispiel:
Probleme:
1. Es ist sehr Fehleranfällig den Schlüssel "name" immer wieder als String in der Form @"name" zu schreiben. Ein kleiner
Tippfehler kann zu schweren Fehlern führen. Das muss nicht sein.
2. Andere Entwickler, die mittels KVC den "Namen" ermitteln möchten sind gezwungen dies in etwa wie folgt zu tun:
Dies ist für den Entwickler, der die Klasse nutzen möchte unschön, da fehleranfällig.
Lösung:
Für jeden Schlüssel, den eine Klasse intern nutzt schafft der Entwickler der Klasse zusätzlich eine Klassenmethode, welche
den Schlüssel liefert. Muss der Schlüssel auch von außerhalb der Klasse bekannt sein, muss die Klassenmethode in der
Headerdatei stehen - sonst in einer Kategorie.
Beispiel eines öffentlichen Schlüssels:
In der Headerdatei:
In der Implementation:
Nun kann die "setName:" Methode wie folgt implementiert werden:
Alternativ kann in der Headerdatei auch eine globale Variable definiert werden.
In der Implementation:
Der Nachteil dieser Lösung ist, dass viele globale Variablen erzeugt werden, die alle mit einem Präfix versehen werden
sollten, da dies der Vermeidung von Namenskonflikten hilft. Bei einer Änderung des eigenen Präfix müssen die Namen vieler
globalen Variablen geändert werden. Bei der Implementierung als Klassenmethode dient die eigene Klasse als "Namensraum". Das
vorgestellte Prinzip kann auch für Namen von "Bindings" sowie für "Kontexte" beim Observieren genutzt werden. Grundsätzlich
immer dann, wenn man es mit Keys oder sonstigen statischen Informationen zu tun hat.
2. Wiederholdende setNeedsDisplay: Aufrufe in NSView Subclass vermeiden.
Negativbeispiel:
Aus der Implementierung eines NSView Subclasses:
Alles anzeigen
Wie zu sehen ist geschieht in den Methoden nichts Spannendes. Der einzige Grund, wieso die Methoden selbst implementiert
werden ist, dasss bei neuem Namen sowie neuem Alter das View neu gezeichnet wird. Dies ist Arbeit, die nicht nötig ist.
Idee zur Verbesserung:
Die Accessoren werden nicht selbst implementiert sondern durch @synthesize. Dann wird das View allerdings nicht automatisch
neu gezeichnet. Allerdings kann das View seine eigenen Attribute observieren.
Hier nun eine bessere Implementierung:
In der Headerdatei:
Implementierung (init weggelassen)
Alles anzeigen
Was geschieht hier? Zunächst wird in der Kategorie "Private" eine "readonly" Eigenschaft erzeugt, die die Keys, deren
Änderung des Wertes ein Neuzeichnen des Views zur Folge haben muss. Zu dieser Eigenschaft gibt es einiges zu sagen:
- Sie ist in der Regel "privat" (daher in der Kategorie), da die interne funktionsweise andere Klassen nichts angeht. In
Ausnahmefällen kann sie auf öffentlich sein. Klar.
- Sie ist "readonly", da niemand die Schlüssel einfach so ändern können soll.
- Sie gibt ein Array und kein Set zurück, sodass das Iterieren mittels der neuen Fast Enumeration
möglich ist. Ein Set hat keine Reihenfolge. Eventuell würde sich das View dann zwei mal für den selben Schlüssel als Observer
anmelden. Alernativ könnte das Iterieren wie folgt geschehen:
Dann wäre die Verwendung eines Sets kein Problem. (Hinweis: Ich bin mir nicht sicher, ob NSFastEnumeration intern das Array
oder das Set über das iteriert wird "speichert". Dem müsste man nachgehen.)
Statt das Array/Set bei jedem Aufruf von self.keysAffectingNeedsDisplay neu zu erzeugen wäre auch denkbar dies mittels
"if/static" zu unterbinden.
- keysAffectingNeedsDisplay ist nicht wie nameKey oder ageKey eine Klassenmethode. Hintergedanke: Momentan gibt
keysAffectingNeedsDisplay einfach einige Schlüssel zurück, deren Änderung des Wertes ein Neuzeichnen zur Folge haben muss.
Denkbar wäre, dass diese Schlüssel abhängig davon sind, was das View momentan anzeigt. Man stelle sich ein großes View vor,
welches nicht all seine Eigenschaften gleichzeitig anzeigen kann. Dann sollten auch nicht alle Änderungen ein Neuzeichnen zur
Folge haben.
Hinweise: init sowie das Beenden der Observierung wurden nicht mit in den Code aufgenommen. Auch über den Zeitpunkt des
Begins der Observierung (in dem Fall geschieht dies bei viewDidMoveToSuperview) kann diskutiert werden. Denkbar:
awakeFromNib, initWithFrame:, ...
3. Welcher Controller ist für welches Binding verantwortlich?
Eine Klasse sollte keine unnötigen Outlets/Membervariablen haben. Oft sehe ich folgenden "Fehler": Der Entwickler erzeugt in
einer nib ein TableView und bindet dessen "content" an einen ArrayController. Dann möchte er das Verhalten des TableViews
mittels eines Delegates beeinflussen. Fein. Er erzeugt eine neue Klasse: MyTableViewDelegate. Da er in einer Delegatemethode
auch auf den ArrayController, der für den "content" des TableViews sorgt benötigt versieht er sein Delegate mit einem Outlet
zum ArrayController. Das muss nicht sein, denn es ist möglich diesen ArrayController "zur Laufzeit" in Abhängigkeit des
TableViews zu ermitteln:
Vorteile: Das eigene Delegate hängt nicht von einem speziellen ArrayController ab sondern ermittelt diesen in Abhängigkeit
des TableViews. So kann das Delegate von mehreren TableViews genutzt werden.
4. Die Layoutberechnungen eines NSView Subclasses "lazy" implementieren.
Angenommen es ist ein View zu implementieren, welches ein blaues Rechteck in die Mitte des Views zeichnet. Das View verfügt
über das Attribut "inset", welches bestimmt wie viel Abstand zwischen dem äußersten Rand des Views und dem blauen Rechteck
sein soll. Ein inset von 0 bedeutet, dass das blaue Rechteck das komplette View bedecken soll. Ein inset von 100 soll das
blaue Rechteck an jeder Seite Einheiten Abstand zum äußersten Rand des Views halten.
Hier ein Negativbeispiel: (blueRect ist definiert als NSRect)
Alles anzeigen
Probleme:
- blueRect ist nicht immer "aktuell", da es nur beim Aufruf von setInset berechnet wird. Wird das View in seiner Größe
verändert, so wird dies nicht berücksichtigt.
- Es ist eine zusätzliche "Membervariable" (blueRect) notwendig.
- Eine Berechnung versuchen zu cachen oder etwas "vorzuberechnen" ist grundsätzlich keine gute Idee.
Alternative:
Alles anzeigen
Diese Vorgehensweise ist auch gut auf kompliziertere Views anwendbar. Konkretes Beispiel:
[Blockierte Grafik: http://christian-kienle.de/MediaSnapScreenshots/finishedWindow.png]
Der Video/Audio Slider verfügt intern über allerhand "readonly" Methoden, die bei jedem Aufruf irgendwas in Abhängigkeit der
Größe des Views (self.bounds.size) berechnen. Das View erzeugt zum Beispiel bei jedem Aufruf von drawRect: folgende Dinge:
- borderBezierPath (ein NSBezierPath für den Rand)
- knobSize (ein NSSize für die Größe des "Schiebereglers")
- knobRect (ein NSRect, welches neben der Größe auch die Position des "schiebereglers" berechnet.)
- freeRect (ein NSRect, welches die Dimensionen des "dunklen" Bereiches beschreibt.)
Usw. Es wird alles "lazy" in diesen Methoden berechnet und das immer wieder. Ohne caching. Die drawRect: Methode wird dadurch
sehr einfach und logisch sowie gut zu warten. Performanceprobleme gibt es auch keine. Cachen nur da, wo unbedingt nötig.
5. Möglichst späte Deklaration und Initialisierung von Variablen
Negativbeispiel:
Besser:
Noch besser:
Sollte klar sein - aber ich sehe das immer wieder.
6. Komplizierte if-Ausdrücke vereinfachen
Negativbeispiel:
Problem: Es ist zwar klar, was überprüft wird - allerdings liest ein Programmierer oftmal mehr Code, als er selbst schreibt.
Das Lesen sollte also möglichsteinfach sein. Daher kann für komplizierte Ausdrücke wieder eine readonly-Eigenschaft
geschaffen werden, die in etwa so aussieht:
Dann ergibt dies:
Und jeder weiß, was gemeint ist.
mir war langweilig. Habe mal einige Tipps/Vorschläge zusammengefasst, die ich beim Programmieren beherzige. Diese sind sicherlich nicht der Weisheit letzter Schluss. Daher bitte ich um eure Gedanken dazu. Einige Tipps/Vorschläge sind sicherlich trivial. Aber hier treiben sich ja auch einige Anfänger rum.
1. Möglichst wenige "Strings" im Quellcode
Zur Verdeutlichung ein einfaches Negativbeispiel:
Probleme:
1. Es ist sehr Fehleranfällig den Schlüssel "name" immer wieder als String in der Form @"name" zu schreiben. Ein kleiner
Tippfehler kann zu schweren Fehlern führen. Das muss nicht sein.
2. Andere Entwickler, die mittels KVC den "Namen" ermitteln möchten sind gezwungen dies in etwa wie folgt zu tun:
Dies ist für den Entwickler, der die Klasse nutzen möchte unschön, da fehleranfällig.
Lösung:
Für jeden Schlüssel, den eine Klasse intern nutzt schafft der Entwickler der Klasse zusätzlich eine Klassenmethode, welche
den Schlüssel liefert. Muss der Schlüssel auch von außerhalb der Klasse bekannt sein, muss die Klassenmethode in der
Headerdatei stehen - sonst in einer Kategorie.
Beispiel eines öffentlichen Schlüssels:
In der Headerdatei:
In der Implementation:
Nun kann die "setName:" Methode wie folgt implementiert werden:
Alternativ kann in der Headerdatei auch eine globale Variable definiert werden.
In der Implementation:
Der Nachteil dieser Lösung ist, dass viele globale Variablen erzeugt werden, die alle mit einem Präfix versehen werden
sollten, da dies der Vermeidung von Namenskonflikten hilft. Bei einer Änderung des eigenen Präfix müssen die Namen vieler
globalen Variablen geändert werden. Bei der Implementierung als Klassenmethode dient die eigene Klasse als "Namensraum". Das
vorgestellte Prinzip kann auch für Namen von "Bindings" sowie für "Kontexte" beim Observieren genutzt werden. Grundsätzlich
immer dann, wenn man es mit Keys oder sonstigen statischen Informationen zu tun hat.
2. Wiederholdende setNeedsDisplay: Aufrufe in NSView Subclass vermeiden.
Negativbeispiel:
Aus der Implementierung eines NSView Subclasses:
Quellcode
- ...
- - (void)setName:(NSString *)newName {
- [self willChangeValueForKey:@"name"];
- name = newName;
- [self didChangeValueForKey:@"name"];
- [self setNeedsDisplay:YES];
- }
- - (void)setAge:(NSNumber *)newAge {
- [self willChangeValueForKey:@"age"];
- age = newAge;
- [self didChangeValueForKey:@"age"];
- [self setNeedsDisplay:YES];
- }
- ...
Wie zu sehen ist geschieht in den Methoden nichts Spannendes. Der einzige Grund, wieso die Methoden selbst implementiert
werden ist, dasss bei neuem Namen sowie neuem Alter das View neu gezeichnet wird. Dies ist Arbeit, die nicht nötig ist.
Idee zur Verbesserung:
Die Accessoren werden nicht selbst implementiert sondern durch @synthesize. Dann wird das View allerdings nicht automatisch
neu gezeichnet. Allerdings kann das View seine eigenen Attribute observieren.
Hier nun eine bessere Implementierung:
In der Headerdatei:
Implementierung (init weggelassen)
Quellcode
- @interface PersonView (Private)
- @property (readonly) NSArray *keysAffectingNeedsDisplay;
- @end
- @implementation PersonView (Private)
- - (NSArray *)keysAffectingNeedsDisplay; { return [NSArray arrayWithObjects:[self nameKey], [self ageKey], nil]; }
- @end
- @implementation PersonView
- + (NSString *)nameKey { return @"name"; }
- + (NSString *)ageKey { return @"age"; }
- @synthesize name;
- @synthesize age;
- - (void)viewDidMoveToSuperview {
- for(NSString *key in self.keysAffectingNeedsDisplay) {
- [self addObserver:self forKeyPath:key options:0 context:0];
- }
- }
- - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void
- *)context {
- if(object == self) {
- [self setNeedsDisplay:YES];
- return;
- }
- [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
- }
- @end
Was geschieht hier? Zunächst wird in der Kategorie "Private" eine "readonly" Eigenschaft erzeugt, die die Keys, deren
Änderung des Wertes ein Neuzeichnen des Views zur Folge haben muss. Zu dieser Eigenschaft gibt es einiges zu sagen:
- Sie ist in der Regel "privat" (daher in der Kategorie), da die interne funktionsweise andere Klassen nichts angeht. In
Ausnahmefällen kann sie auf öffentlich sein. Klar.
- Sie ist "readonly", da niemand die Schlüssel einfach so ändern können soll.
- Sie gibt ein Array und kein Set zurück, sodass das Iterieren mittels der neuen Fast Enumeration
möglich ist. Ein Set hat keine Reihenfolge. Eventuell würde sich das View dann zwei mal für den selben Schlüssel als Observer
anmelden. Alernativ könnte das Iterieren wie folgt geschehen:
Dann wäre die Verwendung eines Sets kein Problem. (Hinweis: Ich bin mir nicht sicher, ob NSFastEnumeration intern das Array
oder das Set über das iteriert wird "speichert". Dem müsste man nachgehen.)
Statt das Array/Set bei jedem Aufruf von self.keysAffectingNeedsDisplay neu zu erzeugen wäre auch denkbar dies mittels
"if/static" zu unterbinden.
- keysAffectingNeedsDisplay ist nicht wie nameKey oder ageKey eine Klassenmethode. Hintergedanke: Momentan gibt
keysAffectingNeedsDisplay einfach einige Schlüssel zurück, deren Änderung des Wertes ein Neuzeichnen zur Folge haben muss.
Denkbar wäre, dass diese Schlüssel abhängig davon sind, was das View momentan anzeigt. Man stelle sich ein großes View vor,
welches nicht all seine Eigenschaften gleichzeitig anzeigen kann. Dann sollten auch nicht alle Änderungen ein Neuzeichnen zur
Folge haben.
Hinweise: init sowie das Beenden der Observierung wurden nicht mit in den Code aufgenommen. Auch über den Zeitpunkt des
Begins der Observierung (in dem Fall geschieht dies bei viewDidMoveToSuperview) kann diskutiert werden. Denkbar:
awakeFromNib, initWithFrame:, ...
3. Welcher Controller ist für welches Binding verantwortlich?
Eine Klasse sollte keine unnötigen Outlets/Membervariablen haben. Oft sehe ich folgenden "Fehler": Der Entwickler erzeugt in
einer nib ein TableView und bindet dessen "content" an einen ArrayController. Dann möchte er das Verhalten des TableViews
mittels eines Delegates beeinflussen. Fein. Er erzeugt eine neue Klasse: MyTableViewDelegate. Da er in einer Delegatemethode
auch auf den ArrayController, der für den "content" des TableViews sorgt benötigt versieht er sein Delegate mit einem Outlet
zum ArrayController. Das muss nicht sein, denn es ist möglich diesen ArrayController "zur Laufzeit" in Abhängigkeit des
TableViews zu ermitteln:
Vorteile: Das eigene Delegate hängt nicht von einem speziellen ArrayController ab sondern ermittelt diesen in Abhängigkeit
des TableViews. So kann das Delegate von mehreren TableViews genutzt werden.
4. Die Layoutberechnungen eines NSView Subclasses "lazy" implementieren.
Angenommen es ist ein View zu implementieren, welches ein blaues Rechteck in die Mitte des Views zeichnet. Das View verfügt
über das Attribut "inset", welches bestimmt wie viel Abstand zwischen dem äußersten Rand des Views und dem blauen Rechteck
sein soll. Ein inset von 0 bedeutet, dass das blaue Rechteck das komplette View bedecken soll. Ein inset von 100 soll das
blaue Rechteck an jeder Seite Einheiten Abstand zum äußersten Rand des Views halten.
Hier ein Negativbeispiel: (blueRect ist definiert als NSRect)
Quellcode
Probleme:
- blueRect ist nicht immer "aktuell", da es nur beim Aufruf von setInset berechnet wird. Wird das View in seiner Größe
verändert, so wird dies nicht berücksichtigt.
- Es ist eine zusätzliche "Membervariable" (blueRect) notwendig.
- Eine Berechnung versuchen zu cachen oder etwas "vorzuberechnen" ist grundsätzlich keine gute Idee.
Alternative:
Quellcode
- @interface RechteckView(Private)
- @property (readonly) NSRect blueRect;
- @end
- @implementation RechteckView(Private)
- - (NSRect)blueRect; { return NSInsetRect(self.bounds, self.inset, self.inset); }
- @end
- @implementation RechteckView
- @synthesize inset; // für setNeedsDisplay: siehe Tipp 2
- - (void)drawRect:(NSRect) {
- [[NSColor blueColor] setFill];
- NSRectFill(self.blueRect);
- }
- @end
Diese Vorgehensweise ist auch gut auf kompliziertere Views anwendbar. Konkretes Beispiel:
[Blockierte Grafik: http://christian-kienle.de/MediaSnapScreenshots/finishedWindow.png]
Der Video/Audio Slider verfügt intern über allerhand "readonly" Methoden, die bei jedem Aufruf irgendwas in Abhängigkeit der
Größe des Views (self.bounds.size) berechnen. Das View erzeugt zum Beispiel bei jedem Aufruf von drawRect: folgende Dinge:
- borderBezierPath (ein NSBezierPath für den Rand)
- knobSize (ein NSSize für die Größe des "Schiebereglers")
- knobRect (ein NSRect, welches neben der Größe auch die Position des "schiebereglers" berechnet.)
- freeRect (ein NSRect, welches die Dimensionen des "dunklen" Bereiches beschreibt.)
Usw. Es wird alles "lazy" in diesen Methoden berechnet und das immer wieder. Ohne caching. Die drawRect: Methode wird dadurch
sehr einfach und logisch sowie gut zu warten. Performanceprobleme gibt es auch keine. Cachen nur da, wo unbedingt nötig.
5. Möglichst späte Deklaration und Initialisierung von Variablen
Negativbeispiel:
Besser:
Noch besser:
Sollte klar sein - aber ich sehe das immer wieder.
6. Komplizierte if-Ausdrücke vereinfachen
Negativbeispiel:
Problem: Es ist zwar klar, was überprüft wird - allerdings liest ein Programmierer oftmal mehr Code, als er selbst schreibt.
Das Lesen sollte also möglichsteinfach sein. Daher kann für komplizierte Ausdrücke wieder eine readonly-Eigenschaft
geschaffen werden, die in etwa so aussieht:
Dann ergibt dies:
Und jeder weiß, was gemeint ist.
Die Objective-Cloud ist fertig wenn sie fertig ist. Beta heißt Beta.
Objective-C und Cocoa Band 2: Fortgeschrittene
Cocoa/Objective-C Seminare von [co coa:ding].
Objective-C und Cocoa Band 2: Fortgeschrittene
Cocoa/Objective-C Seminare von [co coa:ding].
