Drukowana wersja tematu

Kliknij tu, aby zobaczyć temat w orginalnym formacie

Forum PHP.pl _ Object-oriented programming _ Helper functions / funkcje globalne w OOP

Napisany przez: Brick 29.10.2017, 11:20:17

Czy używacie funkcji własnych funkcji globalnych wewnątrz klas? Chodzi mi o typowe funkcjie pomocnicze (helper functions), które dokonują jakiś obliczeń czy robią transformacje na ciągach znaków. Są małe, ogólne i zupełnie niezwiązane z logiką biznesową czy modułami programu.
Minusem tych funkcji jest to że uniemożliwiają testowanie (mockowanie) klas podobnie jak funkcje statyczne. (Jest jakieś dobry polski zamiennik na "mock")?
Plusem jest to, że są zawsze dostępne - nie wymagają tworzenia obiektu i przekazywania za pomocą dependency injection.

Jak rozwiązujecie ten problem?
Bez funkcji globalnych tylko zawsze jako metody przypisane do konkretnej klasy?
Czy może jednak funkcje globalne ale tylko w projektach dla których nie są wykonywane testy jednostkowe?

Napisany przez: sabat24 29.10.2017, 14:20:17

Jeśli o mnie chodzi, to wszystko zależy jakie są powiązania i do czego się używa takich metod. Jeśli to są klasyczne, proste helpery na zasadzie "wyciągnij pierwiastek", to nie ma najmniejszego problemu, by włożyć to w statyczną klasę Math i używać globalnie.

Co do testów, niektórzy powiedzą, że na statycznych nie robi się unit testów, tylko testy integracyjne. Jak dla mnie "zwał jak zwał", ale jeśli metoda się da przetestować, a kod typu:

  1. public http://www.php.net/static function in_array_all($needles, $haystack) {
  2. return ! http://www.php.net/array_diff($needles, $haystack);
  3. }


jest bardzo łatwy to przetestowania, to nie widzę problemu, by dla wygody nie użyć go jako w klasie statycznej.

Odnośnie mockowania (zwię się to atrapa po naszemu), to jest np. taka biblioteka: https://github.com/Codeception/AspectMock

Oczywiście da się przesadzić ze statycznością, ale jeśli się korzysta z takich helperów, ja nie widzę przeciwskazań. Nigdy jednak nie byłem zwolniennikiem purystycznego kodu dla samej sztuki.

Napisany przez: Pyton_000 29.10.2017, 14:44:15

Przecież da się wytestować metodę która ma jakieś static calls. Wystarczy utworzyć mock właśnie tej metody.

Tutaj przydład: https://stackoverflow.com/questions/5961023/unit-testing-and-static-methods

Napisany przez: markuz 29.10.2017, 15:49:25

  1. public http://www.php.net/static function in_array_all($needles, $haystack) {
  2. return ! http://www.php.net/array_diff($needles, $haystack);
  3. }


Dla mnie to nic innego jak własny alias, jak jestem nowym programistą w projekcie to wolę zobaczyć zapis !array_diff($needles, $haystack) niż Costam::in_array_all($needles, $haystack) - w 1 przypadku nie muszę zaglądać głębiej, w 2 niby wiem po nazwie co robi metoda ale doświadczenie pokazuje, że trzeba tam zajrzeć.

Aktualnie jestem zwolennikiem nie używania takich helperów w kodzie klas - często trafia do nich logika biznesowa. A sam fakt, ze musimy użyć w wielu miejscach metody z jakiegoś helpera może świadczyć o błędnej architekturze. Nawet jeżeli nie ma tam logiki biznesowej to pojawi się programista który ją tam umieści żeby nie główkować za długo.

Jednak każdy przypadek trzeba rozpatrzeć osobno, możesz podać konkretny przykład helpera i jego użycia w konkretnych klasach.

Napisany przez: Brick 30.10.2017, 09:20:10

