Tematem niniejszego materiału jest reużywalność w programowaniu, jedna ze świętych zasad pisania dobrego kodu, obojętnie w jakim języku i paradygmacie 💥! Poruszę jedno z najważniejszych zagadnień jakie dotyczy teorii pisania profesjonalnego kodu źródłowego 🚀! Jak jesteś zaciekawiony(-a), to zapraszam ⏩!

WYJAŚNIAM NA CZYM POLEGA REUŻYWALNOŚĆ W PROGRAMOWANIU!

Dobry kod może być częściowo pojęciem względnym, bo jednak gdzieś tam na końcu przemawia przez nas gust i nawyki zapewniające nam komfort, aczkolwiek są takie cechy kodu, które doświadczony programista wyłapie i są wpisane jako dobre uniwersalne praktyki ✅. Jedną z takich praktyk jest reużywalność, więc wyjaśnimy sobie najpierw sam termin, a potem przejdę do ukazania prostego przykładu 👍.

Czym jest reużywalność w programowaniu?

W programowaniu, reużywalność oznacza sposób pisania takich fragmentów kodu (funkcji, klas bądź jakichkolwiek innych struktur), które mogą zostać powtórnie użyte w wielu różnych miejscach i co najważniejsze, bez względu na zastosowanie, problem czy budowę pozostałej części kodu 🌟!

Obojętnie nad czym pracujemy, czego dotyczy zadanie i w jakim znajdujemy się pliku źródłowym bądź skrypcie (zależnie od języka programowania ℹ️), możemy z takiego kodu skorzystać, podstawić we właściwe miejsce i ono zagwarantuje nam oczekiwane działanie 🤩! W ten sposób kod piszemy jeden raz, a potem tylko powielamy jego użycie w całym projekcie, czyli "reużywamy" go 💡.

Teraz opiszę jak ten kod powinien wyglądać, aby stał się reużywalny ✍!

Jakie warunki musi spełniać kod, aby był reużywalny?

Aby dany fragment kodu mógł być reużywalny, musi gwarantować poprawność działania niezależnie od miejsca i zastosowania 🎯. Innymi słowy, wynik we wszystkich wystąpieniach jest poprawny i nie dochodzi do żadnych błędów ✅. Dotyczy to również wprowadzania późniejszych modyfikacji, czyli np. dodanie nowych parametrów wpływających na działanie kodu, nie powoduje żadnych usterek w poprzednio dodanych wystąpieniach sprzed modyfikacji 🔥.

Prostym przykładem jest struktura punktu w układzie współrzędnych 🙂. Punkt na świecie jest jedną z tych danych, którą charakteryzuje uniwersalność. Gdy raz zdefiniujemy sobie taką strukturę, to później nieważne czy użyjesz jej do wyznaczania rozdzielczości obrazu graficznego, pozycji pionka na planszy do gry w szachy czy prędkości lotu pocisku w grze (jako wektor) 😄. To będzie działać w sposób oczekiwany w każdym przypadku ❤️! Jedna budowa, wiele "użyć" 😊.

Z kolei przykładem modyfikacji reużywalnego kodu nienaruszającej poprzednio dodanych wystąpień, może być sytuacja, w której dodajemy nowy parametr do przypisania pozycji w osi Z (głębia), a wcześniej była to reprezentacja dwóch wymiarów 🙂. Wtedy możemy zdecydować, aby ten parametr był opcjonalny (nie był konieczny do wprowadzenia) i w przypadku jego braku, przypisujemy wartość 0 (zero) 👍. To sprawi, że od tej pory będziemy mogli skorzystać z tej struktury z określeniem pozycji w osi Z lub bez niego, natomiast wszystkie poprzednie "użycia" pozostaną nienaruszone 🚀!

Czas na prosty przykład kodu źródłowego 🔔!

Przykład kodu źródłowego

Oto kod w języku C z ukazaniem kilku przykładów reużywalności 👇:

Struktura "Point"
#ifndef POINT_H
#define POINT_H

typedef struct
{
	float x;
	float y;
	float z;
} Point;

#endif

Tu mamy strukturę (typ "struct") do tworzenia punktów w układzie współrzędnych (połączone z "typedef", aby móc posługiwać się nazwą "Point" w każdym miejscu ℹ️) 👍. To jest jeden z reużywalnych elementów ✅. Zamiast każdorazowego definiowania dwóch czy trzech zmiennych typu "float" tam, gdzie jest to potrzebne, posługujemy się tym i nie narażamy się na duplikację tego samego kodu 😊!

U góry znajduje się zabezpieczenie nazywane jako "include guard", aby zapobiec wielokrotnemu dołączaniu tej samej treści podczas działania preprocesora 💡.

