- Architektoniczne antywzorce — te antywzorce powstają podczas projektowania struktury systemu (zwykle przez architekta).
- Antywzorce zarządcze/organizacyjne — Są to antywzorce w zarządzaniu projektami, z którymi zwykle spotykają się różni menedżerowie (lub grupy menedżerów).
- Antywzorce programistyczne — te antywzorce powstają, gdy system jest wdrażany przez zwykłych programistów.
1. Paraliż analityczny
Paraliż analitycznyjest uważany za klasyczny antywzorzec zarządzania. Polega na nadmiernym analizowaniu sytuacji podczas planowania, tak aby nie została podjęta żadna decyzja ani działanie, co zasadniczo paraliżuje proces rozwoju. Często dzieje się tak, gdy celem jest osiągnięcie perfekcji i uwzględnienie absolutnie wszystkiego w okresie analizy. Ten antywzorzec charakteryzuje się chodzeniem w kółko (zamknięta pętla), przeglądaniem i tworzeniem szczegółowych modeli, co z kolei zakłóca przepływ pracy. Na przykład próbujesz przewidzieć rzeczy na poziomie: ale co, jeśli użytkownik nagle chce utworzyć listę pracowników na podstawie czwartej i piątej litery ich nazwiska, w tym listę projektów, nad którymi spędził najwięcej godzin pracy między Nowym Rokiem a Międzynarodowym Dniem Kobiet w ciągu ostatnich czterech lat? W istocie to' za dużo analiz. Oto kilka wskazówek, jak walczyć z paraliżem analitycznym:- Musisz zdefiniować cel długoterminowy jako drogowskaz do podejmowania decyzji, tak aby każda z twoich decyzji przybliżała cię do celu, a nie powodowała stagnację.
- Nie skupiaj się na drobiazgach (po co podejmować decyzję o nieistotnym szczególe, jakby to była najważniejsza decyzja w Twoim życiu?)
- Ustal termin decyzji.
- Nie próbuj wykonać zadania perfekcyjnie — lepiej zrobić to bardzo dobrze.
2. Obiekt Boga
Obiekt Boga to antywzorzec, który opisuje nadmierną koncentrację wszelkiego rodzaju funkcji i dużą ilość odmiennych danych (obiekt, wokół którego obraca się aplikacja). Weź mały przykład:
public class SomeUserGodObject {
private static final String FIND_ALL_USERS_EN = "SELECT id, email, phone, first_name_en, access_counter, middle_name_en, last_name_en, created_date FROM users;
private static final String FIND_BY_ID = "SELECT id, email, phone, first_name_en, access_counter, middle_name_en, last_name_en, created_date FROM users WHERE id = ?";
private static final String FIND_ALL_CUSTOMERS = "SELECT id, u.email, u.phone, u.first_name_en, u.middle_name_en, u.last_name_en, u.created_date" +
" WHERE u.id IN (SELECT up.user_id FROM user_permissions up WHERE up.permission_id = ?)";
private static final String FIND_BY_EMAIL = "SELECT id, email, phone, first_name_en, access_counter, middle_name_en, last_name_en, created_dateFROM users WHERE email = ?";
private static final String LIMIT_OFFSET = " LIMIT ? OFFSET ?";
private static final String ORDER = " ORDER BY ISNULL(last_name_en), last_name_en, ISNULL(first_name_en), first_name_en, ISNULL(last_name_ru), " +
"last_name_ru, ISNULL(first_name_ru), first_name_ru";
private static final String CREATE_USER_EN = "INSERT INTO users(id, phone, email, first_name_en, middle_name_en, last_name_en, created_date) " +
"VALUES (?, ?, ?, ?, ?, ?, ?)";
private static final String FIND_ID_BY_LANG_CODE = "SELECT id FROM languages WHERE lang_code = ?";
........
private final JdbcTemplate jdbcTemplate;
private Map<String, String> firstName;
private Map<String, String> middleName;
private Map<String, String> lastName;
private List<Long> permission;
........
@Override
public List<User> findAllEnCustomers(Long permissionId) {
return jdbcTemplate.query( FIND_ALL_CUSTOMERS + ORDER, userRowMapper(), permissionId);
}
@Override
public List<User> findAllEn() {
return jdbcTemplate.query(FIND_ALL_USERS_EN + ORDER, userRowMapper());
}
@Override
public Optional<List<User>> findAllEnByEmail(String email) {
var query = FIND_ALL_USERS_EN + FIND_BY_EMAIL + ORDER;
return Optional.ofNullable(jdbcTemplate.query(query, userRowMapper(), email));
}
.............
private List<User> findAllWithoutPageEn(Long permissionId, Type type) {
switch (type) {
case USERS:
return findAllEnUsers(permissionId);
case CUSTOMERS:
return findAllEnCustomers(permissionId);
default:
return findAllEn();
}
}
..............…
private RowMapper<User> userRowMapperEn() {
return (rs, rowNum) ->
User.builder()
.id(rs.getLong("id"))
.email(rs.getString("email"))
.accessFailed(rs.getInt("access_counter"))
.createdDate(rs.getObject("created_date", LocalDateTime.class))
.firstName(rs.getString("first_name_en"))
.middleName(rs.getString("middle_name_en"))
.lastName(rs.getString("last_name_en"))
.phone(rs.getString("phone"))
.build();
}
}
Tutaj widzimy ogromną klasę, która robi wszystko. Zawiera zapytania do bazy danych, a także niektóre dane. Widzimy również metodę fasady findAllWithoutPageEn, która obejmuje logikę biznesową. Taki obiekt Boga staje się ogromny i niewygodny w utrzymaniu. Musimy się z tym bawić w każdym fragmencie kodu. Wiele elementów systemu opiera się na nim i jest z nim ściśle powiązanych. Utrzymanie takiego kodu staje się coraz trudniejsze. W takich przypadkach kod powinien być podzielony na osobne klasy, z których każda będzie miała tylko jeden cel. W tym przykładzie możemy podzielić obiekt Boga na klasę Dao:
public class UserDaoImpl {
private static final String FIND_ALL_USERS_EN = "SELECT id, email, phone, first_name_en, access_counter, middle_name_en, last_name_en, created_date FROM users;
private static final String FIND_BY_ID = "SELECT id, email, phone, first_name_en, access_counter, middle_name_en, last_name_en, created_date FROM users WHERE id = ?";
........
private final JdbcTemplate jdbcTemplate;
........
@Override
public List<User> findAllEnCustomers(Long permissionId) {
return jdbcTemplate.query(FIND_ALL_CUSTOMERS + ORDER, userRowMapper(), permissionId);
}
@Override
public List<User> findAllEn() {
return jdbcTemplate.query(FIND_ALL_USERS_EN + ORDER, userRowMapper());
}
........
}
Klasa zawierająca dane i metody dostępu do danych:
public class UserInfo {
private Map<String, String> firstName;
…..
public Map<String, String> getFirstName() {
return firstName;
}
public void setFirstName(Map<String, String> firstName) {
this.firstName = firstName;
}
....
I bardziej odpowiednie byłoby przeniesienie metody z logiką biznesową do usługi:
private List<User> findAllWithoutPageEn(Long permissionId, Type type) {
switch (type) {
case USERS:
return findAllEnUsers(permissionId);
case CUSTOMERS:
return findAllEnCustomers(permissionId);
default:
return findAllEn();
}
}
3. Singleton
Singleton jest najprostszym wzorcem. Zapewnia, że w aplikacji jednowątkowej będzie tylko jedna instancja klasy i zapewnia globalny punkt dostępu do tego obiektu. Ale czy jest to wzorzec czy anty-wzorzec? Spójrzmy na wady tego wzorca:-
Stan globalny Gdy uzyskujemy dostęp do instancji klasy, nie znamy aktualnego stanu tej klasy. Nie wiemy, kto i kiedy to zmienił. Stan może nie być taki, jakiego oczekujemy. Innymi słowy, poprawność pracy z singletonem zależy od kolejności dostępów do niego. Oznacza to, że podsystemy są od siebie zależne, w wyniku czego projekt staje się znacznie bardziej złożony.
-
Singleton narusza zasady SOLID — zasadę pojedynczej odpowiedzialności: oprócz swoich bezpośrednich obowiązków, klasa singleton kontroluje również liczbę instancji.
-
Zależność zwykłej klasy od singletonu nie jest widoczna w interfejsie klasy. Ponieważ pojedyncza instancja zwykle nie jest przekazywana jako argument metody, ale jest uzyskiwana bezpośrednio przez getInstance(), musisz przejść do implementacji każdej metody, aby zidentyfikować zależność klasy od singletonu — wystarczy spojrzeć na publiczną klasę umowa nie wystarczy.
Obecność singletonu zmniejsza testowalność aplikacji jako całości, aw szczególności klas korzystających z singletonu. Po pierwsze, nie można zastąpić singletonu próbnym obiektem. Po drugie, jeśli singleton ma interfejs do zmiany swojego stanu, testy będą od siebie zależne.
Innymi słowy, singleton zwiększa sprzężenie, a wszystko, o czym wspomniano powyżej, jest niczym więcej niż konsekwencją zwiększonego sprzężenia.
A jeśli się nad tym zastanowisz, możesz uniknąć używania singletona. Na przykład całkiem możliwe (i rzeczywiście konieczne) jest użycie różnego rodzaju fabryk do kontrolowania liczby wystąpień obiektu.
Największym niebezpieczeństwem jest próba zbudowania całej architektury aplikacji w oparciu o singletony. Istnieje mnóstwo wspaniałych alternatyw dla tego podejścia. Najważniejszym przykładem jest Spring, a dokładniej jego kontenery IoC: są naturalnym rozwiązaniem problemu kontrolowania tworzenia usług, ponieważ są tak naprawdę „fabrykami na sterydach”.
Na ten temat toczy się obecnie wiele niekończących się i nieprzejednanych debat. To Ty decydujesz, czy singleton jest wzorcem, czy antywzorcem.
Nie będziemy na tym poprzestawać. Zamiast tego przejdziemy do ostatniego wzorca projektowego na dziś — poltergeista.
4. Poltergeista
Poltergeist to antywzorzec obejmujący bezsensowną klasę, który służy do wywoływania metod innej klasy lub po prostu dodaje zbędną warstwę abstrakcji . Ten antywzorzec przejawia się jako obiekty krótkotrwałe, pozbawione stanu. Obiekty te są często używane do inicjowania innych, bardziej trwałych obiektów.
public class UserManager {
private UserService service;
public UserManager(UserService userService) {
service = userService;
}
User createUser(User user) {
return service.create(user);
}
Long findAllUsers(){
return service.findAll().size();
}
String findEmailById(Long id) {
return service.findById(id).getEmail();}
User findUserByEmail(String email) {
return service.findByEmail(email);
}
User deleteUserById(Long id) {
return service.delete(id);
}
}
Po co nam obiekt, który jest tylko pośrednikiem i deleguje swoją pracę komuś innemu? Eliminujemy go i przenosimy niewielką funkcjonalność, jaką posiadał, na obiekty długowieczne. Następnie przechodzimy do wzorców, które najbardziej nas interesują (jako zwykłych programistów), czyli antywzorców programistycznych .
5. Twarde kodowanie
Dotarliśmy więc do tego okropnego słowa: twarde kodowanie. Istotą tego antywzorca jest to, że kod jest silnie powiązany z określoną konfiguracją sprzętową i/lub środowiskiem systemowym. To znacznie komplikuje przenoszenie kodu do innych konfiguracji. Ten antywzorzec jest ściśle powiązany z liczbami magicznymi (te antywzorce często się przeplatają). Przykład:
public Connection buildConnection() throws Exception {
Class.forName("com.mysql.cj.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/someDb?characterEncoding=UTF-8&characterSetResults=UTF-8&serverTimezone=UTC", "user01", "12345qwert");
return connection;
}
Boli, prawda? Tutaj na stałe kodujemy nasze ustawienia połączenia. W rezultacie kod będzie działał poprawnie tylko z MySQL. Aby zmienić bazę danych, będziemy musieli zagłębić się w kod i zmienić wszystko ręcznie. Dobrym rozwiązaniem byłoby umieszczenie konfiguracji w osobnym pliku:
spring:
datasource:
jdbc-url:jdbc:mysql://localhost:3306/someDb?characterEncoding=UTF-8
driver-class-name: com.mysql.cj.jdbc.Driver
username: user01
password: 12345qwert
Inną opcją jest użycie stałych.
6. Kotwica łodzi
W kontekście antywzorców kotwica łodzi oznacza zachowanie części systemu, które nie są już używane po przeprowadzeniu optymalizacji lub refaktoryzacji. Ponadto niektóre części kodu można zachować „do wykorzystania w przyszłości” na wypadek, gdybyś nagle ich potrzebował. Zasadniczo zamienia to twój kod w śmietnik. Przykład:
public User update(Long id, User request) {
User user = mergeUser(findById(id), request);
return userDAO.update(user);
}
private User mergeUser(User findUser, User requestUser) {
return new User(
findUser.getId(),
requestUser.getEmail() != null ? requestUser.getEmail() : findUser.getEmail(),
requestUser.getFirstName() != null ? requestUser.getFirstName() : findUser.getFirstNameRu(),
requestUser.getMiddleName() != null ? requestUser.getMiddleName() : findUser.getMiddleNameRu(),
requestUser.getLastName() != null ? requestUser.getLastName() : findUser.getLastNameEn(),
requestUser.getPhone() != null ? requestUser.getPhone() : findUser.getPhone());
}
Mamy metodę aktualizacji, która wykorzystuje oddzielną metodę do scalania danych użytkownika z bazy danych z danymi użytkownika przekazanymi do metody (jeśli użytkownik przekazany do metody aktualizacji ma pole null, to stara wartość pola jest pobierana z bazy danych) . Załóżmy więc, że istnieje nowe wymaganie, że rekordy nie mogą być łączone ze starymi, ale zamiast tego, nawet jeśli istnieją pola zerowe, są one używane do nadpisywania starych:
public User update(Long id, User request) {
return userDAO.update(user);
}
Oznacza to, że mergeUser nie jest już używany, ale szkoda byłoby go usunąć — a co, jeśli ta metoda (lub idea tej metody) może się kiedyś przydać? Taki kod jedynie komplikuje systemy i wprowadza zamieszanie, nie mając zasadniczo żadnej wartości praktycznej. Nie wolno nam zapominać, że taki kod z „martwymi fragmentami” będzie trudny do przekazania koledze, gdy wyjeżdżasz do innego projektu. Najlepszym sposobem radzenia sobie z kotwicami łodzi jest refaktoryzacja kodu, tj. usunięcie sekcji kodu (wiem, że to bolesne). Dodatkowo, przygotowując harmonogram zabudowy, należy uwzględnić takie kotwice (aby wygospodarować czas na uporządkowanie).
7. Szambo obiektowe
Aby opisać ten antywzorzec, najpierw musisz zapoznać się ze wzorcem puli obiektów . Pula obiektów (pula zasobów) to kreacyjny wzorzec projektowy , zbiór zainicjowanych i gotowych do użycia obiektów. Gdy aplikacja potrzebuje obiektu, jest on pobierany z tej puli, a nie odtwarzany. Kiedy przedmiot nie jest już potrzebny, nie ulega zniszczeniu. Zamiast tego wraca do puli. Ten wzorzec jest zwykle używany w przypadku ciężkich obiektów, których tworzenie jest czasochłonne za każdym razem, gdy są potrzebne, na przykład podczas łączenia się z bazą danych. Spójrzmy na mały i prosty przykład. Oto klasa reprezentująca ten wzorzec:
class ReusablePool {
private static ReusablePool pool;
private List<Resource> list = new LinkedList<>();
private ReusablePool() {
for (int i = 0; i < 3; i++)
list.add(new Resource());
}
public static ReusablePool getInstance() {
if (pool == null) {
pool = new ReusablePool();
}
return pool;
}
public Resource acquireResource() {
if (list.size() == 0) {
return new Resource();
} else {
Resource r = list.get(0);
list.remove(r);
return r;
}
}
public void releaseResource(Resource r) {
list.add(r);
}
}
Klasa ta jest przedstawiona w postaci powyższego singletonowego wzorca/antywzorca, czyli może istnieć tylko jeden obiekt tego typu. Wykorzystuje określone Resource
obiekty. Domyślnie konstruktor wypełnia pulę 4 instancjami. Gdy otrzymasz obiekt, jest on usuwany z puli (jeśli nie ma dostępnego obiektu, obiekt jest tworzony i natychmiast zwracany). I na koniec mamy sposób na odłożenie obiektu z powrotem. Obiekty zasobów wyglądają następująco:
public class Resource {
private Map<String, String> patterns;
public Resource() {
patterns = new HashMap<>();
patterns.put("proxy", "https://en.wikipedia.org/wiki/Proxy_pattern");
patterns.put("bridge", "https://en.wikipedia.org/wiki/Bridge_pattern");
patterns.put("facade", "https://en.wikipedia.org/wiki/Facade_pattern");
patterns.put("builder", "https://en.wikipedia.org/wiki/Builder_pattern");
}
public Map<String, String> getPatterns() {
return patterns;
}
public void setPatterns(Map<String, String> patterns) {
this.patterns = patterns;
}
}
Tutaj mamy mały obiekt zawierający mapę z nazwami wzorców projektowych jako kluczem i odpowiednimi linkami do Wikipedii jako wartością, a także metodami dostępu do mapy. Rzućmy okiem na główne:
class SomeMain {
public static void main(String[] args) {
ReusablePool pool = ReusablePool.getInstance();
Resource firstResource = pool.acquireResource();
Map<String, String> firstPatterns = firstResource.getPatterns();
// use our map somehow...
pool.releaseResource(firstResource);
Resource secondResource = pool.acquireResource();
Map<String, String> secondPatterns = firstResource.getPatterns();
// use our map somehow...
pool.releaseResource(secondResource);
Resource thirdResource = pool.acquireResource();
Map<String, String> thirdPatterns = firstResource.getPatterns();
// use our map somehow...
pool.releaseResource(thirdResource);
}
}
Tutaj wszystko jest wystarczająco jasne: bierzemy obiekt puli, pobieramy obiekt z zasobami z puli, pobieramy mapę z obiektu Resource, coś z tym robimy i umieszczamy to wszystko na swoim miejscu w puli do dalszego wykorzystania. Voila, to jest wzorzec projektowy puli obiektów. Ale mówiliśmy o antywzorcach, prawda? Rozważmy następujący przypadek w metodzie main:
Resource fourthResource = pool.acquireResource();
Map<String, String> fourthPatterns = firstResource.getPatterns();
// use our map somehow...
fourthPatterns.clear();
firstPatterns.put("first","blablabla");
firstPatterns.put("second","blablabla");
firstPatterns.put("third","blablabla");
firstPatterns.put("fourth","blablabla");
pool.releaseResource(fourthResource);
Tutaj ponownie otrzymujemy obiekt Resource, otrzymujemy jego mapę wzorców i robimy coś z mapą. Jednak przed zapisaniem mapy z powrotem do puli obiektów jest ona czyszczona, a następnie wypełniana uszkodzonymi danymi, przez co obiekt Resource nie nadaje się do ponownego użycia. Jednym z głównych szczegółów puli obiektów jest to, że po zwróceniu obiektu musi on zostać przywrócony do stanu odpowiedniego do ponownego wykorzystania. Jeśli obiekty zwrócone do puli pozostają w nieprawidłowym lub niezdefiniowanym stanie, nasz projekt nazywa się szambo obiektowym. Czy przechowywanie przedmiotów, które nie nadają się do ponownego użycia ma sens? W tej sytuacji możemy uczynić mapę wewnętrzną niezmienną w konstruktorze:
public Resource() {
patterns = new HashMap<>();
patterns.put("proxy", "https://en.wikipedia.org/wiki/Proxy_pattern");
patterns.put("bridge", "https://en.wikipedia.org/wiki/Bridge_pattern");
patterns.put("facade", "https://en.wikipedia.org/wiki/Facade_pattern");
patterns.put("builder", "https://en.wikipedia.org/wiki/Builder_pattern");
patterns = Collections.unmodifiableMap(patterns);
}
Próby i chęć zmiany zawartości mapy znikną dzięki wygenerowanemu przez nie wyjątkowi UnsupportedOperationException. Antywzorce to pułapki, na które programiści często napotykają z powodu dotkliwego braku czasu, nieostrożności, braku doświadczenia lub presji ze strony kierowników projektów. Pośpiech, który jest powszechny, może prowadzić do dużych problemów dla aplikacji w przyszłości, dlatego musisz wiedzieć o tych błędach i unikać ich z wyprzedzeniem. Na tym kończy się pierwsza część artykułu. Ciąg dalszy nastąpi...
GO TO FULL VERSION