Piszę dużo kodu, który obejmuje trzy podstawowe kroki.
- Zdobądź skądś dane.
- Przekształć te dane.
- Umieść te dane gdzieś.
Zazwyczaj używam trzech rodzajów zajęć - zainspirowanych ich wzorami projektowymi.
- Fabryki - aby zbudować obiekt z jakiegoś zasobu.
- Mediatorzy - aby skorzystać z fabryki, przeprowadzić transformację, a następnie użyć dowódcy.
- Dowódcy - aby umieścić te dane gdzie indziej.
Moje klasy wydają mi się być dość małe, często pojedyncza (publiczna) metoda, np. Uzyskiwanie danych, przekształcanie danych, wykonywanie pracy, zapisywanie danych. Prowadzi to do mnożenia klas, ale ogólnie działa dobrze.
Tam, gdzie walczę, kiedy przychodzę na testy, kończę na ściśle powiązanych testach. Na przykład;
- Factory - odczytuje pliki z dysku.
- Commander - zapisuje pliki na dysk.
Nie mogę przetestować jednego bez drugiego. Mógłbym również napisać dodatkowy kod „testowy”, aby wykonać odczyt / zapis dysku, ale potem się powtarzam.
Patrząc na .Net, klasa File ma inne podejście, łączy obowiązki (mojej) fabryki i dowódcy. Posiada funkcje tworzenia, usuwania, istnienia i odczytu wszystkich w jednym miejscu.
Czy powinienem podążać za przykładem .Net i łączyć - szczególnie w przypadku zasobów zewnętrznych - moje klasy razem? Kod nadal jest sprzężony, ale jest bardziej celowy - dzieje się to w oryginalnej implementacji, a nie w testach.
Czy mój problem polega tutaj na tym, że zastosowałem zasadę jednej odpowiedzialności w sposób nadmiernie nadgorliwy? Mam osobne klasy odpowiedzialne za czytanie i pisanie. Kiedy mogłem mieć połączoną klasę, która jest odpowiedzialna za zarządzanie konkretnym zasobem, np. Dyskiem systemowym.
źródło
Looking at .Net, the File class takes a different approach, it combines the responsibilities (of my) factory and commander together. It has functions for Create, Delete, Exists, and Read all in one place.
- Pamiętaj, że łączysz „odpowiedzialność” z „rzeczą do zrobienia”. Odpowiedzialność jest bardziej jak „obszar troski”. Obowiązkiem klasy File jest wykonywanie operacji na plikach.File
bibliotece z C # jest, jak wiemy,File
klasa może być po prostu fasadą, umieszczając wszystkie operacje na plikach w jednym miejscu - w klasie, ale może wewnętrznie używać podobnych klas do odczytu / zapisu do twojej, co by faktycznie zawierają bardziej skomplikowaną logikę do obsługi plików. Taka klasaFile
nadal przestrzegałaby SRP, ponieważ proces faktycznej pracy z systemem plików zostałby zaabsorbowany za inną warstwą - najprawdopodobniej z interfejsem ujednolicającym. Nie mówię, że tak jest, ale może być. :)Odpowiedzi:
Przestrzeganie zasady pojedynczej odpowiedzialności mogło być tym, co cię tu kierowało, ale to, gdzie jesteś, ma inną nazwę.
Segregacja odpowiedzialności za zapytania poleceń
Idź przestudiuj to i myślę, że znajdziesz to zgodnie ze znanym schematem i nie jesteś sam w zastanawianiu się, jak daleko się to zajmie. Test kwasowy ma miejsce, jeśli przestrzeganie tego przyniesie ci realne korzyści lub jeśli jest to tylko ślepa mantra, którą przestrzegasz, więc nie musisz myśleć.
Wyraziłeś zaniepokojenie testowaniem. Nie sądzę, aby przestrzeganie CQRS wykluczało pisanie testowalnego kodu. Być może po prostu postępujesz zgodnie z CQRS w sposób uniemożliwiający testowanie kodu.
Pomaga wiedzieć, jak używać polimorfizmu do odwracania zależności kodu źródłowego bez konieczności zmiany przepływu kontroli. Nie jestem do końca pewien, gdzie są twoje umiejętności pisania testów.
Słowo ostrzeżenia, przestrzeganie nawyków, które można znaleźć w bibliotekach, nie jest optymalne. Biblioteki mają własne potrzeby i są szczerze mówiąc stare. Więc nawet najlepszy przykład jest wtedy najlepszym przykładem z tamtych czasów.
Nie oznacza to, że nie ma idealnie poprawnych przykładów, które nie są zgodne z CQRS. Postępowanie zgodnie z nim zawsze będzie trochę uciążliwe. Nie zawsze warto zapłacić. Ale jeśli będziesz go potrzebować, będziesz zadowolony, że go użyłeś.
Jeśli go użyjesz, posłuchaj tego ostrzeżenia:
źródło
Potrzebujesz szerszej perspektywy, aby ustalić, czy kod jest zgodny z zasadą pojedynczej odpowiedzialności. Nie można na nie odpowiedzieć, analizując sam kod, należy zastanowić się, jakie siły lub podmioty mogą spowodować, że wymagania zmienią się w przyszłości.
Załóżmy, że przechowujesz dane aplikacji w pliku XML. Jakie czynniki mogą spowodować zmianę kodu związanego z czytaniem lub pisaniem? Niektóre możliwości:
We wszystkich tych przypadkach będzie trzeba zmienić zarówno czytania i pisania logiki. Innymi słowy, nie są to osobne obowiązki.
Ale wyobraźmy sobie inny scenariusz: Twoja aplikacja jest częścią potoku przetwarzania danych. Odczytuje niektóre pliki CSV wygenerowane przez oddzielny system, przeprowadza analizę i przetwarzanie, a następnie wyprowadza inny plik do przetworzenia przez trzeci system. W takim przypadku czytanie i pisanie są niezależnymi obowiązkami i należy je oddzielić.
Konkluzja: Zasadniczo nie można stwierdzić, czy odczytywanie i zapisywanie plików to osobne obowiązki, zależy to od ról w aplikacji. Ale w oparciu o twoją wskazówkę dotyczącą testowania, sądzę, że w twoim przypadku jest to jedna odpowiedzialność.
źródło
Ogólnie masz dobry pomysł.
Wygląda na to, że masz trzy obowiązki. IMO, „Mediator”, może wiele robić. Myślę, że powinieneś zacząć od modelowania swoich trzech obowiązków:
Następnie program można wyrazić jako:
Nie sądzę, że to jest problem. Wiele małych, spójnych, testowalnych klas IMO jest lepszych niż duże, mniej spójne klasy.
Każdy kawałek powinien być niezależnie testowany. Wzorowany powyżej, możesz reprezentować odczyt / zapis do pliku jako:
Możesz napisać testy integracji, aby przetestować te klasy, aby sprawdzić, czy odczytują i zapisują w systemie plików. Resztę logiki można zapisać jako transformacje. Na przykład, jeśli pliki mają format JSON, możesz je przekształcić
String
.Następnie możesz przekształcić w odpowiednie obiekty:
Każdy z nich można niezależnie przetestować. Można również testy jednostkowe
program
powyżej szyderczyreader
,transformer
iwriter
.źródło
FileWriter
, czytając bezpośrednio z systemu plików zamiast używaćFileReader
. To naprawdę zależy od Twoich celów. Jeśli go użyjeszFileReader
, test zostanie przerwany, jeśli któryś z nich jest uszkodzonyFileReader
lubFileWriter
może to potrwać - co może potrwać dłużej.Dlatego skupiono się na tym, co łączy je ze sobą . Czy przekazujesz obiekt między nimi (taki jak
File
?). Następnie jest to plik, z którym są sprzężeni, a nie siebie nawzajem.Od tego, co powiedziałeś, oddzieliłeś swoje klasy. Pułapka polega na tym, że testujesz je razem, ponieważ jest to łatwiejsze lub „ma sens” .
Dlaczego potrzebujesz danych wejściowych
Commander
z dysku? Wszystko, na czym mu zależy, to pisanie przy użyciu określonych danych wejściowych, a następnie możesz sprawdzić, czy poprawnie zapisał plik, używając tego, co jest w teście .W rzeczywistości testujesz, czy
Factory
„czyta ten plik poprawnie i wypisuje właściwą rzecz”? Wyśmiewaj plik przed odczytaniem go w teście .Alternatywnie, sprawdzanie, czy Fabryka i Dowódca działają w połączeniu, jest w porządku - zgadza się z testami Integracji całkiem szczęśliwie. Pytanie tutaj dotyczy raczej tego, czy Twoja jednostka może je przetestować osobno.
źródło
Jest to typowe podejście proceduralne, o którym pisał David Parnas w 1972 roku. Koncentrujesz się na tym, jak się sprawy mają. Konkretne rozwiązanie problemu traktujesz jako wzorzec wyższego poziomu, co zawsze jest błędne.
Jeśli stosujesz podejście obiektowe, wolę skoncentrować się na swojej domenie . O co w tym wszystkim chodzi? Jakie są główne obowiązki twojego systemu? Jakie są główne pojęcia przedstawione w języku Twoich ekspertów w dziedzinie? Więc zrozum swoją domenę, rozłóż ją, traktuj obszary odpowiedzialności wyższego poziomu jak swoje moduły , traktuj pojęcia niższego poziomu reprezentowane jako rzeczowniki jako obiekty. Oto przykład, który podałem w ostatnim pytaniu, jest bardzo istotny.
I jest wyraźny problem ze spójnością, sam o tym wspominałeś. Jeśli dokonasz modyfikacji, wprowadzisz logikę wejściową i napiszesz na niej testy, to w żaden sposób nie dowodzi, że funkcjonalność działa, ponieważ możesz zapomnieć o przekazaniu tych danych do następnej warstwy. Zobacz, te warstwy są ze sobą sprzężone. A sztuczne odsprzęganie czyni sprawy jeszcze gorszymi. Sam to wiem: 7-letni projekt ze 100 osobolat za moimi ramionami napisany całkowicie w tym stylu. Uciekaj, jeśli możesz.
I cała sprawa SRP. Chodzi przede wszystkim o spójność zastosowaną do przestrzeni problemowej, tj. Domeny. To podstawowa zasada SRP. Powoduje to, że obiekty są inteligentne i realizują swoje obowiązki za siebie. Nikt ich nie kontroluje, nikt nie dostarcza im danych. Łączą dane i zachowanie, odsłaniając tylko te ostatnie. Więc twoje obiekty łączą zarówno surowe sprawdzanie poprawności danych, transformację danych (tj. Zachowanie) i trwałość. Może to wyglądać następująco:
W rezultacie istnieje kilka spójnych klas reprezentujących pewną funkcjonalność. Zauważ, że sprawdzanie poprawności zwykle dotyczy obiektów wartości - przynajmniej w podejściu DDD .
źródło
Uważaj na nieszczelne abstrakty podczas pracy z systemem plików - zbyt często go zaniedbywałem i ma on objawy, które opisałeś.
Jeśli klasa działa na danych pochodzących z / przechodzi do tych plików, wówczas system plików staje się szczegółem implementacji (I / O) i powinien być od niego oddzielony. Klasy te (fabryka / dowódca / mediator) nie powinny być świadome systemu plików, chyba że ich jedynym zadaniem jest przechowywanie / odczyt dostarczonych danych. Klasy zajmujące się systemem plików powinny zawierać parametry specyficzne dla kontekstu, takie jak ścieżki (mogą być przekazywane przez konstruktor), więc interfejs nie ujawniał swojej natury (słowo „Plik” w nazwie interfejsu przez większość czasu pachnie).
źródło
Moim zdaniem brzmi to tak, jakbyś zaczął podążać właściwą ścieżką, ale nie posunąłeś się wystarczająco daleko. Myślę, że podzielenie funkcjonalności na różne klasy, które wykonują jedną rzecz i robią to dobrze, jest poprawne.
Aby pójść o krok dalej, należy utworzyć interfejsy dla klas Factory, Mediator i Commander. Następnie możesz użyć wyśmiewanych wersji tych klas, pisząc testy jednostkowe dla konkretnych implementacji innych. Za pomocą prób można sprawdzić, czy metody są wywoływane we właściwej kolejności i przy poprawnych parametrach oraz czy testowany kod działa poprawnie z różnymi wartościami zwracanymi.
Możesz także spojrzeć na abstrakcję odczytu / zapisu danych. Idziesz teraz do systemu plików, ale może będziesz chciał kiedyś przejść do bazy danych lub nawet gniazda. Klasa mediatora nie powinna się zmieniać, jeśli zmienia się źródło / miejsce docelowe danych.
źródło