CodeGym /Java блог /Случаен /Какво представляват антимоделите? Нека да разгледаме няко...
John Squirrels
Ниво
San Francisco

Какво представляват антимоделите? Нека да разгледаме някои примери (Част 1)

Публикувано в групата
Хубав ден на всички! Онзи ден бях на интервю за работа и ми зададоха няколко въпроса за анти-шаблоните: Howво представляват, Howви типове има и Howви практически примери има. Разбира се, отговорих на въпроса, но много повърхностно, тъй като преди това не бях навлизал дълбоко в тази тема. След интервюто започнах да се ровя в интернет и да се потапям все повече и повече в темата. Какво представляват антимоделите?  Нека да разгледаме някои примери (Част 1) - 1 Днес бих искал да направя кратък преглед на най-популярните анти-модели и да прегледам някои примери. Надявам се, че четенето на това ще ви даде знанията, от които се нуждаете в тази област. Да започваме! Преди да обсъдим Howво е анти-модел, нека си припомним Howво е модел на проектиране. Модел на дизайне повторяемо архитектурно решение за често срещани проблеми or ситуации, които възникват при проектирането на приложение. Но днес не говорим за тях, а по-скоро за техните противоположности - антимодели. Анти -моделът е широко разпространен, но неефективен, рискован и/or непродуктивен подход за решаване на клас общи проблеми. С други думи, това е модел на грешки (наричан понякога и капан). Като правило анти-шаблоните се разделят на следните типове:
  1. Архитектурни анти-модели — Тези анти-модели възникват, когато структурата на системата е проектирана (обикновено от архитект).
  2. Управленски/организационни антимодели — Това са антимодели в управлението на проекти, обикновено срещани от различни мениджъри (or групи от мениджъри).
  3. Анти-шаблони за разработка — Тези анти-модели възникват, когато системата се внедрява от обикновени програмисти.
Пълният набор от антимодели е много по-екзотичен, но няма да ги разглеждаме всички днес. За обикновените разработчици това би било твърде много. Като за начало, нека разгледаме анти-модел за управление като пример.

1. Аналитична парализа

Парализа на анализасе счита за класически анти-модел на управление. Това включва прекомерно анализиране на ситуацията по време на планирането, така че да не се предприемат ниHowви решения or действия, което по същество парализира процеса на развитие. Това често се случва, когато целта е да се постигне съвършенство и да се вземе предвид абсолютно всичко в периода на анализ. Този анти-модел се характеризира с ходене в кръг (обикновен затворен цикъл), преразглеждане и създаване на подробни модели, което от своя страна пречи на работния процес. Например, вие се опитвате да предвидите нещата на ниво: но Howво ще стане, ако потребител внезапно поиска да създаде списък със служители въз основа на четвъртата и петата буква от името им, включително списъка с проекти, на които са прекарали най-много работни часове между Нова година и Международния ден на жената през последните четири години? По същество това твърде много анализи. Ето няколко съвета за борба с парализата на анализа:
  1. Трябва да определите дългосрочна цел като маяк за вземане на решения, така че всяко ваше решение да ви приближава по-близо до целта, instead of да ви кара да стагнирате.
  2. Не се концентрирайте върху дреболии (защо да вземате решение за незначителен детайл, сякаш това е най-важното решение в живота ви?)
  3. Поставете краен срок за решение.
  4. Не се опитвайте да изпълните една задача перфектно - по-добре е да я направите много добре.
Няма нужда да навлизаме твърде дълбоко тук, така че няма да разглеждаме други управленски антимодели. Следователно, без ниHowво въведение, ще преминем към някои архитектурни анти-модели, защото тази статия е по-вероятно да бъде прочетена от бъдещи разработчици, а не от мениджъри.

2. Бог обект

Бог обект е анти-модел, който описва прекомерна концентрация на всяHowви функции и големи количества различни данни (обект, около който се върти приложението). Вземете малък пример:

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();
   }
}
Тук виждаме огромен клас, който прави всичко. Той съдържа заявки за база данни, Howто и някои данни. Виждаме и фасадния метод findAllWithoutPageEn, който включва бизнес логика. Такъв божествен обект става огромен и неудобен за правилна поддръжка. Трябва да се забъркваме с него във всяко парче code. Много системни компоненти разчитат на него и са тясно свързани с него. Става все по-трудно да се поддържа такъв code. В такива случаи codeът трябва да бъде разделен на отделни класове, всеки от които ще има само една цел. В този пример можем да разделим обекта Бог в клас 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());
   }
  
                               ........
}
Клас, съдържащ данни и методи за достъп до данните:

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;
   }
                    ....