Plik nagłówkowy "math.h"
#ifndef MATH_H
#define MATH_H

#include "point.h"

double getEuclideanDistanceBetween(Point* pointA, Point* pointB);
double getSquareOf(double value);

#endif

To jest plik nagłówkowy dla prototypów funkcji do wyznaczania długości odcinka pomiędzy punktami i kwadratu podanej liczby ⭐. Dołączamy plik nagłówkowy dla definicji struktury punktu, ponieważ wymieniamy jego typ w miejscu parametrów jednej z funkcji 🙂. Tu tak samo obejmujemy całą treść" include guard" z tego samego powodu, co wyżej 🔒.

Zaraz się dowiesz dlaczego jest napisane w takiej formie 😉.

Plik źródłowy "math.c"
#include "math.h"
#include <math.h>

double getEuclideanDistanceBetween(Point* pointA, Point* pointB)
{
	float differenceX = pointA->x - pointB->x;
	float differenceY = pointA->y - pointB->y;
	float differenceZ = pointA->z - pointB->z;
	
	return sqrt(getSquareOf(differenceX) + getSquareOf(differenceY) + getSquareOf(differenceZ));
}

double getSquareOf(double value)
{
	return value*value;
}

Tym razem mamy definicje obu funkcji i tu już robi się ciekawie 🔥!

Dołączamy 2 pliki nagłówkowe: jeden z prototypami tych samych funkcji (w cudzysłowie), a drugi już jako funkcje matematyczne ze standardowej biblioteki języka C (w nawiasach kątowych), gdyż potrzebujemy funkcji "sqrt" do wyliczenia pierwiastka kwadratowego 😊.

Niżej znajdują się definicje obu funkcji. Pierwsza z nich wyznacza nam długość odcinka w trzech wymiarach 1️⃣, a druga zwraca kwadrat podanej liczby 2️⃣. Tu jest kolejny fragment reużywalności w kodzie ✅!

Jeżeli popatrzysz na wzór na długość odcinka, to dostrzeżesz, że każdy składnik sumy pod pierwiastkiem jest wyznaczamy tak samo dla każdej pojedynczej składowej punktu 😮! To oznacza, że fragment obliczenia, w którym podnosimy te same składowe obu punktów do kwadratu, możemy sobie "zwinąć" do osobnej funkcji i następnie trzykrotnie jej "reużyć" 🤩!

Czyniąc to w ten sposób, unikamy powtarzania pisania tego samego wyrażenia 👇:

float getEuclideanDistanceBetween(Point* pointA, Point* pointB)
{
	float differenceX = pointA->x - pointB->x;
	float differenceY = pointA->y - pointB->y;
	float differenceZ = pointA->z - pointB->z;
	
	return sqrt(differenceX*differenceX + differenceY*differenceY + differenceZ*differenceZ);
}

Ponadto, możesz skorzystać z tej funkcji również do innych celów i z każdego innego miejsca w programie 🚀! Czyli mamy 2 zalety takiego podejścia 😲!

Możesz w tej chwili sobie myśleć, że to trochę niepotrzebne "bicie piany" 😜, natomiast jeżeli Ci przyszło (albo przyjdzie) pracować nad dowolnym dużym projektem, to im większy on jest, tym bardziej docenisz reużywalność w programowaniu 😁!

Został nam do ukazania plik uruchomieniowy każdego programu w języku C, "main" 😊!

Plik źródłowy "main.c"
#include "math.h"
#include "point.h"
#include <stdio.h>

void printEuclideanDistanceBetween(Point* pointA, Point* pointB);

int main(void)
{
	Point pointA = {3, 6};
	Point pointB = {7.5f, 10};
	Point pointC = {9.5f, 15.5f, 1};

	printEuclideanDistanceBetween(&pointA, &pointB);
	printEuclideanDistanceBetween(&pointA, &pointC);
	printEuclideanDistanceBetween(&pointB, &pointC);
	
	return 0;
}

void printEuclideanDistanceBetween(Point* pointA, Point* pointB)
{
	printf("%f\n", getEuclideanDistanceBetween(pointA, pointB));
}

Tu mamy miejsce wywołania funkcji 🔥! Na górze znajduje się garść dołączonych plików nagłówkowych 👇:

  • do wywołania funkcji wyznaczającej długość odcinka pomiędzy dwoma punktami,
  • do definicji struktury tworzącej punkt,
  • do wypisywania wyników na strumień wyjściowy przy użyciu funkcji "printf".

Zaraz po tym, kolejny prototyp funkcji wypisującej długość odcinka pomiędzy dwoma punktami. To następna część reużywalności ✅! Szczegóły opiszę za moment 🙂.

