@snafu: Berachte meinen Code als Instanz von Uncle Bob's
Clean Architecture.
Im Zentrum sind die Entities, das sind hier einfach Dezimalzahlen und Funktionen darauf (
calculate_*()). Diese Funktionen werden mithilfe von Hilfsfunktionen implementiert, die bestimmte Regeln der Geschäftslogik aufrechterhalten, zB. dass Geldbeträge immer auf zwei Kommastellen gerundet werden, dass das Runden immer ein Abrunden ist, dass als Ergebnis einer Prozentrechnung ein Geldbetrag herauskommt, wenn diese auf einen Geldbetrag angewendet wird, und dass dabei entsprechend der o.g. Regeln gerundet wird. Sollte eine Regel geändert werden müssen, dann braucht man nicht den gesammten Code zu durchsuchen um alle Implementierungen der Regel zu ändern, sondern es gibt nur genau eine Stelle, die man ändern muss und der gesammte Code verwendet dann die geänderte Regel.
Dazu gibt es in der Präsentationsschicht Hilfsfunktionen, die Werte ausspucken, die direkt von den darunterliegenden Schichten verwendet werden können, ohne dass dort Konversionen oder Validierungen vorgenommen werden müssten. Auch die o.g. Regeln werden dort gleich auf die eingegebenen Werte angewendet.
Zwischen der allgemeinen Geschäftslogik (
calculate_*()) und der Präsentationsschicht (
ask_*(), show()) gibt es die Schicht der
Use Cases. Das sind Funktionen wie
beschaffungskalkulation(). Diese Schicht beschreibt anwendungsbezogene Geschäftsregeln, kommuniziert mit der darunterliegenden allgemeinen Geschäftslogik und sorgt dafür, dass Ein- und Ausgaben angestoßen werden (unter Zuhilfenahme der Funktionen der Präsentationsschicht).
Als Ergebnis bekommt man ein System, in dem Entities sich nicht um IO kümmern müssen, eine Präsentationsschicht, die nur ein paar sehr allgemeine Regeln beachten muss (zB. beim Runden) und eine Schicht von Use Cases, die nur über genau definierte Interfaces mit den anderen Schichten kommuniziert. An jeder Stelle können Implementierungen geändert werden, ohne dass dies die Klienten der Module interessieren müsste.
Interessant ist dabei auch, dass der IO soz. "umgestülpt" wird. IO findet nicht irgendwo in den Tiefen der allgemeinen Geschäftslogik statt, sondern auf der Ebene der Use Cases, wo es hingehört ("um ... zu berechnen, gibt der Benutzer die Werte ... und ... ein und erhält als Ergebnis ...").
Um die Implementierungen der Use Cases noch stärker von der Präsentationsschicht zu trennen, könnte man die IO-Funktionen als Callbacks übergeben:
Code: Alles auswählen
def beschaffungskalkulation(ask_for_money_amount, ask_for_percentage, show):
zieleinkaufspreis = calculate_zieleinkaufspreis(
listenpreis=ask_for_money_amount('Listenpreis'),
lieferanten_rabatt=ask_for_percentage('Lieferantenrabatt'),
)
show('Zieleinkaufspreis', zieleinkaufspreis)
...
Oder als Objekt mit entsprechenden Methoden. Wenn man auf diese Weise die Trennung der Schichten vornimmt, dann kann man zB. auch eine GUI darüberlegen. Die
ask_*() und
show() Funktionen würden in diesem Fall einfach bestimmte Felder der GUI auslesen bzw. aktualisieren. Oder man könnte verschiedene Szenarien aus einem Spreadsheet generieren[*]:
Code: Alles auswählen
column_map = {
'Listenpreis': 'A',
'Lieferantenrabatt': 'B',
'Zieleinkaufspreis': 'C',
...
}
def get_money_value(sheet, row, fieldname):
return money_amount(sheet.get(row=row, column=column_map[fieldname]))
def get_percent_value(sheet, row, fieldname):
return float(sheet.get(row=row, column=column_map[fieldname]))
def set_value(sheet, row, fieldname, value):
sheet.set(row=row, column=column_map[fieldname], value=value)
from functools import partial
for row in spreadsheet.rows():
beschaffungskalkulation(
ask_for_money_amount=partial(get_money_value, spreadsheet, row),
ask_for_percentage=partial(get_percent_value, spreadsheet, row),
show=partial(set_value, spreadsheet, row),
)
Praktisch, oder?
[*] Ja, ich weiß, dass Excel das auch ohne Python kann. Mir geht es darum, den Sinn dieser Architektur zu illustrieren.
In specifications, Murphy's Law supersedes Ohm's.