И би било по-подходящо да преместите метода с бизнес логика в услуга:

private List<User> findAllWithoutPageEn(Long permissionId, Type type) {
   switch (type) {
       case USERS:
           return findAllEnUsers(permissionId);
       case CUSTOMERS:
           return findAllEnCustomers(permissionId);
       default:
           return findAllEn();
   }
}

3. Сингълтън

Единичен е най-простият модел. Той гарантира, че в еднонишково приложение ще има един екземпляр на клас и предоставя глобална точка за достъп до този обект. Но модел ли е or антимодел? Нека да разгледаме недостатъците на този модел:
  1. Глобално състояние Когато имаме достъп до екземпляра на класа, ние не знаем текущото състояние на този клас. Не знаем кой or кога го е променил. Държавата може да не е нищо подобно на това, което очакваме. С други думи, коректността на работа със сингълтон зависи от реда на достъп до него. Това означава, че подсистемите са зависими една от друга и в резултат на това дизайнът става сериозно по-сложен.

  2. Единичният клас нарушава принципите на SOLID — принципът на единичната отговорност: в допълнение към преките си задължения, единичният клас също контролира броя на екземплярите.

  3. Зависимостта на обикновен клас от сингълтън не се вижда в интерфейса на класа. Тъй като екземплярът сингълтон обикновено не се предава като аргумент на метода, а instead of това се получава директно чрез getInstance(), трябва да навлезете в имплементацията на всеки метод, за да идентифицирате зависимостта на класа от сингълтона – просто гледайки публичното пространство на класа договор не е достатъчен.

    Наличието на сингълтон намалява възможността за тестване на приложението като цяло и в частност на класовете, които използват сингълтона. Първо, не можете да замените сингълтона с макет обект. Второ, ако сингълтън има интерфейс за промяна на състоянието си, тогава тестовете ще зависят един от друг.

    С други думи, сингълтън увеличава свързването и всичко споменато по-горе не е нищо повече от следствие от увеличеното свързване.

    И ако се замислите, можете да избегнете използването на сингълтън. Например, напълно е възможно (и наистина необходимо) да се използват различни видове фабрики, за да се контролира броят на екземплярите на даден обект.

    Най-голямата опасност се крие в опитите да се изгради цяла архитектура на applications, базирана на сингълтони. Има много прекрасни алтернативи на този подход. Най-важният пример е Spring, а именно неговите IoC контейнери: те са естествено решение на проблема с контролирането на създаването на услуги, тъй като всъщност са „фабрики на стероиди“.

    Много безкрайни и непримирими дебати сега бушуват по тази тема. От вас зависи да решите дали сингълтън е шаблон or анти-модел.

    Няма да се бавим на него. Вместо това ще преминем към последния дизайнерски модел за днес — полтъргайст.

4. Полтъргайст

Полтъргайст е анти-модел, включващ безсмислен клас, който се използва за извикване на методи от друг клас or просто добавя ненужен слой абстракция . Този антимодел се проявява като краткотрайни обекти, лишени от състояние. Тези обекти често се използват за инициализиране на други, по-постоянни обекти.

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);
   }
}
Защо се нуждаем от обект, който е просто посредник и делегира работата си на някой друг? Ние го елиминираме и прехвърляме малката функционалност, която имаше, към дълготрайни обекти. След това преминаваме към моделите, които са от най-голям интерес за нас (като обикновени разработчици), т.е. анти-модели за разработка .

5. Твърдо codeиране

Така стигнахме до тази ужасна дума: твърдо codeиране. Същността на този анти-модел е, че codeът е силно обвързан с конкретна хардуерна конфигурация и/or системна среда. Това значително усложнява пренасянето на codeа към други конфигурации. Този антимодел е тясно свързан с магически числа (тези антимодели често са преплетени). Пример:

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;
}
Боли, нали? Тук codeираме твърдо нашите настройки за връзка. В резултат на това codeът ще работи правилно само с MySQL. За да променим базата данни, ще трябва да се потопим в codeа и да променим всичко ръчно. Добро решение би било да поставите конфигурацията в отделен файл:

spring:
  datasource:
    jdbc-url:jdbc:mysql://localhost:3306/someDb?characterEncoding=UTF-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username:  user01
    password:  12345qwert
Друг вариант е да използвате константи.

