Faisons cela en Python
Essayez d'imiter autant que possible la version originale de Ruby. Le test utilise 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 #Souligner pour éviter les conflits avec la somme des fonctions intégrées
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
Je ne pense pas que vous ayez besoin d'explications, mais si vous en avez besoin, lisez la version Ruby.
Tout d'abord, modifiez le processus qui stocke les commandes dans la plage dans orders_within_range.
C'est là que vous utilisez la notation de filtre ou d'inclusion en Python. filter est utile lorsque la condition est déjà une fonction, mais lorsque vous souhaitez écrire une expression conditionnelle, l'inclusion est préférable à la combinaison de filter et lambda.
version rubis
orders_within_range = @orders.select do |order|
order.placed_at >= @start_date && order.placed_at <= @end_date
end
version python
orders_within_range =
[o for o in self.orders
if self.start_date <= o.placed_at <= self.end_date]
Vient ensuite le processus de rotation de chacun pour obtenir le total des ventes. Le nombre de lignes est extrêmement long en raison de la déclaration de somme. Fondamentalement, une méthode doit être dans les 5 lignes. Le code original comporte 13 lignes. Pour le moment, vous pouvez le refactoriser simplement en raccourcissant le nombre de lignes. Vous pouvez utiliser inject ici.
Pour Python, il y a réduire au lieu d'injecter, mais je ne l'utilise pas beaucoup. Dans de tels cas, utilisez sum avec obéissance. Passez le générateur à l'argument de somme en utilisant la notation d'inclusion.
Version rubis:
orders_within_range.inject(0) do |sum, order|
sum + order.amount
end
Version Python:
return sum(o.amount for o in orders_within_range)
Dans la version Ruby, l'instruction return elle-même pouvait être omise en utilisant inject, mais en Python, elle ne peut pas être omise et elle exprime explicitement que le résultat du calcul est renvoyé.
Tout d'abord, faisons de orders_within_range une méthode privée et laissons-la sortir. Le nom de la méthode est orders_within_range tel quel.
Je n'aime pas vraiment diviser davantage une fonction de cette longueur, mais s'il existe plusieurs autres méthodes de calcul qui utilisent orders_within_range, je pense que vous pouvez les partager. Dans le cas de Python, il n'y a pas de privé, et il est habituel d'utiliser des noms commençant par un trait de soulignement. Puisqu'il n'y a pas d'argument, c'est une propriété.
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]
Eh bien, ensuite,
Cela enfreint la règle "ne pas écouter, dites" que j'ai écrite plus tôt dans ce blog. ... Ici, ce n'est pas bon car j'écoute si order.placed_at est dans la plage de select. Si vous changez ceci en "juste dire", ce sera comme ça.
Donc, j'ajoute place_between?
À la classe Order, mais je suis contre ce changement.
placé_avant?
, placé_après?
, placé_at?
, en ajoutez-vous de plus en plus lorsque vous en avez besoin? Confirmez-vous que l'appelant est parti et supprimez-le lorsque vous n'en avez plus besoin? Avez-vous oublié de supprimer et de laisser beaucoup de méthodes inutilisées dans votre projet?
S'il y a une raison de garder order.placed_at
privé, c'est bien, mais tant qu'il est public, je suis contre l'inclusion du processus général de" comparaison des dates "dans les responsabilités de Order. ..
. En parcourant tout le code, start_date et end_date sont utilisés comme arguments ici et là. Ces start_date et end_date doivent être saisies à chaque fois. Cela réduit la lisibilité et la réutilisabilité. ... start_date et end_date sont toujours des paires, et aucune n'est valide. Le prochain changement à apporter est donc de les assembler.
Est-ce correct. La classe Order est subtile, mais s'il y a beaucoup d'autres endroits qui utilisent la plage de dates, je voudrais l'implémenter.
(En passant, avec SQLAlchemy, vous pouvez également générer du SQL en mappant une "classe qui spécifie une plage de dates" à deux colonnes sur le RDB avec une fonction appelée Type de colonne composite [Référence](http: // qiita. com / méthane / items / 57c4e4cf8fa7822bbceb)))
Ceux mis en œuvre jusqu'à présent sont les suivants. J'étais un peu inquiet à propos de ʻinclude? C'était dans la version Ruby, mais je l'ai changé en
contains`. Lors de l'expression d'une plage, en Python (similaire à C ++ etc.), elle est généralement exprimée par un intervalle semi-ouvert [début, fin), de sorte que la partie est également séparée de Ruby.
Étant donné que DateRange ne dépend pas réellement de la date, il est tenté d'exprimer simplement une plage de nombres sous la forme d'un type Range, mais cela entre en conflit avec la plage intégrée et il existe un fort risque de sur-généralisation, alors évitez de le faire. ..
Pour résumer le code jusqu'à présent
#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]
Dans la partie 3 de l'article original, la refactorisation en deux étapes suivante a été effectuée.
Avant de refactoriser:
#Avant de refactoriser
def total_sales_within_date_range
orders_within_range.map(&:amount).inject(0) do |sum, amount|
sum + amount
end
end
Méthode d'extraction de la partie qui prend la somme de ʻOrder.amount`:
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
Et l'implémentation de total_sales
est idiome sur une seule ligne:
def total_sales(orders)
orders.map(&:amount).inject(0, :+)
end
La raison de l'extraction de la méthode est que nous voulons rendre la mise en œuvre de la méthode publique aussi simple que possible en un coup d'œil, et il est certainement nécessaire de faire une pause pour comprendre ce que fait le code avant de refactoriser. .. Même la dernière implémentation en une ligne de l'idiome n'est pas évidente pour quiconque ne connaît pas cet idiome, il peut donc être judicieux de les séparer par des méthodes nommées.
Par contre, Python est suffisamment facile à lire au moment de sum (o.amount for o in self._orders_within_range)
, et même si vous osez réécrire ceci comme self._total_sales (self._orders_within_range)
, la lisibilité (*) est très bonne. Ne s'améliore pas.
Au contraire, la lisibilité est réduite par le tracas du retour en arrière juste pour s'assurer que l'implémentation de _total_sales
additionne le montant. Je ne ferai donc rien ici.
Personnellement, j'ai eu du mal à comprendre les responsabilités de OrdersReport.
Comme son nom l'indique, date_range ne doit pas être un argument de total_sales_within_date_range
plutôt que transmis au constructeur, car les autres méthodes de génération de rapports peuvent ne pas dépendre de date_range pour le rôle de calcul de divers rapports à partir d'un tableau de commandes. Je me demande?
Ensuite, vous ne savez même pas ce que signifie être une classe. En Python, le fichier lui-même est un module, donc une fonction suffit.
def total_sales_within_date_range(orders, date_range):
return sum(o.amount for o in orders if o.placed_at in date_range)
Lequel préférez-vous, le code que les gens de Ruby trouvent beau d'une manière orientée objet, ou le code que les gens de Python trouvent simple et normal?
Version Python:
#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)
Version rubis:
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
Recommended Posts