Semantyka w kodzie, by po roku było nadal jasno

Wstęp

Cześć moje pysie mysie ❤️

Zanim przejdziemy do części merytorycznej to chciałbym powiedzieć, że jestem w trakcie pisania innego artykułu nt. JavaScript-owego kodu o zamianie tekstu na emotki. Głównie ze względu na to jak ostatnio mało przy komputerze spędzam czasu ten artykuł tak długo tworzę. Liczę jednak na to że skończę go na początku września.

To tyle z ogłoszeń, a jeśli chodzi o nazywanie wszystkiego co masz w kodzie to tutaj nie ma jednej prostej drogi. Jeszcze trzeba się nastawić na to, że tutaj będę rzucał nazwami na lewo i prawo – nie zrażać się tylko googlować 😘 (z resztą ja przez google się o wszystkim dowiedziałem)

Liczenie WTFów

Jest to jedna z ciekawszych metod wyszukiwania rzeczy które mają złe nazwy. Używając tego też warto znać wzorce projektowe jak i kilka różnych zasad programowania.

Dobra zatem co tu trzeba robić? Weź sobie jakiś swój stary kod i wstaw sobie WTF za każdym razem gdy coś nie jest jasne na pierwszy rzut okiem. Nie dochodź o co chodzi: nie wiesz to nie wiesz i masz +1 WTF. Na końcu liczysz ile masz WTFów – im więcej tym gorzej.

Też fajnie by było gdyby ktoś bardziej doświadczony zrobił Ci code review i wystawił WTFy (albo lepiej konstruktywne komentarze) w kodzie.

Na takiej podstawie będziesz wiedzieć co jest do zmiany, a jeśli masz komentarze to możliwe że dowiesz się nawet jak te WTFy zmienić.

Żeby nie było, że sobie to wymyśliłem – programistyczne WTFy to nic nowego, a na internecie znajdziecie wiele stron gdzie polecają taką praktykę. No, a co znaczy ten skrót to już każdy powinien wiedzieć 🙃

Nie wymyślaj nic nie znaczących nazw

Są specjalne programy od tego jak bardzo nieczytelny ma być kod, chociaż inaczej się definiuje wszelkiego rodzaju aplikacje od minifikacji.

Ty swój kod pisz tak, aby tych WTFów było jak najmniej. Często spotykałem się z tym jak ktoś nazywa funkcje czy klasę „MojeCoś” albo „Moje2”. To że twoje to każdy się domyśli. No ale co to znaczy? Co to robi?

Lepiej używać nazw które coś znaczą

Więc tutaj też najlepiej znać wzorce projektowe dzięki czemu część nazwy masz już z samego wzorca. Piszesz coś co obsługuje zapytania do bazy danych? Pewnie chodzi o Repository. Piszesz coś czym przekażesz proste dane? Możliwe że użyjesz DTO. Chcesz zmienić stan aplikacji? Napiszesz Command (i pewnie CommandHandler) itd. itd.

Też mocno pomaga w nazywaniu to jakie masz namespaces albo po prostu ścieżki. Jeśli wszystko do zarządzania userami wstawisz w /src/UserBundle i jeszcze tam stworzysz konkretne podkatalogi to jesteś prawie w domu. Ja podobnie nazywam namespaces: oddają one konkretne katalogi – a przynajmniej jest to znana mi praktyka z języka PHP (ułatwia mocno autoload), ale nie wiem czy w innych językach też tak się robi

Fajnie też gdy uczysz się wzorców projektowych, wtedy one (gdy wiesz jakie użyć) podpowiadają część nazwy. Tworzysz fasadę? – no to w nazwie masz zawarte Facade. Robisz fabrykę? – Factory. Adapter? – no tutaj akurat tak samo Adapter. Większość nazw wzorców też oddaje to czym się zajmują: Builder buduje, Factory produkuje, a czym? no np. builderami 😄Dlatego też zawszę będę polecać naukę wzorców, bo one są własnie po to, aby nam pomóc pisać kod, zwłaszcza taki, który wiemy, że trzeba go dużo zrobić.

