Comment transformer un mauvais code avec la refactorisation Python en code ordinaire qui n'est pas une belle conception orientée objet

Faisons cela en Python

Code d'origine

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.

Partie 1 - Passer de «pas cool» à «mieux»

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é.

Partie 2 - Passer de "Mashi" à "J'aime"

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]

Partie 3-Ne pas changer de "J'aime" à "Suge Like"

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.

Partie 4-Abandonner la conception orientée objet

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)

Résumé

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

Comment transformer un mauvais code avec la refactorisation Python en code ordinaire qui n'est pas une belle conception orientée objet
Comment lire un fichier CSV avec Python 2/3
Comment convertir / restaurer une chaîne avec [] en python
[Python] Comment dessiner un graphique linéaire avec Matplotlib
Comment transformer un fichier .py en fichier .exe
Comment créer un package Python à l'aide de VS Code
[Python] Comment écrire une docstring conforme à PEP8
[Python] Comment créer un histogramme bidimensionnel avec Matplotlib
[Python] Comment dessiner un diagramme de dispersion avec Matplotlib
Comment convertir un tableau en dictionnaire avec Python [Application]
Comment créer un environnement de développement de la série Python2.7 avec Vagrant
Comment entrer dans l'environnement de développement Python avec Vagrant
Comment écrire une classe méta qui prend en charge à la fois python2 et python3
[Python] Une histoire qui semblait tomber dans un piège à contourner
Je souhaite utiliser un caractère générique que je souhaite décortiquer avec Python remove
Comment transformer une chaîne en tableau ou un tableau en chaîne en Python
[Introduction à Python] Comment fractionner une chaîne de caractères avec la fonction split
[Python 3.8 ~] Comment définir intelligemment des fonctions récursives avec des expressions lambda
Comment importer des fichiers CSV et TSV dans SQLite avec Python
[Python] Un mémo que j'ai essayé de démarrer avec asyncio
Comment créer une caméra de surveillance (caméra de sécurité) avec Opencv et Python
Comment se connecter à Cloud Firestore à partir de Google Cloud Functions avec du code Python
[Python] Comment obtenir une valeur avec une clé autre que value avec Enum
[ROS2] Comment lire un fichier bag avec le lancement au format python
Comment créer un environnement d'exécution Python et Jupyter avec VSCode
Python: comment utiliser async avec
Comment déboguer un programme Python en se connectant à distance à un conteneur Docker dans un environnement WSL2 avec VS Code
Comment démarrer avec Python
Comment appeler une requête POST prenant en charge le japonais (Shift-JIS) avec des requêtes
[Python] Explique comment utiliser la fonction range avec un exemple concret
J'étais accro à la création d'un environnement Python venv avec VS Code
Notez que l'environnement Python de Pineapple peut être modifié avec pyenv
Procédure de création d'un environnement virtuel Python avec VS Code sous Windows
Comment dessiner une ligne verticale sur une carte de chaleur dessinée avec Python Seaborn
Comment utiliser hmmlearn, une bibliothèque Python qui réalise des modèles de Markov cachés
[Introduction à Python] Comment écrire une chaîne de caractères avec la fonction format
3. Traitement du langage naturel avec Python 1-2. Comment créer un corpus: Aozora Bunko
Comment générer un code QR et un code à barres en Python et le lire normalement ou en temps réel avec OpenCV
Que faire si une erreur se produit lorsque vous chargez un projet Python créé avec de la poésie dans VS Code
[Python] Un programme qui crée des escaliers avec #
Qiita (1) Comment écrire un nom de code
Comment ajouter un package avec PyCharm
[Python] Comment rendre une classe itérable
[Python] Comment convertir une liste bidimensionnelle en liste unidimensionnelle
[Python] Comment inverser une chaîne de caractères
Comment obtenir stacktrace en python
Comment faire un test de sac avec python
Comment afficher le japonais python avec lolipop
Comment entrer le japonais avec les malédictions Python
Un monde typé qui commence par Python
Comment exécuter des scripts Maya Python
Comment installer python3 avec docker centos
Essayez de résoudre le problème du voyageur de commerce avec un algorithme génétique (code Python)
Comment déposer Google Docs dans un dossier dans un fichier .txt avec python
[Python] Comment enregistrer des images sur le Web à la fois avec Beautiful Soup
Comment installer la bibliothèque Python qui peut être utilisée par les sociétés pharmaceutiques
Une note à laquelle j'étais accro lors de l'exécution de Python avec Visual Studio Code
Une histoire à laquelle j'étais accro après la communication SFTP avec python