Coding Guideline - gala377/TIN GitHub Wiki

Czy muszę to czytać?

W zasadzie skrócę tu na początku zasady czystego kodu i jeżeli uważasz, że dobrze je znasz to możesz je pominąć. Potem jednak będzie o tym jak piszemy kod i to już wolałbym żeby każdy przeczytał i się stosował :)

Po co?

Ogólnie chcemy żeby cały projekt dało się w miarę dobrze czytać. A kod czyta się dobrze jeżeli cały wygląda jak napisany przez jedną osobę. Dlatego wrzucam kilka zaleceń co do pisania kodu. Ale pamiętajcie, jeżeli coś wydaje wam się bardziej czytelne niż jak zrobicie to zgodnie z zasadami to zostawcie tak jak jest.

Ogólne

Nazwy

Pamiętamy o sensownych nazwach zmiennych, klas, namespaców i metod. Tak żeby od razu było wiadomo jakie jest jej przeznaczenie. W szczególności nazwy jednolitere jak:

auto a = newSocket();

Nie mają racji bytu (chyba, że i, j, k w pętli).

Nazwy metod powinny być czasownikami. Nazwy zmiennych powinny być rzeczownikami. Co więcej długość nazwy zmiennej powinna odpowiadać jej scopowi. Czyli okej jest użycie zmiennej o nazwie curr w krótkiej pętli, ale nie jako pole klasy. Lepsze byłoby np. currently_used_socket. Nazwy klas powinny określać rolę klasy np. PacketBuilder - jeżeli to ta klasa robi, albo TCPSocket - jeżeli tym ta klasa jest. Nazwy takie jak Manager nic nie mówią, staramy się ich unikać. Jeżeli klasa realizuje jakąś strukturę danych albo wzorzec projektowy, to nie ma co się wysilać na fancy nazwy typu CarCreator lepiej użyć nazwy wzorca CarFactory.

Pojedyńcza odpowiedzialność

Pamiętamy, że klasy i funkcje obejmuje zasada pojedyńczej odpowiedzialności. Mają robić tylko jedną rzecz i mają ją robić dobrze.

Pamiętamy też, że metoda nie musi wiedzieć wiecej niż musi. Tak samo klasy. Nie przesyłamy przez argument całego obiektu jeżeli używamy z niego tylko jednego parametru.

Ogólnie jak funkcja przyjmuje więcej niż 2 parametry to prawdopodobnie jest zbyt skomplikowana i robi za dużo.

Coś więcej o funkcjach.

Zwracanie przez argument jest tak bardzo C98 i tak bardzo fuuuj. Nie robimy tego. Chyba, że ma sens. Patrz kopiowanie dużego obiektu

Dobra funkcja ma max około 20 linijek. Tyle w temacie.

Grupowanie logiczne

Grupujcie klasy i funckcje w namespacy jeżeli to grupowanie ma sens. Unikamy takiej sytuacji:

enum PackageType { ... }
enum PackageState { ... } 

Zamiast tego grupujemy

namespace Package {
    enum Type { ... }
    enum State { ... }
}

A najlepiej, jeżeli sa na tym jeszcze wykonywane jakieś operacje to

class Package {
    enum Type { ... }
    enum State { ... }
}

Grupowanie wizualne

Porównajcie sobie

class Source {
public:
    void closeFile();

    bool eof() const;

    bool opened() const;

    char getChar();

    char getNextNonBlankChar();

    Source(std::string path);

    char getNextChar();

    ~Source();

    void ungetChar(char ch);

    std::uint32_t line() const;

    std::uint32_t inLinePosition() const;

private:
    std::fstream _file;
    bool _is_file_opened = false;
    std::uint32_t _file_line = 0;
    std::uint32_t _in_line_position = 0;
    char _last_read_ch = '\0';
};
class Source {
public:
    Source(std::string path);
    ~Source();

    void closeFile();

    char getChar();
    char getNextChar();
    char getNextNonBlankChar();

    void ungetChar(char ch);

    std::uint32_t line() const;
    std::uint32_t inLinePosition() const;

    bool eof() const;
    bool opened() const;
private:
    // File information
    std::fstream _file;
    bool _is_file_opened = false;

    // Reading information
    std::uint32_t _file_line = 0;
    std::uint32_t _in_line_position = 0;

    char _last_read_ch = '\0';
};

Grupujemy razem funkcje, które coś mają ze sobą wspólnego - między nimi brak odstepu. Odstęp oznacza kolejne grupowanie. Plus ustawiamy je w kolejności zależności lub złozoności. Np.

    char getChar();
    char getNextChar();
    char getNextNonBlankChar();

Jest zapisane w kolejności złożoności bo każda kolejna metoda rozszerza kolejną.

    std::fstream _file;
    bool _is_file_opened = false;

Jest zapisane w kolejności zależności, bo _is_file_opened jest zależny od _file. Wybieramy to sortowanie, które uważamy, że ma większy sens.

Jak piszemy

typy

