Wpis z mikrobloga

#cpp

Stworzyłem swój pierwszy "troszeczkę bardziej" rozbudowany programik obiektowy służący do walidacji danych nowego klienta banku. Generalnie nie chodzi o samą walidację, bo kilku mechanizmów brakuje, np. jak się poda hasło ze spacją, to akceptuje. Chodzi mi raczej o nakierowanie, czy sama struktura programu obiektowego jest poprawna. Co zmienić, dobre praktyki itp... jakiś wzorzec może byłoby lepiej tu zastosować, a może nie...

https://github.com/VanQator/bankAccount
  • 23
@VanQator: największy problem jaki widzę to mocne połączenie pomiędzy operacjami IO a twoimi klasami. To dość częsty błąd wśród początkujących. Jak chcesz mieć to ładnie to klasy odpowiedzialne za trzymanie poprawnej wartości, czyli wszystkie ValidCośTam powinny rzucać wyjątek, albo używać jakiegoś innego mechanizmu raportowania błędów. Tam gdzie jest logika nie powinno być IO, bo czegoś takiego nie da się testować i reużywać.
Inne sprawy:
- w cpp jest raczej konwencja, że
@VanQator:
pirmo - dla nowego moze to byc upierdliwe i zbyteczne, ale nie mieszac logiki z wyswietlaniem to kiedys zaprocentuje i sam sobie bedziesz chcial obciagnac za tak genialne rozwiazanie
drugie - uzywaj wiecej std, w wiekszosci przypadkow bo to jest piekne, zgrabne i wiekszosci przypadkow jak juz pisalem wydajne np. twoja walidaje tutaj https://github.com/VanQator/bankAccount/blob/master/validBalance.cpp#L10 mozna zastapic pieknym 1 linijkowcem:

bool isValid = find_if(str.begin(), str.end(), is_not_alnum_space) == str.end();
po trzecie -
via Wykop Mobilny (Android)
  • 2
@VanQator: dobre aplikacje są budowane tak, aby sama główna logika (biznesowa) aplikacja była oddzielona zarówno od użytkownika, jak i warstwy "persistent" ( powinny być osobno od ).

Takie podejście umożliwia rozszerzalność aplikacji w postaci np. dodania GUI bez znacznych zmian w głównej logice.
via Wykop Mobilny (Android)
  • 1
@VanQator: to nie muszą być flagi.
Może to być np. klasa reprezentująca problemy w walidacji - w sensie może to być typ złożony, który przenosi jeszcze dodatkowe informacje, albo nawet prosty Enum jak ci się podoba.
@patrolez: potem pomyślę, jak to rozwiązać, chcę też dodać jakiś mechanizm agregujący klientów w jednej strukturze, i nwm czy najlepiej zastosować tutaj zwykły vector, czy jakiś wzorzec typu kompozyt
@VanQator:

możesz zrobić to na 2 sposoby:

1, tradycyjnie w C++, rzucasz w środku konstruktora wyjątek i łapiesz go:
ValidBalance readBalanceFromStdin() {
while(true) {
cout << "Enter correct name.\n";
cout << "Can't be empty.\n";
cout << "Contain only alphabetic characters.\n\n";
cout << "Enter: ";

std::string input;
cin >> input;

try {
return ValidBalance(input)
} catch (std::domain_error const& e) {
cout << "Bad input: " << e.what();
}
}
}

2, w
@VanQator: nie powiedziałbym, że to programowanie obiektowe. W każdym paradygmacie powinna być seperacja, w proceduralnym wymieszane IO nie boli tak bardzo (ale też boli) jak w obiektowym. W funkcyjnym paradygmacie mieszanie IO i logiki jest upierdliwe, więc tam nawet nie trzeba się pilnować, bo słuszna droga jest najczytelniesza
@VanQator: taka uwaga generalna po samym tylko otworzeniu repozytorium - nie bój się nowych plików i unikaj konstrukcji typu "mainHeader.h".

Osobny plik nagłówkowy i .cpp dla każdej z klas jest zdecydowanie lepszym rozwiązaniem.

A poza tym to co pisali poprzednicy - oddziel IO od logiki.

Ja zawsze byłem bojownikiem pisania '{' linijkę niżej, ale o ile to Twój mały projekt, to tylko i wyłącznie kwestia Twoich preferencji - tym niemniej bądź
@Saly: Ok powiedzmy, że funkcja validPassword obiektu ValidPassword zeraca wyjątek, bo użytkownik podał złe hasło. Wyjątek jest obsługiwany w konstruktorze obiektu ValidPassword. Problem jest jednak taki, że nawet po zwróceniu błędu o nieptaridłowym haśle, obiekt ValidPassword zostanie zainicjalizowy... czy się mylę
@VanQator: wyjątek przelatuje przez konstruktor. Miejsce, gdzie łapiesz, to tam gdzie tworzysz obiekt. Jak widać na moim przykładzie jak poleci wyjątek to obiekt nie powstanie, nie wykona się return tylko wskoczysz do catch
@Saly: Ok, ale powstanie obiekt Account, który będzie posiadał pole balance_ zainicjalizowane śmieciową wartością. Ten wyjątek oczywiście łapię w konstruktorze klasy ValidBalance?
@VanQator: wyobrazam sobie taki flow:

ValidBalance balance = readValidBalanceFromStdin();
...
...
...

Account account(balance, .....)

Funkcja readValidBalanceFromStdin wygląda jak w poprzednim komentarzu. W tej funkcji masz pętle while, z której da się wyjść tylko wtedy, gdy konstruktor ValidBalance nie rzuci wyjątku (return ValidBalance(input)). W innym wypadku wracamy na począte funkcji, więc nie ma opcji, żeby Account nie powstał, bo przepływ programu nie dojdzie tam, jeżeli funkcja readValidBalanceFromStdin nie zwróci wartości
@Saly: Ok, no to załóżmy, że chcę zrezygnować całkowicie z IO. To co mnie interesuje to utworzenie obiektu Account tylko i wylącznie w wypadku, gdy podczas tworzenia żadnego z obiektu typu ValidBalance, ValidPassword... nie zostanie wyrzucony wyjątek.
@VanQator: w takim wypadku robisz to tak:

try {
ValidBalance balance("sdfsdf");
Account account(balance .... );

} catch (std::domainerror const& e) {
std::cout << e.what();
}

jeżeli konstruktor ValidBalance rzuci wyjątek to nie przejdziesz do następnej lini, tylko do klauzuli catch, więc account nie będzie miał śmieciowego pola balance
, ponieważ sam konstruktor Account nigdy się nie odpali