Pierwsze koty za płoty – Python – refaktoryzacja pierwszej aplikacji

devkot.pl 9 miesięcy temu

Cześć, w poprzednim artykule omówiliśmy podstawy programowania obiektowego w Pythonie. Nadszedł teraz czas, aby poznaną teorię przekuć w praktykę. W ramach tego artykułu poznasz jeden z najważniejszych procesów związanych z programowaniem, a mianowicie proces refaktoryzacji. Dokonamy również zmian w aplikacji kalkulatora, którą napisaliśmy w artykule Pierwsze koty za płoty – Python – pierwsza aplikacja. Celem tych zmian będzie refaktoryzacja skryptu, tak aby działał z wykorzystaniem klas i obiektów. Zatem do dzieła!

Refaktoryzacja - co to takiego?

Refaktoryzacja to jeden z najważniejszych procesów w programowaniu. Jest to nieustająca walka o jak najlepszą jakość, wydajność oraz czytelność kodu. Refaktoryzacja powinna być wykonywana systematycznie oraz na bieżąco. Jest to proces szczególnie istotny w oprogramowaniu, które jest zaniedbane lub ma już za sobą swoje lata świetności. Jest ona również bardzo ważna w projektach, w których wdrażanie nowych funkcji/mechanizmów jest bardzo trudne. Dobrze przeprowadzona refaktoryzkcja skutkuje ogólną poprawą jakości kodu.

Refaktoryzacja często jest wynikiem zaciągnięcia tzw. długu technicznego, czyli wybrania pozornie łatwiejszej/szybciej ścieżki do osiągnięcia jakiegoś celu, która w dłuższej perspektywie staje się mniej opłacalna. jeżeli chciałbyś dowiedzieć się więcej na temat długu technicznego polecam zapoznać się z tym artykułem: Dług techniczny – co to jest (przyczyny i przykłady)

Rafaktoryzacja skryptu do modelu obiektowego

Jak wspomniałem wcześniej w ramach tego artykułu zajmiemy się refaktoryzacją skryptu utworzonego w jednym z poprzednich artykułów, a mianowicie skryptu kalkulatora:

print("Pierwsze koty za płoty - Python - Kalkulator") shouldExit = False while not shouldExit: print() print("Dostępne opcje:") print("1 - Dodawanie") print("2 - Odejmowanie") print("3 - Mnożenie") print("4 - Dzielenie") print("5 - Wyłącz") try: print() print("Przed wyjątkiem") option = int(input("Wybierz opcję: ")) print() if option == 1: print("Dodawanie") a = int(input("Podaj liczbę: ")) b = int(input("Podaj kolejną liczbę: ")) print(f"Wynik dodawania to: {a + b}") elif option == 2: print("Odejmowanie") a = int(input("Podaj liczbę: ")) b = int(input("Podaj drugą liczbę: ")) print(f"Wynik odejmowania to: {a - b}") elif option == 3: print("Mnożenie") elif option == 4: print("Dzielenie") elif option == 5: shouldExit = True else: print("Błedny wybór spróbój ponownie") except ValueError: print("Wybrałeś nieprawidłową opcję. Uruchom program ponownie") except Exception: print("Nieznany błąd, spróbuj uruchomić program ponownie")

Klasa Calculator – wyodrębnienie operacji matematycznych do osobnej jednostki

Pierwszym krokiem, którym się zajmiemy w ramach refaktoryzacji będzie wyodrębnienie kodu, związanego z realizacją obliczeń matematycznych, do osobnej klasy o nazwie Calculator. Analizując powyższy kod możesz zauważyć, iż aktualna wersja skryptu wspiera akcje takie jak: dodawanie, odejmowanie, mnożenie i dzielenie. Są to podstawowe operacje, które powinny zostać zaimplementowane w ramach nowo tworzonej klasy. Powinna ona zawierać 4 metody, wykonujące te operacje.
class Calculator: def add(self, num1: int, num2: int): return num1 + num2 def subtract(self, num1: int, num2: int): return num1 - num2 def multiply(self, num1: int, num2: int): return num1 * num2 def divide(self, num1: int, num2: int): if (num2 == 0): raise ZeroDivisionError return num1/num2