Metody też fajnie nazywać tak jakby opisywały konkretną czynność. Tak jak bywa że nazwa klasy to coś w rodzaju Rzeczownika to metody to są czymś w rodzaju Czasownika. Mówię, że coś tego rodzaju, bo są odstępstwa od tych reguł jak np. UserCreated, które jest eventem, a coś co nasłuchuje po nim może się nazywać UserCreatedSubscriber, a metoda która będzie wywoływana to może być zwykły invoke. Jak chcesz coś dodać np. linki społecznościowe pod usera to masz w klasie User metodę addSocialLink, ew. jeśli ma być jeden tylko link to robisz setSocialLink (że ustaw, no bo już dodawać wtedy nie trzeba). Podobnie jest w przyrodzie – masz obiekt np. ptactwo i ono to co umie robić (czyli metody) to są ich czynności np. latają, siedzą, jedzą itd. Jeszcze z realnego życia można wziąć przedmioty nieożywione jak np. stół, któremu można nadać konkretną pozycję czyli stół da się przemieścić. Stół::przemieść, a z normalnego na nasze Table::setPosition 🤣

Przykład kodu (troszkę abstrakcyjnie)

Ja programuje aplikacje backendowe więc jeśli mam coś napisać to zaczynam od kontrolera, który działa pod jakimś tam URL i przyjmuje od zapytania jakieś argumenty (często to jest ciało zapytania). Czyli jest jakiś Route, który kieruje wykonanie kodu do konkretnej metody kontrolera tzw. akcji.

Załóżmy że tworzymy użytkownika. Jako iż wiem, że większość z tego co wymyślę będzie często ponownie wykorzystywane to też zaplanuje cały proces tworzenia kodu. Uwaga, cześć może być na początku nie zrozumiała:

  • Będę operować na agregacie User. W moim przypadku będzie to też Encja. Klasa zatem będzie nazywać się User (tak łatwo się nazywa encje czy niektóre domeny).
  • Będę zmieniać stan bo będzie tworzony User, wiec będzie potrzebny WriteModel. U mnie sprawdzi się coś co utworzy na podstawie Encji User nowe dane w bazie danych. Będzie to UserRepository
  • Skoro będzie zmieniany stan to na podstawie wzorcu CQRS utworze Command i CommandHandler i nazwę je CreateUserCommand oraz analogicznie CreateUserCommandHandler. Command będzie swego rodzaju DTO, a Handler będzie miał w sobie logikę tworzenia Usera (będzie też jasno gdzie jest tworzenia Usera)
  • OK. Jeszcze coś co będzie warstwę wyżej, coś co wychwyci argumenty i przekaże je do Commanda. Tutaj załóżmy, że tworzymy REST API, przez które da się utworzyć Usera, więc przyda nam się controller od Userów i jego akcja do tworzenia Userów. To będzie nasza tzw. warstwa Port. Odpowiedni Route przekieruje request na akcję.

Załóżmy, że argumentami są wysyłane w zapytaniu nazwa, hasło i email użytkownika.

Teraz ułóżmy sobie to w odpowiedniej kolejności:

  1. Route np. PUT /api/user, który przekieruje request na akcję UserController::create (nie, to nie będzie klasa statyczna, pomimo tego zapisu)
  2. UserController na podstawie argumentów uzupełni CreateUserCommand (a tak w zasadzie to ten Command oczekuje tych konkretnych danych)
  3. CreateUserCommand w „jakiś sposób” będzie użyty do utworzenia Usera. Tutaj jest wiele możliwości i też wiele zależy jaki język jest używany. w C# robi to się inaczej niż w PHP. Ja pokaże jeden z łatwiejszych/bardziej zrozumiałych sposobów czyli zwykłe wywołanie CommandHandlera w kontrolerze
  4. CreateUserCommandHandler używając danych z CreateUserCommand tworzy nową encję User ustawia jej poszczególne własności (np. konstruktorem albo setterami albo osobnymi serwisami – ważne żeby po prostu to osiągnąć)
  5. CreateUserCommandHandler używając utworzonej encji User zapisuje ją w bazie danych (czyli tworzy stan) używając UserRepository::save (to też nie jest klasa statyczna)
  6. Skoro to Command, a nie Query (o co z tym chodzi to zapraszam do zapoznania się z CQRS), a route to PUT, a nie GET to nie zwracamy żadnych danych w response – czyli w odpowiedzi jest tylko status HTTP 201, który wprost mówi o tym, że został utworzony stan.