W miarę możliwości używamy typów, które zawsze mają taki sam rozmiar na różnych architekturach, więc int odpada. Zamiast tego proszę o std::int32_t i odpowiedniki jak potrzebujemy czegoś innego.

Nie nawalamy w kodzie auto jak powaleni XD. Jasne można, ale potrafi to ukryć informacje o typie, które są przydatne. Przykład dobrego użycia auto:

for(const auto& el : collection) {
    ...
}

Albo:

// Instead of
Syntax::Lexer::Token::Type type = token.type();
// we can use
auto type = token.type()
// we clearly see that the type of variable type is Token::Type

Fory, ify, while, elsy

ogólnie

// for collections use ranges
for(const auto el : collection) { .. }
// folding on the same line
if(cond) {
    ...
} else if(cond) {
   ...
} else {
   ...
}

while(cond) {
    ...
}

Ogólnie nie jestem zwolennikiem konstrukcji:

if(cond)
   single_instruction();

for(int i=0; i < n; ++i)
   process(i);

Sam raczej psizę

if(cond) {
   single_instruction();
}

for(int i=0; i < n; ++i) {
   process(i);
}

Ale tego już nie wymuszam i to możecie robić jak chcecie :P

C++17 ma jeszcze takie fajne rzeczy jak:

// in if and switch initialization
if(auto it = collecion.find(value); it != collection.end()) {
    ...
}
switch(auto data = token.data(); data.type()) {
    ...
}
// Structure bindings
for(const auto&[key, value]: my_map) {
    ...
}

Ale zanim zaczniemy ich używac dowiedzmy się czy będziemy pokazywać na własnych komputerach XDD

Przy range loopach pamiętamy o takich funkcjach jak std::for_each, std::transform z lambdami :P, czasami nie warto pisać 3 linijek, jako można to zrobić w jednej przekazując lambdę.

zmienne

snake_case - czyli_zawsze_z_małej_i_rozdzielamy_znakiem _

Zmienne prywatne/chronione zaczynamy od znaku _, jak w kodzie wyżej.

funkcje, metody

camelCase - każdyChybaWieOCoMiChodziPrawda?

Rozpoczęcie bloku w tym samym co definicja funkcji. Czyli od teraz uważacie, że

void doNothing() {
    ...
}

jest jedynym możliwym sposobem pisania i jak ktoś uważa inaczej to nie ma racji.

Łamiemy argumenty jeżeli linia wychodzi za długa.

std::uint64_t doesNothingWithManyParameters(
                          std::string name,
                          std::ostream out,
                          TestCase test,
                          Flags flags) {
         ....
}

Ale staramy się unikać funkcji przyjmujących więcej niż 2 parametry. Im więcej parametrów przyjmuje funkcja tym więcej robi, a pamiętamy o tym, że funkcja ma się zajmować tylko jedną rzeczą.

C++ rzeczy do funkcji

PLS używamy const, referencji (&) i rvalue(&&), bo to są jedne z sensowniejszych rzeczy w tym języku. Pamiętamy o std::move, a w przypadku templatów o std::forward.

Uważamy no to kiedy obiekt się kopiuje, a kiedy jest przenoszony, bo zachowanie może być inne w zależności co zrobicie.

Ale nie korzystamy z każdego featura C++ - jak gdzieś zobaczę

auto myFunction() { ... }

to łeb ukręcę XD

Klasy

Case - TutajTeżWiemyOCoChodzi

O grupowaniu już wspominałem. Zasada pojedyńczej odpowiedzialności też.

Pamiętamy o inicjalizacji przy konstruktorze czyli. Nie

Lexer::Lexer(Source& source) {
    _source = source 
}

Tylko

Lexer::Lexer(Source& source) : _source(source) { ... }

Pamiętamy o konstruktorze kopiującym i przenoszącym (przypisania też)

MyClass(const Myclass& other);  // copy
MyClass(MyClass&& other);  // move

MyClass& operator=(const MyClass& other);
MyClass& operator=(MyClass&& other);

pamiętamy o tym, że one tworzą się automatycznie, więc warto się zastanowić czy na pewno je chcemy i czy przypadkiem nie przydałoby się ich nadpisać. Jak nie chcemy

MyClass(MyClass&& other) = delete;  

Zastanawiamy się czy na pewno chcemy oznaczyć zmienne jako private, to taki nawyk, a może protected będzie lepsze.

Namespacy

Ogólnie używamy, nie boimy się. Namespacy pozwalają na ładne grupowanie logiczne klas i funkcji. Nie musimy też tak bardzo przejmować się kolizjami nazw. Plus od razu widać z jakiego modułu co jest. Np.

// without namespaces
// we need to add 'json' and 'base64' at the
// beginning of function names to avoid name collisions
// like in C
auto header = jsonDecode(response['header']);
auto content = base64Decode(response['body']);

// with namespaces
auto header = json::decode(response['header']);
auto content = base64::decode(response['body']);

i namespacy ładnie podzielą nasz projekt na moduły

Factory::
Queue::
Communication::
// and so on