Wie man schlechten Code mit Python-Refactoring in normalen Code verwandelt, der kein schönes objektorientiertes Design ist

Lassen Sie uns dies in Python tun

Originalcode

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.

Teil 1 - Wechsel von "nicht cool" zu "besser"

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

Teil 2 - Wechsel von "Mashi" zu "Like"

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]

Teil 3 - Wechseln Sie nicht von "Like" zu "Suge Like".

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.

Teil 4 - Objektorientiertes Design aufgeben

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)

Zusammenfassung

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

Recommended Posts

Wie man schlechten Code mit Python-Refactoring in normalen Code verwandelt, der kein schönes objektorientiertes Design ist
Lesen einer CSV-Datei mit Python 2/3
So konvertieren / wiederherstellen Sie einen String mit [] in Python
[Python] Wie zeichnet man mit Matplotlib ein Liniendiagramm?
So verwandeln Sie eine .py-Datei in eine .exe-Datei
So erstellen Sie ein Python-Paket mit VS Code
[Python] So schreiben Sie eine Dokumentzeichenfolge, die PEP8 entspricht
[Python] So erstellen Sie mit Matplotlib ein zweidimensionales Histogramm
[Python] Wie zeichnet man mit Matplotlib ein Streudiagramm?
So konvertieren Sie mit Python [Anwendung] von einem Array in ein Wörterbuch
Erstellen einer Entwicklungsumgebung für die Python2.7-Serie mit Vagrant
So gelangen Sie mit Vagrant in die Python-Entwicklungsumgebung
So schreiben Sie eine Meta-Klasse, die sowohl Python2 als auch Python3 unterstützt
[Python] Eine Geschichte, die in eine Rundungsfalle zu geraten schien
Ich möchte einen Platzhalter verwenden, den ich mit Python entfernen möchte
So machen Sie einen String in Python zu einem Array oder ein Array zu einem String
[Einführung in Python] So teilen Sie eine Zeichenfolge mit der Funktion split
[Python 3.8 ~] Wie man rekursive Funktionen mit Lambda-Ausdrücken intelligent definiert
So importieren Sie CSV- und TSV-Dateien mit Python in SQLite
[Python] Ein Memo, das ich versucht habe, mit Asyncio zu beginnen
So erstellen Sie eine Überwachungskamera (Überwachungskamera) mit Opencv und Python
Herstellen einer Verbindung zum Cloud Firestore über Google Cloud-Funktionen mit Python-Code
[Python] So erhalten Sie mit Enum einen Wert mit einem anderen Schlüssel als dem Wert
[ROS2] So spielen Sie eine Bag-Datei mit Start im Python-Format ab
So erstellen Sie eine Python- und Jupyter-Ausführungsumgebung mit VSCode
Python: So verwenden Sie Async mit
Debuggen eines Python-Programms durch Remoteverbindung mit einem Docker-Container in einer WSL2-Umgebung mit VS-Code
Erste Schritte mit Python
So rufen Sie eine POST-Anfrage auf, die Japanisch (Shift-JIS) mit Anfragen unterstützt
[Python] Erklärt anhand eines konkreten Beispiels, wie die Bereichsfunktion verwendet wird
Ich war süchtig danach, eine Python-Venv-Umgebung mit VS Code zu erstellen
Ein Hinweis, mit dem Sie die Python-Umgebung von Pineapple mit pyenv ändern können
Vorgehensweise zum Erstellen einer virtuellen Python-Umgebung mit VS-Code unter Windows
Wie zeichnet man eine vertikale Linie auf einer Heatmap, die mit Python Seaborn gezeichnet wurde?
Verwendung von hmmlearn, einer Python-Bibliothek, die versteckte Markov-Modelle realisiert
[Einführung in Python] So schreiben Sie eine Zeichenfolge mit der Formatierungsfunktion
3. Verarbeitung natürlicher Sprache mit Python 1-2. So erstellen Sie einen Korpus: Aozora Bunko
So generieren Sie QR-Code und Barcode in Python und lesen ihn normal oder in Echtzeit mit OpenCV
Was tun, wenn beim Laden eines mit Poesie erstellten Python-Projekts in VS Code ein Fehler auftritt?
[Python] Ein Programm, das Treppen mit # erstellt
Qiita (1) Wie schreibe ich einen Codenamen?
So fügen Sie ein Paket mit PyCharm hinzu
[Python] Wie man eine Klasse iterierbar macht
[Python] So konvertieren Sie eine zweidimensionale Liste in eine eindimensionale Liste
[Python] So invertieren Sie eine Zeichenfolge
Wie bekomme ich Stacktrace in Python?
Wie man einen Taschentest mit Python macht
So zeigen Sie Python-Japanisch mit Lolipop an
Wie man mit Python-Flüchen Japanisch eingibt
Eine typisierte Welt, die mit Python beginnt
So führen Sie Maya Python-Skripte aus
So installieren Sie Python3 mit Docker Centos
Versuchen Sie, das Problem des Handlungsreisenden mit einem genetischen Algorithmus (Python-Code) zu lösen.
So legen Sie Google Text & Tabellen in einem Ordner zusammen in einer TXT-Datei mit Python ab
[Python] So speichern Sie Bilder mit Beautiful Soup sofort im Web
So installieren Sie die Python-Bibliothek, die von Pharmaunternehmen verwendet werden kann
Ein Hinweis, dem ich beim Ausführen von Python mit Visual Studio Code verfallen war
Eine Geschichte, der ich nach der SFTP-Kommunikation mit Python verfallen war