Załóżmy jeszcze jedną rzecz taką dla ułatwienia: przykład kodu będzie na fikcyjnym frameworku z mechanizmem autowired , który jeszcze ma menadżer encji – dzięki temu nie będę musiał wszystkiego pisać 😇

Wiem, że teraz nawaliłem jakimiś nazwami i rzeczami, które mogą wyglądać na nie zrozumiałe, ale ten dodatkowy wysiłek się przyda i ja wiem, że dla niektórych to piszę herezję, no bo po co cała klasa na utworzenie usera i po co na ta encja skoro można po prostu wszystko od razu przekazać do DB i mieć z głowy.. no niby można, ale wtedy pozbawiasz się możliwości fajnego rozszerzenia kodu. Kontroler trzyma wszystkie akcje, które można wykonać pod np. daną encję (u nas User). Można pobierać, zmieniać czy tworzyć, ale i nie tylko. Jeśli np. zapisujemy każde logowanie wraz z datą i godziną to możemy zrobić z tego statystyki logowania. Nasz UserController będzie dosyć prosty: będzie dziedziczyć domyślne funkcje frameworkowego Controller-a i używając atrybutów (czy jak kto woli dekoratorów) przypiszemy do niego konkretny route

import { Controller, Put, Body } from '@someFramework';
import { CreateUserCommand } from './CreateUserCommand';
import { CreateUserCommandHandler } from './CreateUserCommandHandler';
import { UserRepository } from './UserRepository';

// nastawiamy route na /api/user
// to "/api" załóżmy że wzięło się z niewidzialnego configu
@Controller('user')
export class UserController {

  // ustawiamy metodę put na tym create
  // będzie to endpoint PUT /api/user
  @Put()
  // ustawiamy że jeśli wszystko pódzie dobrze, to nam zwroci status http 201 czyli "utworzone"
  @HttpCode(201)
  // To co ma przyjść z requestu ma być zmapowane do CreateUserCommandHandler
  // Czyli ma też ma zawierać te samo pola co ta klasa
  create(@Body() createUserCommand: CreateUserCommand): void
  {
    // tworzymy nasz command handler
    const handler = new CreateUserCommandHandler(new UserRepository);
    // wykonujemy tworzenie usera
    handler.handle(createUserCommand);
    // całą resztą zajme się już framework, a my już nic więcej nie musimy
  }
}

Command jest stricte sporym ułatwieniem i przechowuje najprostsze typy danych. Nasz CreateUserCommand będzie trzymał tylko nazwę, email i hasło usera. Każde jedno typu string. Wtedy też masz jasno z góry ustalony tzw. kontrakt (czyli w naszym przypadku to co przychodzi w body requestu). Command jest swego rodzaju DTO tylko że pod konkretny cel, wszystkie inne DTO-sy mogą być ogólnego celu. Też jeden DTO może wymagać inny DTO jako własność, wtedy fajne drzewko można robić. DTO też może mieć jako własność ObjectValue. Natomiast DTO nie może przechowywać logiki – chodzi o to, aby nie był tym co coś zmienia czy tworzy, a ma być tym dzięki czemu można coś zmienić lub stworzyć. Jego odpowiedzialność jest z góry ustalona. To pudełko na rzeczy, które można tylko wykorzystać, a same z siebie nic nie robią. Też Command/DTO przydaje się jeśli chcesz mieć ustaloną strukturę, a nie chcesz wysyłać jako argument ze 20 zmiennych – wtedy masz tylko jedno DTO.