Dzięki za odpowiedzi. Ta biblioteka AspectMock to super sprawa, nie wiedziałem że jest coś takiego.
Oczywiście zgadza się - wszystko zależy od tego jak się używa takich globalnych funkcji. Jak ktoś potrafi wstawić tam logikę biznesową czy elementy które powiązane z konkretnymi modułami programu no to lepiej żeby nie ich nie używał.
Poniżej przykładowy kod:

  1. public function copyUploadedFile($filename, $dst_dir)
  2. {
  3. $filename = convertToSimpleString($filename);
  4. $extension = getFileExtension($filename);
  5. $dst_dir = convertToSimpleString($dst_dir);
  6. createDir($dst_dir);
  7. ....
  8. $file_size = covertToHumanFormat(http://www.php.net/filesize(...));
  9. ... //tutaj zapisanie pliku i przekazanie informacji o nim (finalna nazwa, rozszerzenie, rozmiar) do innej metody, która zapisuje te dane do bazy
  10. }

Jest to oczywiście przykład uproszczony, sama metoda może nie zawierać na raz tyle funkcji globalnych. Chodzi o to żeby pokazać ich wykorzystanie.

- Funkcja convertToSimpleString() zamienia ciąg znaków np: "Raport biegłego rewidenta z oświadczeniem - za ROK 2012" na "Raport_bieglego_rewidenta_z_oswiadczeniem_za_rok_2012" jednocześnie usuwając różne niepożądane znaki.
- Funkcja createDir() sprawdza czy podany katalog istnieje - jeżeli nie to go tworzy.
- Funkcja convertToHumanFormat() - zamienia rozmiar w bajtach na rozmiar w kB, MB, GB

Napisany przez: Pilsener 30.10.2017, 09:59:29

Cytat
Czy używacie funkcji własnych funkcji globalnych wewnątrz klas?


Nie, bo to zła praktyka. Różnego rodzaju "utilsy" są pogrupowane w serwisach wg ich przeznaczenia (np. service/tool/StringService.php) i "wstrzykiwane" do odpowiedniego kontekstu. Przy czym część funkcji jest dostępna defaultowo (np. $this->logger w kontekście całej aplikacji) a inne są "na życzenie".
Oczywiście dużo łatwiej jest użyć czegoś wprost, niż implementować jako zależność, ale to pozorna oszczędność bo tworzymy w ten sposób różne ukryte zależności które ciężko jest kontrolować, powstają różne dziwne błędy i tak dalej.

Napisany przez: Pyton_000 30.10.2017, 10:18:30

Cytat(Brick @ 30.10.2017, 09:20:10 ) *
Dzięki za odpowiedzi. Ta biblioteka AspectMock to super sprawa, nie wiedziałem że jest coś takiego.
Oczywiście zgadza się - wszystko zależy od tego jak się używa takich globalnych funkcji. Jak ktoś potrafi wstawić tam logikę biznesową czy elementy które powiązane z konkretnymi modułami programu no to lepiej żeby nie ich nie używał.
Poniżej przykładowy kod:
  1. public function copyUploadedFile($filename, $dst_dir)
  2. {
  3. $filename = convertToSimpleString($filename);
  4. $extension = getFileExtension($filename);
  5. $dst_dir = convertToSimpleString($dst_dir);
  6. createDir($dst_dir);
  7. ....
  8. $file_size = covertToHumanFormat(http://www.php.net/filesize(...));
  9. ... //tutaj zapisanie pliku i przekazanie informacji o nim (finalna nazwa, rozszerzenie, rozmiar) do innej metody, która zapisuje te dane do bazy
  10. }

Jest to oczywiście przykład uproszczony, sama metoda może nie zawierać na raz tyle funkcji globalnych. Chodzi o to żeby pokazać ich wykorzystanie.

- Funkcja convertToSimpleString() zamienia ciąg znaków np: "Raport biegłego rewidenta z oświadczeniem - za ROK 2012" na "Raport_bieglego_rewidenta_z_oswiadczeniem_za_rok_2012" jednocześnie usuwając różne niepożądane znaki.
- Funkcja createDir() sprawdza czy podany katalog istnieje - jeżeli nie to go tworzy.
- Funkcja convertToHumanFormat() - zamienia rozmiar w bajtach na rozmiar w kB, MB, GB


Jak na moje to nadaje się na normalną klasę i wstrzyknięcie jako DI bo tu dużo logiki się dzieje jak na helper.

Napisany przez: Brick 31.10.2017, 08:13:31

Cytat
Przy czym część funkcji jest dostępna defaultowo (np. $this->logger w kontekście całej aplikacji) a inne są "na życzenie"

W jaki sposób jest dostępna defaultowo? Każdy obiekt ma domyślnie wstrzykiwany odpowiedni zestaw? Czy to się robi za pomocą Data Object Container?

Czy czasem w klasie nie robi się zbyt dużo tych wstrzyknięć? Np:
  1. class Example
  2. {
  3. public function __construct(StringService $string_service, NumberService $number_service, FileController $file_controller ...)
  4. }

Chyba, że jest to zwykle objaw sugerujący że klasa ma za dużą odpowiedzialność (naruszenie single responsibility)?

Napisany przez: Pilsener 31.10.2017, 09:04:15

Cytat
W jaki sposób jest dostępna defaultowo?
- najczęściej jest zaimplementowana w odpowiedniej warstwie abstrakcji, np. ControllerAbstract ma metodę redirect.

Cytat
Czy to się robi za pomocą Data Object Container?
- w ten sposób pobieramy zależności "na życzenie" i tak, zauważyłem, że kontener zależności ładowanych w sposób "lazy" to dziś pewien standard. Jednak używanie wszędzie w kodzie:
  1. $this->container->get(MyService::class);
- zalatuje lekko globalem. Znów tworzymy jakieś zależności pozaszywane w kodzie. Lepiej uszyć getry/swetry a jeszcze lepiej przez konstruktor - wtedy od razu widać, jakich zależności potrzebujemy.

Cytat
Czy czasem w klasie nie robi się zbyt dużo tych wstrzyknięć?
- robi, ale to jest już kwestia trzymania porządku w kodzie. Jeśli klasa się rozrasta to z pomocą przychodzi nam fasada i podzielenie tego na pod-serwisy, problem jest taki, że nikomu nie chce się tego robić bo poza uporządkowaniem kodu nie ma z tego żadnego zysku biznesowego businesssmiley.png

Bardzo istotne też jest, czy piszemy testy jednostkowe - jeśli tak, to już to wymusza zupełnie inne podejście do architektury kodu. Tylko w ilu firmach preferuje się takie podejście arrowheadsmiley.png

Napisany przez: Brick 2.11.2017, 13:06:27

Cytat
najczęściej jest zaimplementowana w odpowiedniej warstwie abstrakcji, np. ControllerAbstract ma metodę redirect

Czy możesz rozwinąć trochę ten wątek? Jak to wygląda w praktyce?

Napisany przez: Pilsener 3.11.2017, 08:49:20

Najlepiej spojrzeć na jakiś framework, gdy projektujemy np. kontroler to musimy przemyśleć nasz abstrakt:
- jakie metody w nim zaimplementować (co często jest trudne)
- jak dostarczyć do niego potrzebne zależności
- jak w łatwy sposób rozszerzać klasę

Przy dostarczaniu zależności wykorzystujemy kontener, implementując interfejs z metodą "setContainer"

Potem gdy rozszerzamy klasę, to zazwyczaj deklarujemy ją jako serwis aczkolwiek jeśli implementuje ona kontener to możemy używać:

  1. $this->container->get(Service::class);

Jednak z wiadomych względów nie jest to zalecane.
Oczywiście są też względy praktyczne, stąd używanie kontenera bo:
- z klasy abstrakcyjnej nie da się przecież utworzyć obiektu i czegoś tam wstrzyknąć
- dobrze jest mieć do dyspozycji konstruktor (ciągłe call super czy tam parent nie jest przecież zalecane)
- wygoda

Napisany przez: Brick 6.11.2017, 09:35:02

Cytat
zauważyłem, że kontener zależności ładowanych w sposób "lazy" to dziś pewien standard

Co dokładnie masz na myśli pisząc "lazy"? Ładowanie obiektu na żądanie, wtedy gdy bo faktycznie potrzebujemy a nie "na zapas"?

Napisany przez: Pilsener 6.11.2017, 13:28:14

Chodzi o to, że obiekty są przygotowywane dopiero wtedy, gdy są potrzebne. Np. masz kontener połączeń z bazami danych - w podejściu tradycyjnym połączenie do baz następuje przy starcie aplikacji, w podejściu "lazy" dopiero wtedy, gdy pojawi się pierwsze zapytanie do danej bazy.

Napisany przez: Brick 9.11.2017, 12:51:48

Dzięki za wszystkie odpowiedzi.

Podsumowując dla osób które trafią na ten wątek:


Napisany przez: nospor 9.11.2017, 12:55:50

Cytat
chyba że są to zupełnie małe, ogólne i niezależne od logiki funkcje, np oblicz pole powierzchni figury.
Tak czytam ten temat i czytam i nikogo, procz ciebie, nie widze by to pochwalal.

Napisany przez: Pyton_000 9.11.2017, 13:30:42

Z tym pp figury to akurat słaby przykład wink.gif Bo taka metoda będzie w klasie Object i implementacja w konkretnej klasie figury wink.gif

Tak na prawdę jeśli chodzi o utilsy jako takie to nie ma złotego środka.
Można grupować utils ze względu na przznaczenie np. w klasie String i tam odpowiednie itd. Wtedy potrzebującu utils do generowania slug albo zaciągasz klasę String albo klasę Uri (w zależności gdzie podczepisz).

Powered by Invision Power Board (http://www.invisionboard.com)
© Invision Power Services (http://www.invisionpower.com)