Lassen Sie uns dies in Python tun
Versuchen Sie, die ursprüngliche Ruby-Version so weit wie möglich zu imitieren. Der Test verwendet py.test.
#ordersreport.py
from collections import namedtuple
Order = namedtuple("Order", "amount placed_at")
class OrdersReport:
def __init__(self, orders, start_date, end_date):
self.orders = orders
self.start_date = start_date
self.end_date = end_date
def total_sales_within_date_range(self):
orders_within_range = []
for order in self.orders:
if self.start_date <= order.placed_at <= self.end_date:
orders_within_range.append(order)
sum_ = 0 #Unterstreichen Sie, um Konflikte mit der integrierten Funktionssumme zu vermeiden
for order in orders_within_range:
sum_ += order.amount
return sum_
#ordersreport_test.py
from datetime import date
from ordersreport import Order, OrdersReport
def test_sales_within_date_range():
order_within_range1 = Order(
amount=5, placed_at=date(2016, 10, 10))
order_within_range2 = Order(
amount=10, placed_at=date(2016, 10, 15))
order_out_of_range = Order(
amount=6, placed_at=date(2016, 1, 1))
orders = [order_within_range1, order_within_range2, order_out_of_range]
start_date = date(2016, 10, 1)
end_date = date(2016, 10, 31)
report = OrdersReport(orders, start_date, end_date)
assert report.total_sales_within_date_range() == 15
Ich glaube nicht, dass Sie eine Erklärung brauchen, aber wenn Sie sie brauchen, lesen Sie die Ruby-Version.
Ändern Sie zunächst den Prozess, in dem Bestellungen innerhalb des Bereichs in orders_within_range gespeichert werden.
Hier verwenden Sie die Filter- oder Einschlussnotation in Python. Filter ist nützlich, wenn die Bedingung bereits eine Funktion ist. Wenn Sie jedoch einen bedingten Ausdruck schreiben möchten, ist die Einbeziehung besser als die Kombination von Filter und Lambda.
Ruby-Version
orders_within_range = @orders.select do |order|
order.placed_at >= @start_date && order.placed_at <= @end_date
end
Python-Version
orders_within_range =
[o for o in self.orders
if self.start_date <= o.placed_at <= self.end_date]
Als nächstes wird an jedem gedreht, um den Gesamtumsatz zu erhalten. Die Anzahl der Zeilen ist aufgrund der Summenerklärung extrem lang. Grundsätzlich sollte eine Methode innerhalb von 5 Zeilen liegen. Der ursprüngliche Code besteht aus 13 Zeilen. Vorerst können Sie es umgestalten, indem Sie die Anzahl der Zeilen verkürzen. Hier können Sie injizieren.
Für Python gibt es Reduzieren statt Injizieren, aber ich benutze es nicht viel. Verwenden Sie in solchen Fällen gehorsam die Summe. Übergeben Sie den Generator unter Verwendung der Einschlussnotation an das Argument der Summe.
Ruby-Version:
orders_within_range.inject(0) do |sum, order|
sum + order.amount
end
Python-Version:
return sum(o.amount for o in orders_within_range)
In der Ruby-Version kann die return-Anweisung selbst mithilfe von inj weggelassen werden. In Python kann sie jedoch nicht weggelassen werden, und es wird ausdrücklich angegeben, dass das Berechnungsergebnis zurückgegeben wird.
Lassen Sie uns zuerst orders_within_range zu einer privaten Methode machen und sie loslassen. Der Methodenname lautet so wie er ist orders_within_range.
Ich mag es nicht wirklich, eine Funktion dieser Länge weiter aufzuteilen, aber wenn es mehrere andere Berechnungsmethoden gibt, die orders_within_range verwenden, denke ich, dass es in Ordnung ist, sie zu teilen. Im Fall von Python gibt es kein privates und es ist üblich, Namen zu verwenden, die mit einem Unterstrich beginnen. Da es kein Argument gibt, handelt es sich um eine Eigenschaft.
def total_sales_within_date_range(self):
return sum(o.amount for o in self._orders_within_range)
@property
def _orders_within_range(self):
return [o for o in self.orders
if self.start_date <= o.placed_at <= self.end_date]
Nun, als nächstes
Dies verstößt gegen die Regel "Nicht zuhören, sagen", die ich zuvor in diesem Blog geschrieben habe. ... Hier ist es nicht gut, weil ich "zuhöre", ob order.placed_at innerhalb des Bereichs in select liegt. Wenn Sie dies in "nur sagen" ändern, wird es so sein.
Also füge ich der Order-Klasse "platzierte_zwischen" hinzu, aber ich bin gegen diese Änderung.
platzierte_vorher?
, platzierte_nachher?
, platzierte_at?
, Fügen Sie immer mehr hinzu, wenn Sie es brauchen? Bestätigen Sie, dass der Anrufer weg ist, und löschen Sie ihn, wenn Sie ihn nicht mehr benötigen? Vergessen Sie, viele nicht verwendete Methoden in Ihrem Projekt zu löschen und zu belassen?
Wenn Sie einen Grund haben, order.placed_at
privat zu halten, ist das in Ordnung, aber solange es öffentlich ist, bin ich dagegen, den allgemeinen Prozess des" Vergleichens von Daten "in die Zuständigkeiten von Order aufzunehmen. ..
. Bei Betrachtung des gesamten Codes werden hier und da start_date und end_date als Argumente verwendet. Diese start_date und end_date müssen jedes Mal eingegeben werden. Dies verringert die Lesbarkeit und Wiederverwendbarkeit. ... start_date und end_date sind immer Paare und keines ist gültig. Die nächste Änderung besteht darin, sie zusammenzusetzen.
Ist das richtig. Die Order-Klasse ist subtil, aber wenn es viele andere Orte gibt, die den Datumsbereich verwenden, würde ich ihn gerne implementieren.
(Abgesehen davon können Sie mit SQLAlchemy auch SQL generieren, indem Sie die "Klasse, die den Datumsbereich angibt", zwei Spalten in der RDB mit einer Funktion namens Composite Column Type [Reference](http: // qiita) zuordnen. com / methan / items / 57c4e4cf8fa7822bbceb)))
Die bisher implementierten sind wie folgt. Ich war ein wenig besorgt über "Include?" In der Ruby-Version, aber ich habe es in "contains" geändert. Wenn ein Bereich in Python (ähnlich wie in C ++ usw.) ausgedrückt wird, wird er normalerweise durch ein halboffenes Intervall [Anfang, Ende] ausgedrückt, sodass dieser Teil auch von Ruby getrennt wird.
Da DateRange nicht wirklich vom Datum abhängt, ist es versucht, einfach einen Zahlenbereich als Bereichstyp auszudrücken. Dies steht jedoch im Widerspruch zum integrierten Bereich, und es besteht ein starkes Risiko einer Überverallgemeinerung. Unterlassen Sie dies daher. ..
Um den Code bisher zusammenzufassen
#ordersreport.py
from collections import namedtuple
Order = namedtuple("Order", "amount placed_at")
class DateRange(namedtuple("DateRange", "start end")):
def __contains__(self, date):
return self.start <= date < self.end
class OrdersReport:
def __init__(self, orders, date_range):
self.orders = orders
self.date_range = date_range
def total_sales_within_date_range(self):
return sum(o.amount for o in self._orders_within_range)
@property
def _orders_within_range(self):
return [o for o in self.orders if o.placed_at in self.date_range]
In Teil 3 des Originalartikels wurde das folgende zweistufige Refactoring durchgeführt.
Vor dem Refactoring:
#Vor dem Refactoring
def total_sales_within_date_range
orders_within_range.map(&:amount).inject(0) do |sum, amount|
sum + amount
end
end
Methodenextraktion des Teils, das die Summe von "Order.amount" ergibt:
def total_sales_within_date_range
total_sales(orders_within_range)
end
private
def total_sales(orders)
orders.map(&:amount).inject(0) do |sum, amount|
sum + amount
end
end
Und die Implementierung von "total_sales" ist eine Redewendung in einer Zeile:
def total_sales(orders)
orders.map(&:amount).inject(0, :+)
end
Der Grund für das Extrahieren der Methode besteht darin, dass wir die Implementierung der öffentlichen Methode so einfach wie möglich gestalten möchten. Es ist sicherlich erforderlich, eine Pause einzulegen, um zu verstehen, was der Code vor dem Refactoring tut. .. Selbst die letzte einzeilige Implementierung des Idioms ist für niemanden offensichtlich, der dieses Idiom nicht kennt. Daher kann es sinnvoll sein, sie durch benannte Methoden zu trennen.
Andererseits ist Python zum Zeitpunkt von "sum (o.amount for o in self._orders_within_range)" ausreichend leicht zu lesen, und selbst wenn Sie es wagen, dies als "self._total_sales (self._orders_within_range)" umzuschreiben, ist die Lesbarkeit (*) sehr gut. Verbessert sich nicht. Im Gegenteil, die Lesbarkeit wird durch den Aufwand des Zurückspringens verringert, nur um sicherzustellen, dass die Implementierung von "_total_sales" den Betrag summiert. Also werde ich hier nichts machen.
Persönlich fiel es mir schwer, die Verantwortlichkeiten von OrdersReport zu verstehen.
Wie der Name schon sagt, sollte date_range kein Argument für "total_sales_within_date_range" sein, sondern an den Konstruktor übergeben werden, da andere Methoden zur Berichterstellung möglicherweise nicht von date_range abhängen, um verschiedene Berichte aus einem Array von Aufträgen zu berechnen. Ich wundere mich?
Dann wissen Sie nicht einmal, was es bedeutet, eine Klasse zu sein. In Python ist die Datei selbst ein Modul, daher ist eine Funktion ausreichend.
def total_sales_within_date_range(orders, date_range):
return sum(o.amount for o in orders if o.placed_at in date_range)
Was bevorzugen Sie, Code, den Ruby-Leute objektorientiert schön finden, oder Code, den Python-Leute einfach und normal finden?
Python-Version:
#ordersreport.py
from collections import namedtuple
Order = namedtuple("Order", "amount placed_at")
class DateRange(namedtuple("DateRange", "start end")):
def __contains__(self, date):
return self.start <= date < self.end
def total_sales_within_date_range(orders, date_range):
# (List[Order], DateRange) -> int
return sum(o.amount for o in orders if o.placed_at in date_range)
Ruby-Version:
class OrdersReport
def initialize(orders, date_range)
@orders = orders
@date_range = date_range
end
def total_sales_within_date_range
orders_within_range.map(&:amount).inject(0) do |sum, amount|
sum + amount
end
end
private
def orders_within_range
@orders.select do |order|
order.placed_between?(@date_range)
end
end
end
class DateRange < Struct.new(:start_date, :end_date)
def include?(date)
(start_date..end_date).cover? date
end
end
class Order < OpenStruct
def placed_between?(date_range)
date_range.include?(placed_at)
end
end