Szukam porady projektowej OO

12

Tworzę aplikację, która będzie używana do otwierania i zamykania zaworów w środowisku przemysłowym, i myślałam o czymś prostym:

public static void ValveController
{
    public static void OpenValve(string valveName)
    {
        // Implementation to open the valve
    }

    public static void CloseValve(string valveName)
    {
        // Implementation to close the valve
    }
}

(Implementacja zapisuje kilka bajtów danych do portu szeregowego w celu sterowania zaworem - „adres” pochodzący od nazwy zaworu oraz „1” lub „0” w celu otwarcia lub zamknięcia zaworu).

Inny twórca zapytał, czy zamiast tego powinniśmy utworzyć osobną klasę dla każdej zastawki fizycznej, której jest kilkadziesiąt. Zgadzam się, że będzie ładniej kodu pisać jak PlasmaValve.Open()zamiast ValveController.OpenValve("plasma"), ale jest to przesada?

Zastanawiałem się również, jak najlepiej poradzić sobie z tym projektem, mając na uwadze kilka hipotetycznych przyszłych wymagań:

  1. Jesteśmy proszeni o wsparcie nowego typu zaworu wymagającego różnych wartości, aby go otworzyć i zamknąć (nie 0 i 1).
  2. Jesteśmy proszeni o wsparcie zaworu, który można ustawić w dowolnej pozycji od 0-100, zamiast po prostu „otwarte” lub „zamknięte”.

Zwykle do tego rodzaju rzeczy używałbym dziedziczenia, ale ostatnio zacząłem się zastanawiać nad „kompozycją nad dziedziczeniem” i zastanawiam się, czy istnieje lepsze rozwiązanie przy użyciu kompozycji?

Andrew Stephens
źródło
2
Stworzyłbym ogólną klasę zaworu, która ma identyfikator dla konkretnego zaworu (nie ciąg znaków, być może wyliczenie) i wszelkie informacje niezbędne do sterowania przepływem wewnątrz metod OpenValve / CloseValve. Alternatywnie możesz zrobić streszczenie dla klasy Valv i wykonać osobne implementacje dla każdego z nich, w których zawór otwierający / zamykający po prostu wywołuje logikę wewnątrz danej klasy zaworu w przypadku, gdy różne zawory mają różne mechanizmy otwierania / zamykania. Wspólny mechanizm zostałby zdefiniowany w klasie podstawowej.
Jimmy Hoffa
2
Nie martw się o hipotetyczne przyszłe wymagania. YAGNI.
pdr
3
@pdr YAGNI jest obosiecznym ostrzem, zgadzam się, że warto go przestrzegać ogólnie, ale do końca można powiedzieć, że robienie czegokolwiek, aby pomóc w przyszłości w utrzymaniu lub czytelności, narusza YAGNI, z tego powodu uważam, że zakres YAGNI jest zbyt niejednoznaczny dla wiele. To powiedziawszy, wiele osób wie, gdzie używać YAGNI i gdzie go rzucić, ponieważ rozliczenie się z przyszłością zaoszczędzi ci poważnego bólu. Myślę, że należy uważać, by sugerować ludziom, aby śledzili YAGNI, gdy nie wiesz, gdzie wylądują w tym spektrum.
Jimmy Hoffa
2
Człowieku, „kompozycja nad dziedziczeniem” jest przereklamowana. Zrobiłbym abstrakcyjną klasę / interfejs Valve, a następnie podklasowałem je do PlasmaValve. A potem upewnię się, że mój ValveController będzie działał z Valve (s), nie dbając o to, którą dokładnie podklasą są.
MrFox,
2
@suslik: Absolutnie. Widziałem także doskonały kod zwany spaghetti przez ludzi, którzy nie rozumieją zasad SOLID. Moglibyśmy to kontynuować na zawsze. Chodzi mi o to, że widziałem więcej problemów spowodowanych przez odrzucenie ustalonych zasad (zrodzonych z wieloletniego doświadczenia) z ręki, niż widziałem spowodowane nadmiernym przestrzeganiem zasad. Ale zgadzam się, że obie skrajności są niebezpieczne.
pdr

Odpowiedzi:

12

Jeśli każda instancja obiektu zaworu będzie uruchamiała ten sam kod, co ten ValveController, wydaje się, że wiele instancji jednej klasy byłoby właściwą drogą. W takim przypadku po prostu skonfiguruj, który zawór kontroluje (i jak) w konstruktorze obiektu zaworu.

Jeśli jednak każde sterowanie zaworem potrzebuje innego kodu do uruchomienia, a bieżący ValveController uruchamia gigantyczną instrukcję przełączającą, która robi różne rzeczy w zależności od typu zaworu, oznacza to, że źle zaimplementowano polimorfizm. W takim przypadku przepisz go do wielu klas ze wspólną bazą (jeśli ma to sens) i pozwól, aby zasada pojedynczej odpowiedzialności była Twoim przewodnikiem projektowym.

kamieniste
źródło
1
+1 za wymienienie instrukcji przełączania opartych na typach jako zapachu kodu. Często widzę tego rodzaju instrukcje przełączania, w których deweloper twierdzi, że właśnie śledził KISS. Doskonały przykład tego, jak zasady projektowania mogą być wypaczone heh
Jimmy Hoffa
2
Wiele instancji może również ułatwić łączenie zaworów w sekwencji, umożliwiając modelowanie rzeczywistej instalacji rurowej jako ukierunkowanego wykresu w kodzie. Możesz także dodać logikę biznesową do klas, na wypadek gdybyś musiał zrobić coś takiego jak otwieranie jednego zaworu, gdy drugi zamyka się, aby uniknąć wzrostu ciśnienia, lub zamykanie wszystkich zaworów za nimi, aby nie uzyskać efektu „uderzenia wody” kiedy zawór zostanie ponownie otwarty.
TMN
1

Moim głównym problemem jest używanie ciągów jako parametru identyfikującego zawór.

Przynajmniej stwórz Valveklasę, która ma getAddressformę wymagającą implementacji i przekaż je do ValveControlleri upewnij się, że nie możesz stworzyć nieistniejących zaworów. W ten sposób nie będziesz musiał obsługiwać niewłaściwych ciągów w każdej z metod otwierania i zamykania.

To, czy stworzysz dogodne metody, które będą wywoływać otwieranie i zamykanie, ValveControllerzależy od ciebie, ale szczerze mówiąc, zachowałbym całą komunikację z portem szeregowym (łącznie z kodowaniem) w jednej klasie, którą inne klasy będą w razie potrzeby wywoływać. Oznacza to, że gdy trzeba przeprowadzić migrację do nowego kontrolera, wystarczy zmodyfikować tylko jedną klasę.

Jeśli podoba ci się testowanie, powinieneś zrobić ValveControllersingleton, aby go wyśmiewać (lub stworzyć maszynę szkoleniową dla operatorów).

maniak zapadkowy
źródło
Nigdy wcześniej nie widziałem, żeby ktokolwiek polecał singletona ze względu na testy - zwykle dzieje się to w drugą stronę.
Kazark
szczerze mówiąc, singleton jest bardziej w celu uniknięcia statyki, więc komunikacja może być zsynchronizowana
maniak zapadkowy