Oto często spotykany problem: Niech będzie projekt sklepu internetowego, który ma klasę produktu. Chcę dodać funkcję, która pozwala użytkownikom publikować recenzje w produkcie. Mam więc klasę Review, która odwołuje się do produktu. Teraz potrzebuję metody, która wyświetla wszystkie recenzje produktu. Istnieją dwie możliwości:
(ZA)
public class Product {
...
public Collection<Review> getReviews() {...}
}
(B)
public class Review {
...
static public Collection<Review> forProduct( Product product ) {...}
}
Spoglądając na kod, wybrałbym (A): Nie jest statyczny i nie potrzebuje parametru. Jednak wyczuwam, że (A) narusza zasadę pojedynczej odpowiedzialności (SRP) i zasadę otwartego i zamkniętego (OCP), podczas gdy (B) nie:
(SRP) Kiedy chcę zmienić sposób zbierania recenzji produktu, muszę zmienić klasę produktu. Ale powinien istnieć tylko jeden powód, dla którego należy zmienić klasę produktu. I to z pewnością nie są recenzje. Jeśli spakuję każdą funkcję, która ma coś wspólnego z produktami w produkcie, wkrótce zostanie stuknięta.
(OCP) Muszę zmienić klasę produktu, aby rozszerzyć ją o tę funkcję. Myślę, że narusza to część zasady „Zamknięci na zmiany”. Zanim dostałem prośbę klienta o wdrożenie recenzji, uznałem Produkt za gotowy i „zamknąłem” go.
Co jest ważniejsze: przestrzeganie zasad SOLID lub prostszy interfejs?
Czy też robię tu coś złego?
Wynik
Wow, dziękuję za wszystkie twoje wspaniałe odpowiedzi! Trudno wybrać jedną jako oficjalną odpowiedź.
Pozwól, że streszczę główne argumenty z odpowiedzi:
- pro (A): OCP nie jest prawem i liczy się również czytelność kodu.
- pro (A): relacja bytu powinna być możliwa do nawigacji. Obie klasy mogą wiedzieć o tym związku.
- pro (A) + (B): wykonaj oba i deleguj w (A) na (B), aby prawdopodobieństwo zmiany produktu było mniejsze.
- pro (C): umieść metody wyszukiwania w trzeciej klasie (usłudze), gdzie nie jest statyczna.
- contra (B): utrudnia drwiny w testach.
Przyczyniło się kilka dodatkowych rzeczy, które włożyły moje uczelnie w pracy:
- pro (B): nasz framework ORM może automatycznie wygenerować kod dla (B).
- pro (A): ze względów technicznych naszej struktury ORM konieczna będzie zmiana „zamkniętego” bytu w niektórych przypadkach, niezależnie od tego, dokąd trafi szukający. W każdym razie i tak nie zawsze będę w stanie pozostać przy SOLID.
- contra (C): za dużo zamieszania ;-)
Wniosek
Używam zarówno (A) + (B) z delegacją do mojego bieżącego projektu. Jednak w środowisku zorientowanym na usługi skorzystam z (C).
Assert(5 = Math.Abs(-5));
Abs()
nie jest problemem, testowanie czegoś, co od niego zależy. Nie masz szwu do izolowania zależnego testowanego kodu (CUT), aby użyć makiety. Oznacza to, że nie można go przetestować jako jednostki atomowej, a wszystkie testy stają się testami integracyjnymi, które logikę jednostki testowej. Błąd w teście może być w CUT lub w Abs()
(lub jego zależnym kodzie) i usuwa zalety diagnostyczne testów jednostkowych.