Jak możesz zauważyć powyższa klasa implementuje wszystkie operacje, które wcześniej były zaimplementowane w skrypcie. Elementem, na który warto zwrócić więcej uwagę jest metoda divide, ponieważ oprócz wykonania operacji matematycznej na zmiennych, sprawdza czy czasem dzielnik nie jest równy 0. W przypadku kiedy, jego wartość jest równa 0 to rzuca wyjątek, w wyniku którego wykonywanie tej operacji zostanie przerwane. W związku z tym możesz być pewny, iż każde wywołanie tej metody sprawdzi wartość dzielnika i w razie wykrycia problemu zawsze zwróci wyjątek ZeroDivisionError.

Kod źródłowy powyższej klasy jest spójny oraz przejrzysty. Jest ona odpowiedzialna tylko i wyłącznie za dokonanie obliczeń matematycznych. Dzięki temu skrypt wykorzystujący klasę Calculator nie musi zawierać kodu związanego z konkretnymi obliczeniami. Wystarczy tylko, iż wywoła odpowiednią funkcję klasy Calculator. Ukrywanie skomplikowanej logiki w klasach jest bardzo użyteczne, ponieważ pozwala na współdzielenie tej samej logiki w różnych miejscach w kodzie bez konieczności kopiowania kodu.

Skoro udało Ci się już przenieść całą logikę obliczeń matematycznych do osobnej klasy, należy teraz posprzątać trochę główny skrypt. W tym celu zamienimy wszystkie operacje matematyczne na wywołania metod powyższej klasy. Poniżej znajduje się kod skryptu po zmianach:

from calculator import Calculator print("Pierwsze koty za płoty - Python - Kalkulator") shouldExit = False calculator = Calculator() while not shouldExit: print() print("Dostępne opcje:") print("1 - Dodawanie") print("2 - Odejmowanie") print("3 - Mnożenie") print("4 - Dzielenie") print("5 - Wyłącz") try: print() print("Przed wyjątkiem") a = int(input("Podaj liczbę:")) b = int(input("Podaj kolejną liczbę:")) option = int(input("Wybierz opcję: ")) print() if option == 1: print("Dodawanie") print(f"Wynik dodawania to: {calculator.add(a, b)}") elif option == 2: print("Odejmowanie") print(f"Wynik odejmowania to: {calculator.subtract(a, b)}") elif option == 3: print("Mnożenie") print(f"Wynik mnożenia to: {calculator.multiply(a, b)}") elif option == 4: print("Dzielenie") print(f"Wynik dzielenia to: {calculator.divide(a, b)}") elif option == 5: shouldExit = True else: print("Błedny wybór spróbój ponownie") except ValueError: print("Wybrałeś nieprawidłową opcję. Uruchom program ponownie") except Exception: print("Nieznany błąd, spróbuj uruchomić program ponownie")

Przyjrzymy się chwilę zmianom dokonanym powyżej. Pierwszym elementem na który warto zwrócić uwagę jest linia nr.1

from calculator import Calculator

Zadaniem tej linii jest wskazanie interpreterowi, iż kod wykorzystuje klasę Calculator z pliku/pakietu calculator. Bez tej linii nie byłoby możliwości uruchomienia skryptu, ponieważ interpreter nie wiedziałby gdzie szukać informacji o tej klasie.

Console – wyodbrędnienie logiki obsługi wejścia/wyjścia

Kolejnym krokiem procesu refaktoryzacji skryptu, który warto byłoby wykonać jest zamiana wywołań metod print oraz input. Przeniesienie kodu związanego z obsługą mechanizmów wejścia/wyjścia (odczytu danych z konsoli i wypisania ich na ekranie), pozwoli na uniezależnienie głównego skryptu od konkretnych operacji związanych z konsolą. Dzięki temu kod skryptu stanie się mniej zależny od terminala, platformy czy też środowiska uruchomieniowego. Na początku skupmy się na utworzeniu klasy Console. Jej kod możesz zobaczyć poniżej:

