CodeGym /Java 博客 /随机的 /什么是反模式?让我们看一些例子(第 1 部分)
John Squirrels
第 41 级
San Francisco

什么是反模式?让我们看一些例子(第 1 部分)

已在 随机的 群组中发布
大家好!前几天我有一个工作面试,我被问到一些关于反模式的问题:它们是什么,有哪些类型,以及有哪些实际例子。当然,我回答了这个问题,但非常肤浅,因为我以前没有深入探讨过这个话题。面试结束后,我开始上网刷题,越来越沉浸在话题中。 什么是反模式? 让我们看一些例子(第 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,更改地图内容的尝试和愿望将逐渐消失。 反模式是开发人员由于严重缺乏时间、粗心、缺乏经验或来自项目经理的压力而经常遇到的陷阱。仓促行事很常见,可能会给以后的应用程序带来很大的问题,因此您需要了解这些错误并提前避免它们。本文的第一部分到此结束。待续...
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION