大家好!前幾天我有一個工作面試,我被問到一些關於反模式的問題:它們是什麼,有哪些類型,以及有哪些實際例子。當然,我回答了這個問題,但非常膚淺,因為我以前沒有深入探討過這個話題。面試結束後,我開始上網刷題,越來越沉浸在話題中。 什麼是反模式? 讓我們看一些例子(第 1 部分)- 1 今天,我想對最流行的反模式進行簡要概述,並回顧一些示例。我希望閱讀本文能為您提供該領域所需的知識。讓我們開始吧!在我們討論什麼是反模式之前,讓我們回顧一下什麼是設計模式。一種設計模式是針對設計應用程序時出現的常見問題或情況的可重複架構解決方案。但今天我們不是在談論它們,而是在談論它們的對立面——反模式。反模式是解決一類常見問題的廣泛但無效、有風險和/或無成效的方法。換句話說,這是一種錯誤模式(有時也稱為陷阱)。通常,反模式分為以下類型:
  1. 架構反模式——這些反模式在系統結構設計時出現(通常由架構師設計)。
  2. 管理/組織反模式——這些是項目管理中的反模式,通常由不同的經理(或經理組)遇到。
  3. 開發反模式——這些反模式是在普通程序員實現系統時出現的。
全方位的反模式更加奇特,但我們今天不會考慮它們。對於普通開發人員來說,這太過分了。對於初學者,讓我們以管理反模式為例。

1.分析癱瘓

分析癱瘓被認為是經典的管理反模式。它涉及在規劃過程中過度分析情況,以至於沒有採取任何決定或行動,從根本上使開發過程陷入癱瘓。當目標是實現完美並在分析期間絕對考慮所有事情時,通常會發生這種情況。這種反模式的特點是原地踏步(一個普通的閉環),修改和創建詳細的模型,這反過來又會干擾工作流程。例如,您正在嘗試在某個級別上預測事物:但是如果用戶突然想根據他們姓名的第四個和第五個字母創建一個員工列表,包括他們花費最多工作時間的項目列表怎麼辦?在過去的四年裡,在元旦和國際婦女節之間?本質上,它' 分析太多了。以下是一些對抗分析癱瘓的技巧:
  1. 你需要將一個長期目標定義為決策的燈塔,這樣你的每一個決定都會讓你離目標更近,而不是讓你停滯不前。
  2. 不要專注於瑣事(為什麼要對一個微不足道的細節做出決定,就好像這是你一生中最重要的決定?)
  3. 為決定設定最後期限。
  4. 不要試圖完美地完成一項任務——最好把它做得很好。
這裡不需要太深入,所以我們不會考慮其他管理反模式。因此,在沒有任何介紹的情況下,我們將繼續討論一些架構反模式,因為這篇文章最有可能被未來的開發人員而不是管理人員閱讀。

2.神物

上帝對像是一種反模式,它描述了各種功能的過度集中和大量不同的數據(應用程序圍繞的對象)。舉個小例子:

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();
   }
}
在這裡,我們看到一個巨大的類,它可以做任何事情。它包含數據庫查詢以及一些數據。我們還看到了包含業務邏輯的 findAllWithoutPageEn 外觀方法。這樣的上帝對像變得巨大且難以正確維護。我們必須在每一段代碼中都弄亂它。許多系統組件都依賴它並與之緊密耦合。維護這樣的代碼變得越來越難。在這種情況下,代碼應該被拆分成單獨的類,每個類只有一個目的。在這個例子中,我們可以將 God 對象拆分成一個 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.單例

單例是最簡單的模式。它確保在單線程應用程序中將有一個類的單個實例,並提供對該對象的全局訪問點。但它是模式還是反模式?讓我們看看這種模式的缺點:
  1. 全局狀態 當我們訪問類的實例時,我們並不知道這個類的當前狀態。我們不知道是誰或何時更改了它。狀態可能並不像我們期望的那樣。換句話說,使用單例的正確性取決於訪問它的順序。這意味著子系統相互依賴,因此設計變得非常複雜。

  2. 單例違反了 SOLID 原則——單一職責原則:單例類除了直接的職責外,還控制著實例的數量。

  3. 普通類對單例的依賴在類的接口中是不可見的。因為單例實例通常不會作為方法參數傳遞,而是直接通過 getInstance() 獲取,因此您需要深入到每個方法的實現中才能識別類對單例的依賴——只需查看類的 public合同是不夠的。

    單例的存在降低了整個應用程序的可測試性,尤其是使用單例的類。首先,您不能用模擬對象替換單例。其次,如果一個單例有一個改變其狀態的接口,那麼測試將相互依賴。

    換句話說,單例增加了耦合,上面所說的一切無非是增加了耦合的結果。

    而且如果你考慮一下,你可以避免使用單例。例如,很可能(而且確實有必要)使用各種工廠來控制對象的實例數。

    最大的危險在於試圖構建基於單例的整個應用程序架構。這種方法有很多很棒的替代方法。最重要的例子是 Spring,即它的 IoC 容器:它們是控制服務創建問題的自然解決方案,因為它們實際上是“類固醇工廠”。

    許多沒完沒了、不可調和的辯論現在正圍繞這個主題展開。由您決定單例是模式還是反模式。

    我們不會在上面逗留。相反,我們將繼續討論今天的最後一個設計模式 — poltergeist。

4.鬧鬼

poltergeist是一種涉及無意義類的反模式,該類用於調用另一個類的方法或只是添加一個不必要的抽象層這種反模式表現為短暫的對象,沒有狀態。這些對象通常用於初始化其他更永久的對象。

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.硬編碼

所以我們得出了這個可怕的詞:硬編碼。這種反模式的本質是代碼與特定的硬件配置和/或系統環境緊密相關。這使將代碼移植到其他配置變得非常複雜。這種反模式與幻數密切相關(這些反模式通常交織在一起)。例子:

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;
}
很痛,不是嗎?在這裡我們硬編碼我們的連接設置。因此,該代碼只能與 MySQL 一起正常工作。要更改數據庫,我們需要深入研究代碼並手動更改所有內容。一個好的解決方案是將配置放在一個單獨的文件中:

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.船錨

在反模式的上下文中,船錨意味著在執行一些優化或重構後保留不再使用的系統部分。此外,代碼的某些部分可以保留“以備將來使用”,以防萬一您突然需要它們。本質上,這會將您的代碼變成垃圾箱。例子:

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());
}
我們有一個 update 方法,它使用一個單獨的方法將數據庫中的用戶數據與傳遞給該方法的用戶數據合併(如果傳遞給 update 方法的用戶具有空字段,則舊字段值從數據庫中獲取) . 那麼假設有一個新的需求,記錄不能和舊的合併,而是,即使有空字段,也用來覆蓋舊的:

public User update(Long id, User request) {
   return userDAO.update(user);
}
這意味著 mergeUser 不再使用,但刪除它會很可惜——如果有一天這個方法(或這個方法的想法)可能會派上用場呢?這樣的代碼只會使系統複雜化並引起混亂,基本上沒有實用價值。我們一定不要忘記,當您去另一個項目時,這種帶有“死塊”的代碼將很難傳遞給同事。處理船錨的最好方法是重構代碼,即刪除部分代碼(我知道這令人心碎)。此外,在製定開發計劃時,需要考慮到這些主播(以分配時間進行整理)。

7.對象污水池

要描述這種反模式,首先需要熟悉對像池模式。對像池(資源池)是一種創建型設計模式,一組已初始化且隨時可用的對象。當應用程序需要一個對象時,它會從這個池中取出而不是重新創建。當不再需要一個對象時,它不會被銷毀。相反,它會返回到池中。這種模式通常用於每次需要時都需要耗時創建的重對象,例如連接數據庫時。讓我們看一個小而簡單的例子。這是一個代表這種模式的類:

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;
   }
}
這裡我們有一個小對象,其中包含一個以設計模式名稱為鍵、相應的維基百科鏈接為值的地圖,以及訪問地圖的方法。讓我們看一下主要內容:

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);
   }
}
這裡的一切都很清楚:我們得到一個池對象,從池中得到一個包含資源的對象,從資源對像中得到映射,用它做一些事情,然後把所有這些放在池中它的位置以供進一步重用。瞧,這就是對像池設計模式。但是我們在談論反模式,對吧?讓我們在 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);
在這裡,我們再次獲得了一個 Resource 對象,我們獲得了它的模式圖,然後我們對該圖進行了一些操作。但是在將映射保存回對像池之前,它會被清除,然後用損壞的數據填充,使得 Resource 對像不適合重用。對像池的一個主要細節是,當對像被返回時,它必須恢復到適合進一步重用的狀態。如果返回到池中的對象仍然處於不正確或未定義的狀態,那麼我們的設計稱為對象污水池。存儲不適合重用的對像是否有意義?在這種情況下,我們可以在構造函數中使內部映射不可變:

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,更改地圖內容的嘗試和願望將逐漸消失。 反模式是開發人員由於嚴重缺乏時間、粗心、缺乏經驗或來自項目經理的壓力而經常遇到的陷阱。倉促行事很常見,可能會給以後的應用程序帶來很大的問題,因此您需要了解這些錯誤並提前避免它們。本文的第一部分到此結束。待續...