W środku funkcji "main" tworzymy sobie 3 punkty z różnymi współrzędnymi i zwróć uwagę, że tylko ostatni z nich ma zdefiniowaną pozycję w osi Z 😊. Reszta punktów przyjmie wartość 0 (zero), natomiast to nie powoduje żadnego ryzyka błędów w implementacji, zatem struktura jest reużywalna ☑️. Mamy 3 struktury, a nie 9 zmiennych typu "float" 😉. Teraz wyobraź sobie ten niepotrzebny bałagan w wielu różnych plikach źródłowych 💥.

Następnie wywołujemy sobie trzykrotnie tę samą funkcję do wypisywania długości odcinka 👍. Jedyną różnicą między tymi wywołaniami są punkty wstawiane w miejsce parametrów. Poniżej funkcji "main" znajduje się definicja tej funkcji, a w niej mamy wywołanie "printf" z właściwym specyfikatorem formatu dla wyniku zwracanego przez funkcję obliczającą długość odcinka pomiędzy dwoma punktami ("%f" jest przeznaczony dla typów "float" i "double" ℹ️) 😄.

Wyodrębnienie wywołania "printf" do osobnej funkcji daje nam taką zaletę, że określamy specyfikator tylko jeden raz przez co nie narażamy się na pomyłki przy każdorazowym pisaniu tego samego 😲! Oprócz tego, gdy kiedykolwiek zechcesz dodać własny komunikat do wyniku, będziesz musiał(a) wprowadzić modyfikację tylko w jednym miejscu ❤️! Forma wypisywania jest identyczna, wyznaczanie wyniku też, więc funkcja jest reużywalna ☑️!

Czas na prezentację 👀!

Aby uruchomić ten program, musisz dołączyć wszystkie wspomniane pliki nagłówkowe i źródłowe podczas wprowadzania polecenia kompilacji ⚠️. Na przykładzie GCC, będzie to wyglądać tak 👇:

gcc -o main.exe point.h math.h math.c main.c

Uruchomienie programu spowoduje wypisanie na konsolę 3 wartości oznaczających odległość pomiędzy dwoma punktami, zgodnie z wywołaniami funkcji 😊. Pytanie czy dostrzegasz jedną ważną rzecz tej funkcji 🙂.

Wyznaczanie odległości operuje na punktach w przestrzeni trójwymiarowej, czyli oś X, Y oraz Z. Natomiast 2 punkty zostały zdefiniowane bez użycia osi Z, natomiast czy to w jakikolwiek sposób wpłynęło niekorzystnie na jej działanie? Nie 😁! To jest niezbity dowód reużywalności funkcji 🏆!

Liczba wymiarów nie wpływa na wynik i jeżeli jest potrzeba wyznaczenia odległości na przestrzeni dwuwymiarowej (oba punkty przyjmują wartość w osi Z równą zero), to wynik zostanie bez problemu wyznaczony ✅.

Wiem, że ten przykład może w paru miejscach wyglądać na przesadzony, bo np. definiowanie funkcji dla wyznaczenia kwadratu podanej liczby może okazać się bardziej czasochłonne, niż wstawienie wyrażenia "x*x" i tyle 😄. Chciałem tylko pokazać jak bardzo takie podejście wpływa na dalsze pisanie kodu, a w większości przypadków potrafi naprawdę uprzyjemnić pracę i zmniejszyć ryzyko powstawania błędów 🏆!

Reużywalność w programowaniu

Reużywalność w programowaniu oznacza pisanie kodu wielokrotnego użytku, który bez względu na struktury danych, kontekst czy zastosowanie, potrafi w każdym przypadku wyznaczyć prawidłowy wynik, a późniejsze modyfikacje tego kodu nie naruszają poprawności wszystkich poprzednich wystąpień.

Ostatnia rzecz na koniec. Pamiętaj, że możliwości czy sposoby zapisu kodu celem uczynienia go reużywalnym, zależą od możliwości i zasad narzucanych przez język programowania ⚠️. Na przykład gdy korzystasz z podejścia obiektowego (podział programu na klasy i obiekty), to definiowanie klas potomnych od klasy abstrakcyjnej również jest elementem reużywalności, ponieważ przy odwoływaniu się do poszczególnych obiektów poprzez klasę bazową (polimorfizm), możesz wywołać tę samą metodę (której postać jest zawarta w treści klasy abstrakcyjnej), a każda klasa potomna zawierająca jej definicję, "zareaguje" na to we własny indywidualny sposób 💡. Mamy jedną metodę i wiele "użyć" 🙂.


I na tym kończę temat reużywalności w programowaniu 🙂. Jeżeli zależy Ci na byciu zawodowym programistą, to jest obowiązkowa rzecz do wprowadzenia w swoją praktykę 🔥!

PODOBNE ARTYKUŁY