export class CreateUserCommand {
    name: string;
    email: string;
    password: string;
}

CommandHandler (w naszym przypadku CreateUserCommandHandler) to logika wykorzystania CommandHandlera. Przeważnie jest to dosyć prosta klasa: argumentem jest Command, który z góry ma ustalone jakie są dane i bez krępowania używasz ich. CommandHandler może tworzyć/zmieniać/usuwać stan – w naszym przypadku tworzy (bo mamy w nazwie Create). Jeśli chodzi o to jak swoją pracę wykonuje to już tutaj można albo w nim opisać to co się dzieje albo można stworzyć osobne serwisy, które zrobią za CommandHandler bardziej zaawansowaną robotę. U nas obejdzie się bez serwisu.

import { CreateUserCommand } from "./CreateUserCommand";
import { User } from "./User";
import { UserRepository } from "./UserRepository";

export class CreateUserCommandHandler {
    _userRepo: UserRepository;

    constructor(userRepo: UserRepository)
    {
        this._userRepo = userRepo;
    }

    handle(command: CreateUserCommand) : void
    {
        // tworzy usera
        const user = new User();

        // ustawia usera
        user.name = command.name;
        user.email = command.email;
        user.password = command.password;

        // zapisuje usera
        this._userRepo.save(user);
    }
}

Wszelkie Repozytoria służą do komunikacji z bazą danych. No i tutaj może być różnie, bo mogą używać mechanizmów frameworka, ale czasami trzeba i tak zdefiniować przez np. jakiś QueryBuilder to co chcemy osiągnąć, zwłaszcza jak trzeba relacyjnie się odwoływać. Dla mnie Repozytoria jeśli chodzi o budowę są podobne do Kontrolerów, w sensie dane repozytorium jest miejscem dla różnych akcji pod bazę danych. Jest np. znajdź po ID, albo znajdź po atrybutach, no tylko z tą różnicą, że też można zapisywać. No… chyba że mamy bardziej zaawansowaną aplikację, gdzie zapis i odczyt realizujemy przez osobne repozytoria, bo np. chcemy mieć różne możliwości zapisu. Dla naszego ułatwienia przyjmijmy, że korzystamy z fikcyjnego frameworka, który ma menadżer encji, którym można utworzyć instancję repozytorium dla naszej klasy repozytorium (masło maślane – chodzi oto, że opakujemy frameworkowe repozytorium w ładne metody). Nasz UserReposiotry będzie służyć tylko do zapisywania.

import { em, IEntityRepository } from "@someFramework/EntityManager";
import { User } from "./User";

export class UserRepository {
    _repository: IEntityRepository

    constructor()
    {
        // Tworzy nam repozytorium na podstawie encji User
        // Dzięki temu mamy połączenie z tabelą user w DB
        this._repository = em.getRepository((new User).constructor.name)
    }

    save(user: User) : void
    {
        // Sprawdzamy czy trzeba dodać usera
        if(!this._repository.contain(user)) {
            // Jeśli nie jest zawarty to dodaj
            this._repository.persist(user);
        }

        // Commit do bazy danych
        this._repository.flush();
    }
}

Nasza legendarna Encja User to będzie nic innego jak obiektowy reprezentant naszej bazy danych. Ci co kojarzą ORM to już pewnie wiedzą o co chodzi, a ci co nie.. No zobacz co to ORM 🥲

import { ORMTable, ORMColumn, ORMId } from "@someFramework/ORM";

// Dzięki ORM wiadomo jaką tabele reprezentuje clasa User i jakie ma kolumny

@ORMTable('user')
export class User {

    @ORMId('AI')
    @ORMColumn('int')
    _id: number

    @ORMColumn('varchar(15)')
    _name: string;

    @ORMColumn('varchar(50)')
    _email: string;

    @ORMColumn('varchar(64)')
    _password: string;