class Console: def write_line(self, text: str): print(text) def read_line(self, prompt): return input(prompt) def read_line_as_int(self, prompt): return int(input(prompt))

Jak możesz zobaczyć jedynym zadaniem tej klasy jest ukrycie mechanizmu obsługi wiersza poleceń za metodami write_line, read_line, read_line_as_int.

Przejdźmy teraz do refaktoryzacji głównego skryptu, tak aby go uprościć i wykorzystać w nim nowo utworzoną klasę Console:

from calculator import Calculator from console import Console def print_options(console: Console): console.write_line('') console.write_line("Dostępne opcje:") console.write_line("1 - Dodawanie") console.write_line("2 - Odejmowanie") console.write_line("3 - Mnożenie") console.write_line("4 - Dzielenie") console.write_line("5 - Wyłącz") console.write_line('') shouldExit = False console = Console() calculator = Calculator() console.write_line("Pierwsze koty za płoty - Python - Kalkulator") while not shouldExit: print_options(console) try: option = console.read_line_as_int("Wybierz opcję: ") if option == 5: shouldExit = True break a = console.read_line_as_int("Podaj liczbę: ") b = console.read_line_as_int("Podaj kolejną liczbę: ") result = 0 if option == 1: result = calculator.add(a, b) elif option == 2: result = calculator.subtract(a, b) elif option == 3: result = calculator.multiply(a, b) elif option == 4: result = calculator.divide(a, b) else: console.write_line("Błedny wybór spróbój ponownie") continue console.write_line(f"Wynik to: {result}") except ZeroDivisionError: console.write_line("Nie można dzielić przez 0") except ValueError: console.write_line("Wybrałeś nieprawidłową opcję. Uruchom program ponownie") except Exception: console.write_line("Nieznany błąd, spróbuj uruchomić program ponownie")

Zobacz, iż nie zawiera on żadnej konkretnej logiki obsługi operacji matematycznych czy pobierania/wypisywania informacji na ekranie. Zamiast tego do obsługi tych elementów wykorzystuje klasy, które są pewną warstwą abstrakcji. Dzięki takiemu podejściu i ukryciu większości logiki za klasami, w łatwy sposób możesz je wymieniać na inne mechanizmy. Przyjmijmy, iż chciałbyś zmienić logikę wyświetlania informacji tak, aby dodatkowo była zapisywana do pliku tesktowego.

ConsoleFile – pobieraj dane z konsoli zapisuj wyjście do pliku

Aby rozszerzyć działanie klasy Console o obłsugę zapisu do pliku, wykorzystamy jeden z podstawowych mechanizmów obiektowych języków programowania, a dokładniej dziedziczenie. Pozwala on na utworzenie klasy potomnej, która rozszerza bądź modyfikuje działanie klasy bazowej.
W Pythonie dziedziczenie polega na dziedziczeniu metod i atrybutów z jednej klasy przez inną klasę, nazywaną klasą pochodną lub klasą potomną. Klasa pochodna może używać metod i atrybutów z klasy bazowej i implementować dodatkowe metody lub atrybuty, które są specyficzne dla danej klasy.
Aby stworzyć klasę pochodną, należy użyć składni: “class NazwaKlasyPochodnej(NazwaKlasyBazowej):“. Metoda __init__ klasy pochodnej może wywołać metodę __init__ klasy bazowej dzięki funkcji super(), co umożliwia dziedziczenie konstruktora klasy bazowej.
from console import Console class ConsoleFile(Console): def __init__(self, file_name): Console.__init__(self) self.file = open(file_name, "w") def write_line(self, text: str): super().write_line(text) self.file.write(f"{text}\r\n") self.file.flush() def close(self): self.file.close()

W powyższym kodzie możesz zobaczyć, iż klasa ConsoleFile dziedziczy po klasie Console. Klasa pochodna rozszerza konstruktor klasy bazowej oraz wymaga podania ścieżki do pliku, w którym mają być zapisywane informacje. Kolejnym ważnym aspektem jest rozszerzenie metody write_line o kod związany z zapisywaniem tej samej wiadomości do pliku.

Klasa ConsoleFile w linii 7 otwiera plik o przekazanej nazwie wraz z prawami do zapisu (jeśli plik istnieje powinien zostać nadpisany. Świadczy o tym przekazanie parametru ‘w’ jako drugiego argumentu funkcji open. Wykorzystuje do tego wbudowaną w Pythona metodę open. Otwiera/tworzy ona nowy zasób systemowy w postaci pliku, który powinien zostać zamknięty, gdy tylko nie będzie już potrzebny, w celu zwolnienia zasobów systemowych. Służy do tego metoda close() wywoływana w linii 15.

Finalna wersja skryptu

Skoro posiadasz już klasę, która potrafi wypisywać informację do pliku oraz na ekranie konsoli należy teraz dokonać zmiany w głównym skrypcie tak aby korzystał z nowej klasy:

from calculator import Calculator from console import Console from console_file import ConsoleFile def print_options(console: Console): console.write_line('') console.write_line("Dostępne opcje:") console.write_line("1 - Dodawanie") console.write_line("2 - Odejmowanie") console.write_line("3 - Mnożenie") console.write_line("4 - Dzielenie") console.write_line("5 - Wyłącz") console.write_line('') shouldExit = False console = ConsoleFile('output.txt') calculator = Calculator() console.write_line("Pierwsze koty za płoty - Python - Kalkulator") while not shouldExit: print_options(console) try: option = console.read_line_as_int("Wybierz opcję: ") if option == 5: shouldExit = True console.close() break a = console.read_line_as_int("Podaj liczbę: ") b = console.read_line_as_int("Podaj kolejną liczbę: ") result = 0 if option == 1: result = calculator.add(a, b) elif option == 2: result = calculator.subtract(a, b) elif option == 3: result = calculator.multiply(a, b) elif option == 4: result = calculator.divide(a, b) else: console.write_line("Błedny wybór spróbój ponownie") continue console.write_line(f"Wynik to: {result}") except ZeroDivisionError: console.write_line("Nie można dzielić przez 0") except ValueError: console.write_line("Wybrałeś nieprawidłową opcję. Uruchom program ponownie") except Exception: console.write_line("Nieznany błąd, spróbuj uruchomić program ponownie")

W celu wykorzystania nowego mechanizmu obsługi wejśca/wyjścia w głównym skrypcie należy zaimportować poprzednio utworzoną klasę ConsoleFile oraz utworzyć jej instancję przekazując nazwę pliku, do którego powinna zapisywać informacje. Dodatkowo, ze względu na to, iż nowa klasa korzysta z plików musimy pamiętać o zwolnieniu zasobów przy zamykaniu aplikacji. W tym celu należy dodać wywołanie metody close w linii 30 skryptu.

Efektem uruchomienia skryptu powinno być utworzenie nowego pliku o przykładowej zawartości:

Pierwsze koty za płoty - Python - Kalkulator Dostępne opcje: 1 - Dodawanie 2 - Odejmowanie 3 - Mnożenie 4 - Dzielenie 5 - Wyłącz Wynik to: 7 Dostępne opcje: 1 - Dodawanie 2 - Odejmowanie 3 - Mnożenie 4 - Dzielenie 5 - Wyłącz

Powyżej znajduje się finalna wersja skryptu. W ramach tego artykułu miałeś okazję lepiej zapoznać się z programowaniem obiektowym. Przeprowadziłem Ciebie również, krok po kroku, przez proces refaktoryzacji, w wyniku którego skrypt stał się czytelniejszy oraz zaczął działać na warstwie abstrakcji.

Mam nadzieję, iż lektura była interesującą. Kod kalkulatora jest dostępny na moim GitHubie pod adresem: https://github.com/devkotpl/python-calculator

Idź do oryginalnego materiału