6. Котва за лодка

В контекста на анти-шаблоните, котва за лодка означава запазване на части от системата, които вече не се използват след извършване на известна оптимизация or рефакторинг. Също така, някои части от codeа могат да бъдат запазени „за бъдеща употреба“ само в случай, че внезапно ви потрябват. По същество това превръща вашия code в кофа за боклук. Пример:

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());
}
Имаме метод за актуализиране, който използва отделен метод за обединяване на потребителски данни от базата данни с потребителските данни, предадени на метода (ако потребителят, предаден на метода за актуализиране, има нулево поле, тогава старата стойност на полето се взема от базата данни) . Тогава да предположим, че има ново изискване, че записите не трябва да се обединяват със старите, но instead of това, дори ако има нулеви полета, те се използват за презаписване на старите:

public User update(Long id, User request) {
   return userDAO.update(user);
}
Това означава, че mergeUser вече не се използва, но би било жалко да го изтриете — ами ако този метод (or идеята за този метод) може да ви бъде полезен някой ден? Такъв code само усложнява системите и въвежда объркване, като по същество няма практическа стойност. Не трябва да забравяме, че такъв code с "мъртви парчета" ще бъде трудно да се предаде на колега, когато напуснете за друг проект. Най-добрият начин да се справите с котви за лодки е да преработите codeа, т.е. да изтриете части от codeа (сърцераздирателно, знам). Освен това, когато се изготвя графикът за развитие, е необходимо да се вземат предвид такива котви (да се отдели време за подреждане).

7. Обект помийна яма

За да опишете този анти-модел, първо трябва да се запознаете с модела на набора от обекти . Обектният пул (ресурсният пул) е модел за създаване на дизайн , набор от инициализирани и готови за използване обекти. Когато дадено приложение се нуждае от обект, той се взема от този пул, instead of да се създава отново. Когато даден обект вече не е необходим, той не се унищожава. Вместо това се връща в басейна. Този модел обикновено се използва за тежки обекти, чието създаване отнема много време всеки път, когато са необходими, като например при свързване към база данни. Нека да разгледаме малък и прост пример. Ето един клас, който представя този модел:

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);
   }
}
Този клас е представен под формата на горния сингълтън модел/анти-модел, т.е. може да има само един обект от този тип. Използва определени Resourceпредмети. По подразбиране конструкторът запълва пула с 4 екземпляра. Когато получите обект, той се премахва от пула (ако няма наличен обект, такъв се създава и веднага се връща). И накрая, имаме метод да върнем обекта обратно. Ресурсните обекти изглеждат така:

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;
   }
}
Тук имаме малък обект, съдържащ карта с имена на модели на дизайн като ключ и съответните връзки към Wikipedia като стойност, Howто и методи за достъп до картата. Нека да разгледаме основните:

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);
   }
}
Всичко тук е достатъчно ясно: получаваме пул обект, получаваме обект с ресурси от пула, получаваме картата от ресурсен обект, правим нещо с него и поставяме всичко това на мястото му в пула за по-нататъшна повторна употреба. Ето, това е шаблонът за проектиране на набор от обекти. Но говорихме за антимодели, нали? Нека разгледаме следния случай в основния метод:

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);
Тук отново получаваме ресурсен обект, получаваме неговата карта от модели и правим нещо с картата. Но преди да се запази картата обратно в набора от обекти, тя се изчиства и след това се попълва с повредени данни, което прави обекта Resource неподходящ за повторна употреба. Един от основните детайли на пул обекти е, че когато даден обект бъде върнат, той трябва да се възстанови до състояние, подходящо за по-нататъшна повторна употреба. Ако обектите, върнати в пула, останат в неправилно or недефинирано състояние, тогава нашият дизайн се нарича обектна помийна яма. Има ли смисъл да съхраняваме предмети, които не са подходящи за повторна употреба? В тази ситуация можем да направим вътрешната карта неизменна в конструктора:

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);
}
Опитите и желанието за промяна на съдържанието на картата ще изчезнат благодарение на UnsupportedOperationException, което ще генерират. Анти-шаблоните са капани, с които разработчиците се сблъскват често поради остра липса на време, небрежност, неопитност or натиск от ръководителите на проекти. Бързането, което е често срещано, може да доведе до големи проблеми за приложението в бъдеще, така че трябва да знаете за тези грешки и да ги избягвате предварително. С това приключваме първата част на статията. Следва продължение...
Коментари
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION