Jak podzielić duże, ściśle powiązane klasy?

14

Mam kilka ogromnych klas ponad 2k wierszy kodu (i ciągle rośnie), które chciałbym przebudować, jeśli to możliwe, aby mieć trochę więcej lekkiego i czystego projektu.

Powód, dla którego jest tak duży, to głównie dlatego, że klasy te obsługują zestaw map, do których większość metod potrzebuje dostępu, a metody są ze sobą bardzo powiązane.

Podam bardzo konkretny przykład: mam klasę o nazwie, Serverktóra obsługuje przychodzące wiadomości. Ma metod, takich jak joinChatroom, searchUsers, sendPrivateMessage, itd. Wszystkie te metody manipulowania mapach takich jak users, chatrooms, servers, ...

Może byłoby miło, gdybym mógł mieć klasę obsługującą wiadomości dotyczące czatów, inną zajmującą się użytkownikami itp., Ale głównym problemem tutaj jest to, że muszę korzystać ze wszystkich map w większości metod. Dlatego na razie wszyscy trzymają się Serverklasy, ponieważ wszyscy polegają na tych wspólnych mapach, a metody są ze sobą bardzo powiązane.

Musiałbym stworzyć czat klasy, ale z odniesieniem do każdego z pozostałych obiektów. Użytkownicy klasy ponownie z odniesieniem do wszystkich innych obiektów itp.

Czuję, że zrobiłbym coś złego.

Mateusz
źródło
Jeśli stworzyłbyś takie klasy, jak User i Chatroom, czy te klasy potrzebowałyby tylko odniesienia do wspólnej struktury danych, czy też nawzajem się nawiązywałyby?
Jest tu kilka zadowalających odpowiedzi, powinieneś wybrać jedną.
jeremyjjbrown
@jeremyjjbrown pytanie zostało przeniesione i zgubiłem je. Wybrałem odpowiedź, dzięki.
Matthew

Odpowiedzi:

10

Z twojego opisu zgaduję, że twoje mapy to czysta torba danych, z całą logiką Servermetod. Przenosząc całą logikę pokoju rozmów do osobnej klasy, nadal utkniesz z mapami zawierającymi dane.

Zamiast tego spróbuj modelować poszczególne pokoje rozmów, użytkowników itp. Jako obiekty. W ten sposób będziesz przekazywał tylko określone obiekty wymagane dla określonej metody, zamiast ogromnych map danych.

Na przykład:

public class User {
  private String name;
  ...

  public void sendMessage(String message) {
    ...
  }
}

public class Chatroom {
  // users in this chatroom
  private Collection<User> users;

  public void add(User user) {
    users.add(user);
  }

  public void sendMessage(String msg) {
    for (User user : users)
      user.sendMessage(msg);
  }
}

public class Server {
  // all users on the server
  private Collection<User> users;

  // all chatrooms on the server
  private Collection<Chatroom> chatrooms;

  /* methods to handle incoming messages */
}

Teraz łatwo jest wywołać kilka konkretnych metod obsługi wiadomości:

Chcesz dołączyć do pokoju rozmów?

chatroom.add(user);

Chcesz wysłać prywatną wiadomość?

user.sendMessage(msg);

Chcesz wysłać wiadomość publiczną?

chatroom.sendMessage(msg);
Casablanka
źródło
5

Powinieneś być w stanie stworzyć klasę, która pomieści każdą kolekcję. Chociaż Serverbędzie potrzebował odniesienia do każdej z tych kolekcji, potrzebuje jedynie minimalnej ilości logiki, która nie będzie wymagała dostępu do podstawowych kolekcji ani zarządzania nimi. To sprawi, że bardziej oczywiste będzie dokładnie to, co robi Serwer, i rozdzieli, jak to robi.

Peter Lawrey
źródło
4

Kiedy zobaczyłem takie duże klasy, zauważyłem, że często jest tam klasa (lub więcej), która próbuje się wydostać. Jeśli znasz metodę, która Twoim zdaniem może nie być powiązana z tą klasą, ustaw ją w sposób statyczny. Kompilator poinformuje Cię o innych metodach wywoływanych przez tę metodę. Java będzie nalegać, aby były one również statyczne. Sprawiasz, że są statyczne. Ponownie kompilator powie ci o każdej metodzie, którą wywołuje. Robisz to w kółko, dopóki nie będziesz mieć więcej błędów kompilacji. Masz mnóstwo metod statycznych w swojej dużej klasie. Możesz teraz wyciągnąć je do nowej klasy i uczynić metodę niestatyczną. Następnie możesz nazwać tę nową klasę z oryginalnej dużej klasy (która powinna teraz zawierać mniej wierszy)

Następnie możesz powtarzać proces, aż będziesz zadowolony z projektu klasy.

Książka Martina Fowlera to naprawdę dobra lektura, więc polecam ją również, ponieważ czasami nie można użyć tej statycznej sztuczki.

RNJ
źródło
1
Ta książka Martina Fowlera martinfowler.com/books/refactoring.html
Arul
1

Ponieważ większość kodu istnieje, sugeruję skorzystanie z klas pomocniczych w celu przeniesienia metod. Pomoże to w łatwym refaktoryzacji. Tak więc twoja klasa serwera nadal będzie zawierać mapy. Ale korzysta z klasy pomocniczej, powiedzmy ChatroomHelper z metodami takimi jak join (mapa czatów, użytkownik String), lista getUsers (mapa czatów), mapa getChatrooms (użytkownik String).

Klasa serwera przechowuje instancję ChatroomHelper, UserHelper itp., Przenosząc w ten sposób metody do swoich logicznych klas pomocniczych. Dzięki temu możesz pozostawić publiczne metody nienaruszone na serwerze, więc żaden rozmówca nie musi się zmieniać.

techuser soma
źródło
1

Aby dodać do wnikliwej odpowiedzi Casablanki - jeśli wiele klas musi robić te same podstawowe czynności z jakimś rodzajem bytu (dodawanie użytkowników do kolekcji, obsługa wiadomości itp.), Te procesy również powinny być oddzielone.

Można to zrobić na wiele sposobów - poprzez dziedziczenie lub kompozycję. Do dziedziczenia można użyć abstrakcyjnych klas bazowych z konkretnymi metodami, które zapewniają pola i funkcje do obsługi na przykład użytkowników lub wiadomości, a podmioty takie jak chatroomi user(lub dowolne inne) rozszerzają te klasy.

Z różnych powodów dobrą ogólną zasadą jest używanie kompozycji zamiast dziedziczenia. Możesz użyć kompozycji, aby to zrobić na różne sposoby. Ponieważ obsługa użytkowników lub komunikatów jest funkcją kluczową dla klas, można argumentować, że wstrzyknięcie konstruktora jest najbardziej odpowiednie. W ten sposób zależność jest przezroczysta i nie można utworzyć obiektu bez wymaganej funkcjonalności. Jeśli sposób, w jaki użytkownicy lub wiadomości są obsługiwane, może się zmienić lub zostać rozszerzony, należy rozważyć użycie czegoś takiego jak wzorzec strategii .

W obu przypadkach pamiętaj o kodowaniu w kierunku interfejsów, a nie konkretnych klas zapewniających elastyczność.

Biorąc to wszystko pod uwagę, zawsze bierz pod uwagę koszt dodatkowej złożoności podczas korzystania z takich wzorców - jeśli nie będziesz go potrzebować, nie koduj go. Jeśli wiesz, że prawdopodobnie nie zmienisz sposobu obsługi użytkowników / wiadomości, nie potrzebujesz strukturalnej złożoności wzorca strategii - ale aby rozdzielić obawy i uniknąć powtórzeń, nadal powinieneś rozwodzić się ze wspólną funkcjonalnością z konkretnych przypadków, które go używają - i jeśli nie istnieją nadrzędne powody przeciwne, skomponuj użytkowników takiej funkcji obsługi (czaty, użytkownicy) z obiektem, który obsługuje.

Więc by podsumować:

  1. Jak napisał Casablanca: Oddziel i obuduj pokoje rozmów, użytkowników itp.
  2. Oddzielna wspólna funkcjonalność
  3. Rozważ oddzielenie poszczególnych funkcji, aby rozwieść reprezentację danych (a także dostęp i mutację) od bardziej złożonych funkcji w odniesieniu do poszczególnych instancji danych lub ich agregatów (na przykład coś takiego searchUsersmoże znaleźć się w klasie kolekcji lub repozytorium / mapie tożsamości )
Michael Bauer
źródło
0

Słuchaj, myślę, że twoje pytanie jest zbyt ogólne, aby na nie odpowiedzieć, ponieważ tak naprawdę nie mamy pełnego opisu problemu, więc oferowanie dobrego projektu przy tak małej wiedzy jest niemożliwe. Mogę na przykład odpowiedzieć na jedną z twoich obaw dotyczących możliwej bezcelowości lepszego projektowania.

Mówisz, że twoja klasa Server i twoja przyszła klasa Chatroom dzielą się danymi o użytkownikach, ale dane te powinny być inne. Serwer prawdopodobnie ma zestaw użytkowników połączony z nim, podczas gdy Chatroom, który sam należy do Serwera, ma inny zestaw użytkowników, podzestaw pierwszego zestawu, tylko użytkowników zalogowanych obecnie do określonego pokoju Chatroom.

To nie jest ta sama informacja, nawet jeśli typy danych są identyczne.
Dobry projekt ma wiele zalet.

Nie przeczytałem jeszcze wspomnianej książki Fowlera, ale przeczytałem inne rzeczy Folwer i poleciły mi ją osoby, którym ufam, więc czuję się na tyle swobodnie, by zgodzić się z innymi.

Shrulik
źródło
0

Konieczność dostępu do map nie usprawiedliwia mega-klasy. Musisz rozdzielić logikę na kilka klas, a każda klasa musi mieć metodę getMap, aby inne klasy mogły uzyskać dostęp do map.

Tulains Córdova
źródło
0

Użyłbym tej samej odpowiedzi, którą podałem gdzie indziej: weźcie klasę monolityczną i podzielcie jej obowiązki na inne klasy. Zarówno DCI, jak i wzorzec odwiedzin zapewniają dobre opcje.

Mario T. Lanza
źródło
-1

Pod względem wielkości oprogramowania dużą klasą jest torba. Istnieją nieograniczone dokumenty potwierdzające to oświadczenie. Dlaczego ? ponieważ duże klasy są trudniejsze do zrozumienia niż małe klasy i potrzeba więcej czasu na modyfikację. Co więcej, duże klasy są bardzo trudne podczas testów. Duże klasy są dla ciebie bardzo trudne, gdy chcesz ich ponownie użyć, ponieważ najprawdopodobniej zawierają niepożądane rzeczy.

cat_minhv0
źródło