    public get id(): number {
        return this._id;
    }

    public get name(): string
    {
        return this._name;
    }

    public set name(value: string)
    {
        this._name = value;
    }

    public get email(): string
    {
        return this._email;
    }

    public set email(value: string)
    {
        // any email valid
        this._email = value;
    }

    public set password(value: string)
    {
        // any password hasher
        this._password = value;
    }
}

Tak trochę specjalnie przykład kodu zawiera błędy – czy potrafisz je wszystkie znaleźć i poprawić? 😉

Gdyby ktoś chciał wiedzieć to trochę (bo nie całkiem) wzorowałem się na użyciu frameworka NestJS – nie umiem go picuś glancuś, więc mój kod na pewno z nim nie zadziała – ale TY możesz to naprawić! 😄

Zasady programowania

Część zasad programowania odnosi się też do nazewnictwa, ale i co powinien zawierać jakościowy kod. Po części też dzięki temu, że wiemy co robić, nie zrobimy czegoś niepotrzebnego, więc i nazywania będzie tyle ile trzeba 😇

Tych zasad jest dużo i ich nauka też trochę schodzi czasu. Też na moim blogu znajdziecie opisy KISS oraz SRP. Chciałbym tak na prawdę opisać wszystkie jakie znam, ale.. kiedy ja na to czas znajdę? 😱 No ale ale, zawsze mogę pomóc podając kilka z nich. Poza wcześniej wypisanymi KISS i SRP na start jeszcze polecam SOLID (literka S oznacza to SRP i każda osobna literka to osobna zasada), YANGI, DRY, TDA oraz SCA. Naukę i kolejność poznawania oczywiście dobrać pod siebie 😘 Opiszę je w osobnych artykułach 😄

Cztery podstawowe konwencje nazewnictwa

Stricte one nie mówią wprost dokładnie jakich słów należy używać, ale używając ich w odpowiedni sposób, twój kod zyska na większej czytelności:

  1. snake_case – chodzi o całe nazewnictwo gdzie słowa są pisane tylko z małych liter, a pomiędzy nimi jest znak podłogi czyli _ – ja to spotykam najczęściej jako nazwy tabel w bazie danych, a wy?
  2. camelCase – moje najbardziej ulubione, jednakże robię od tego wyjątki. Chodzi o to, że nazwa zaczyna się z małej litery, a każde kolejne słowo z Wielkiej – w takim przypadku nie są potrzebne dodatkowe znaki. Ja to używam do nazw zmiennych i funkcji
  3. PascalCase – jest bardzo podobne do camelCase, z tą różnicą, że pierwsze słowo też zaczyna się od Wielkiej litery. Używam tego do nazywania Klas
  4. kebab-case – moim zdaniem bardzo osobliwy przypadek, ale też łatwo można się na to nadziać, zwłaszcza w XML albo wszelkich frameworkach (jak React czy Vue), gdzie tworzysz własne tagi. Jeszcze używane w CSS i jego pochodnych. Na pewno też jest wiele innych przykładów, które można znaleźć, ale póki co tylko to mi przyszło do głowy 😅

Odpowiednie komentarze

Tak jak widać przykład kodu to większość z tych komentarzy (albo w sumie wszystkie) są zbędne. Odpowiednie formatowanie kodu ma sprawić to, że nasz kod jest zrozumiały – trzeba na to spojrzeć jak na książkę: czy lepiej się czyta książkę, gdzie musisz czytać każdy komentarz autora, aby wiedzieć o co chodzi czy po prostu wolisz z czytania treści (bez komentarzy) wiedzieć o czym jest fabuła? Pytanie oczywiście retoryczne – wiadomo, że fabuła naszego kodu musi być jasna 💪

Oczywiście zdarzają się przypadki kiedy wszystko idealnie nazwiesz, no ale.. coś jednak jest nie tak. Ci co znają Regex pewnie wiedzą o co chodzi 🙂

Przykład złego komentarza

Takiego fe fe komentarza nie robić:

// Moja klasa, która jest encją User
class User
{
  // ID usera, które zmuszę, aby było int
  _id: number;
}

No i widać, że te komentarze nic nie znaczą 🙁 – wiadomo, że to klasa i nawet nie ważne, że to encja czy co innego. Tak samo ID, nosz.. wiadomo, że w większości przypadkach musi to być INT, a number zawiera w sobie tez int.

Przykład dobrego komentarza

// Minimum eight characters, at least one letter, one number and one special character
const passwordRegex = "^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$"

Pomimo tego, że nazwa nazwa w pełni oddaje tym czym jest ten obiekt to jednak bez komentarza trudniej rozszyfrować składnię Regex-a. Podobnie w książce czasami pojawi się obcojęzyczne słowo, którego nie znamy i komentarz nam wyjaśnia o co chodzi.

Komentarz – dokumentacja

Na przykładzie jezyka PHP jeszcze pokażę jak pisać dokumentację w kodzie za pomocą komentarzy. Przydaje się to czasami, gdy chcesz wygenerować dokumentację np. stronę internetową pod swój kod:

<?php

namespace App\Example\UserBundle\Domain\EventSubscriber;

use App\Example\UserBundle\Domain\UserRepository;
use App\Example\UserBundle\Domain\Event\UserCreated;

class UserCreatedSubscriber
{
 
    /**
     * @param UserRepository $userRepository
     */
    public function __construct(
        private UserRepository $userRepository
    ){
    }

    /**
     * @param UserCreated $event
     * @return void
     */
    public function __invoke(UserCreated $event) : void
    {
        // .. tutaj jakiś kod
    }
}

Jak widać można się obejść bez tych komentarzy, ale z drugiej strony są one jakoś pomocne. Wielu programistów jest do nich przyzwyczajonych, więc tutaj myślę, że nikt nie skrzyczy za ich robienie – w moim przypadku mam zainstalowaną tyczkę, która sama mi taką dokumentację pisze, a dokładnie to dosłownie piszę znaki /**, a potem wystarczy enter i już mam wszystko na podstawie funkcji / metody.

Komentarz – atrybut

Niektóre języki programowania mogą nawet wymagać takich komentarzy, ale to już zostawię do własnej interpretacji. W Takim języku PHP przed wersją 8 trzeba było atrybuty pisać w komentarzach, co nie zawsze wyglądało dobrze, ale z drugiej strony i tak były tam gdzie miały być, więc jakoś było dobrze.

Tutaj raczej nie ma co pisać przykładów, bo jeśli nie piszesz w PHP to w innych językach programowania, do tego zagadnienia są inne rozwiązania. Np. w TypeScript są @Dekoratory, w które się nie zagłębiałem, ale pewnie dużo robią skoro atrybutów w tym języki nie widziałem

Podsumowanie

Zatem poznaliście kilka tajemnych technik pisania kodu, o które zostałem proszony byłem opisać. Jestem jeszcze w trakcie pisania dwóch innych artykułów – jeden to będzie poradnik z JS, a drugi konkurs, jednakże pewnie wcześniej pojawi się trzeci czyli podsumowanie miesiąca jakim jest Sierpień 😄

Wszystkim zaczynającym szkołę życzę miłych szkolnych dni i wytrwania do kolejnych wakacji 😉 Nauczycielom za to wyrozumiałości, no.. chociaż pewnie bardziej cierpliwość się przyda. Jak wy dajecie radę z tyloma.. hehe 😅 Rodzicom też cierpliwości i wytrwałości, aby wam się chciało na te wywiadówki chodzić no i pamiętajcie polecić swoim dzieciom mój blog, aby wyrosły na utalentowanych i samodzielnych programistów 😘 – będą dużo zarabiać to wam się dołożo do wakacji

Dziękuję i do kolejnego! Meow 😺

Subscribe
Powiadom o
guest
0 komentarzy
Inline Feedbacks
View all comments
Włączyć powiadomienia? Bardzo pragnę Dam sobie radę