Der Statistik nach scheint "Code Retreat: Keine If-Ausdrücke" der am häufigsten gelesene Artikel dieses Blogs zu sein (Wie geht das, bei nur zwei Lesern?). Dabei enthält der Artikel nur ein unsinniges Anti-Pattern und ist damit nicht sehr hilfreich.
In diesem Artikel möchte ich deswegen etwas nützlichere Informationen geben. Ich wechsle dabei in den Beispielen zwischen C#- und Python-Code hin und her :-)
Natürlich sind bedingte Verzweigungen (if, switch, ?-Operator) nicht an sich schlecht. Allerdings kann es Alternativen zu ihrer Verwendung geben, die den Code übersichtlicher und verständlicher machen. Ob das in einem konkreten Fall wirklich so ist, muss dann situativ entschieden werden.
Fall 1: Desine decision vs. Runtime decision
Einige Ifs können manchmal schon entfernt werden, weil sich herausstellt, dass die Fallunterscheidungsinformationen bereits zur Zeit der Programmierung und nicht erst zur Laufzeit zur Verfügung steht.Mit If:
File Open(string path, string accessType) { if (accessType == "w") { //Open file for write access //and return file descriptor } if (accessType == "r") { //Open file for read access //and return file descriptor } //Unknown access type return null; } void main() { File dataFile = Open("data.txt", "r"); //read data }
Ohne If:
File OpenWrite(string path) { //Open file for write access //and return file descriptor } File OpenRead(string path) { //Open file for read access //and return file descriptor } void main() { File dataFile = OpenRead("data.txt"); //read data }
Fall 2: Constant-Member
Wenn die Fallunterscheidung aufgrund einer Instanzvariablen erfolgt, die einmalig im Konstruktor gesetzt und ansonsten nur gelesen wird, ist die Aufteilung in mehrere Unterklassen wahrscheinlich die bessere Lösung.Mit If:
class ConversationBot: def __init__(self, formal): self.formal = formal def printGreeting(self): if self.formal: print("Good morning") else: print("Hi") def printFarewell(self): if self.formal: print("Goodbye") else: print("Peace")
Ohne If:
class FormalConversationBot: def printGreeting(self): print("Good morning") def printFarewell(self): print("Goodbye") class CasualConversationBot: def printGreeting(self): print("Hi") def printFarewell(self): print("Peace")
Fall 3: State-Member
Ändert sich die Variable während der Laufzeit - und damit auch das bedingte Verhalten - würde sich das Strategie- oder Zustands-Pattern als Alternative anbieten (wiewohl mir das Zustands-Pattern nicht zusagt und ich eher eine klassische Zustandstabelle bevorzuge).Mit If:
puplic class CDPlayer { private string state; public CDPlayer() { this.state = "CLOSED"; } public void EjectButtonPushed() { if (this.state == "CLOSED") { //Do stuff to open the device this.state = "OPEN"; } else { //Do stuff to close the device this.state = "CLOSED" } } }
Ohne If:
puplic class CDPlayer { public DeviceState state; public CDPlayer() { this.state = new ClosedState(); } public void EjectButtonPushed() { this.state.EjectButtonPushed(this); } } public interface DeviceState { void EjectButtonPushed(CDPlayer player); } public class OpenState : DeviceState { public void EjectButtonPushed(CDPlayer player) { //Do stuff to close the device player.state = new ClosedState(); } } public class CloseState : DeviceState { public void EjectButtonPushed(CDPlayer player) { //Do stuff to open the device player.state = new OpenState(); } }
Fall 4: Dictionary
Statt eines switch-Ausdrucks könnte auch ein dictionary verwendet werden, um Werte aufeinander zu mappen (wobei Werte auch Funktionen sein können), was die Möglichkeit eröffnet, solche Mappings aus einer Konfigurationsdatei zu laden.Mit If:
def getAreaCode(landCode): if landCode == "FR": return "+33" if landCode == "GB": return "+44" if landCode == "PL": return "+48" if landCode == "DE": return "+49" return None
Ohne If:
landToArea = { "FR": "+33" "GB": "+44" "PL": "+48" "DE": "+49" } def getAreaCode(landCode): try: return landToArea[landCode] except Exception: return None
Fall 5: Mathematic solution
Man könnte sich außerdem überlegen, ob eine Wertetransformation auch durch eine mathematische oder logische Funktion erfolgen kann.Mit If:
string getTimeDependingGreeting(int hour) { if (hour < 6) { return "Gute Nacht!"; } if (hour >= 6 && hour < 12) { return "Guten Morgen!"; } if (hour >= 12 && hour < 18) { return "Guten Tag!"; } return "Guten Abend!"; }
Ohne If:
string getTimeDependingGreeting(int hour) { var greetings = new [] {"Gute Nacht!", "Guten Morgen!", "Guten Tag!", "Guten Abend!"} return greetings[hour / 6]; }
Wer noch andere Wege zur If-Vermeidung kennt, teile diese mir bitte mit!
In Python gibt es ja keine Switch Anweisung und man verwendet da dann oftmals das von dir beschriebene Vorgehen mit einem Dictionary. Manchmal ist das gut, manchmal eher schwierig, insbesondere dann, wenn das Dictionary viel weiter oben im Code definiert wurde und am Entscheidungspunkt nicht sichtbar ist. Dann beginnt die fröhliche Scrollerei.
AntwortenLöschenAuch wenn dein Beispiel 3 natürlich nur ein Beispiel ist, so führt es doch schnell zu einem Wust an Klassen. Und wehe, dir fällt 2 Wochen später ein weiterer else-if Zweig ein. Dann kannst du wieder alle Klassen umbenennen :-)
Wie dem auch sei, sehr gute Beispiele!
Ich sehe in Python als Ersatz für Switches sehr oft lange folgen von elifs:
AntwortenLöschenif value==1:
#Do 1
elif value==2:
#Do 2
elif value==3:
#Do3
...
Um die von Dir kritisierte Scrollerei einzuschränken, sollte sich das Dictionary und die entsprechenden Zugriffsmethoden in einer hoch kohäsiven Klasse befinden und damit nur eine Leerzeile voneinander entfernt sein :-). Anonsten kommt es natürlich darauf an, welche Daten aufeinander gemappt werden und ob und warum der Leser des Codes, den Inhalt des Mappings kennen muss. Evtl. ein Hinweis, dass da noch mehr im Argen liegt.
Was das Zustands-Pattern angeht: Ja, hier stört mich auch die Unübersichtlichkeit, die durch die vielen Klassen entsteht. Deswegen bevorzuge ich in solchen Fällen auch eher Zustandstabellen, weil hier die Informationen an einer Stelle vereint sind und trotzdem von der Ablauflogik getrennt.