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