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

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

已在 随机的 群组中发布
什么是反模式?让我们看一些例子(第 1 部分) 今天我们继续回顾最流行的反模式。如果你错过了第一部分,就在这里什么是反模式? 让我们看一些例子(第 2 部分)- 1因此,设计模式是最佳实践。换句话说,它们是解决特定问题的经过时间考验的好方法的例子。反过来,反模式是它们的对立面,从某种意义上说,它们是解决各种问题时的陷阱或错误的模式(邪恶模式)。让我们继续下一个软件开发反模式。

8.金锤

锤子是一种反模式,其定义是相信特定的解决方案是普遍适用的。例子:
  1. 在遇到问题并找到完美解决方案的模式后,程序员会尝试将这种模式无处不在,将其应用到当前和所有未来的项目中,而不是为特定情况寻找合适的解决方案。

  2. 一些开发人员曾经为特定情况创建了自己的缓存变体(因为没有其他合适的)。后来,在下一个不涉及特殊缓存逻辑的项目中,他们再次使用他们的变体,而不是使用现成的库(例如,Ehcache)。结果是一堆错误和不兼容,以及大量的时间浪费和焦躁不安。

    任何人都会爱上这种反模式。如果您是初学者,您可能不了解设计模式。这可能会导致您尝试以您掌握的一种方式解决所有问题。如果我们谈论的是专业人士,那么我们称之为专业变形或书呆子观点。您有自己喜欢的设计模式,您可以使用自己喜欢的而不是正确的设计模式,假设过去的良好契合可以保证将来产生相同的结果。

    这个陷阱会产生非常可悲的结果——从糟糕的、不稳定的、难以维护的实施到项目的彻底失败。正如没有包治百病的药丸一样,也没有一种适用于所有场合的设计模式。

9.过早的优化

过早的优化是一种反模式,其名称不言自明。
“程序员花费大量时间思考和担心代码中非关键的地方并试图优化它们,这只会对后续的调试和支持产生负面影响。我们通常应该在 97% 的情况下忘记优化。而且,过早的优化是万恶之源。也就是说,我们必须全力关注剩下的 3%。” — 唐纳德·克努特
例如,过早地向数据库添加索引。为什么那么糟糕?好吧,糟糕的是,索引存储为二叉树。因此,每次添加和删除新值时,都会重新计算树,这会消耗资源和时间。所以,只有在急需的时候(数据量大,查询时间过长)才加索引,而且只对最重要的字段(查询次数最多的字段)加索引。

10.意大利面条代码

意大利面条代码是一种反模式,由结构不良、混乱且难以理解的代码定义,包含各种分支,例如包装异常、条件和循环。以前,goto 运算符是这种反模式的主要盟友。Goto 语句实际上不再被使用,它愉快地消除了一些相关的困难和问题。

public boolean someDifficultMethod(List<String> XMLAttrList) {
           ...
   int prefix = stringPool.getPrefixForQName(elementType);
   int elementURI;
   try {
       if (prefix == -1) {
        ...
           if (elementURI != -1) {
               stringPool.setURIForQName(...);
           }
       } else {
        ...
           if (elementURI == -1) {
           ...
           }
       }
   } catch (Exception e) {
       return false;
   }
   if (attrIndex != -1) {
       int index = attrList.getFirstAttr(attrIndex);
       while (index != -1) {
           int attName = attrList.getAttrName(index);
           if (!stringPool.equalNames(...)){
           ...
               if (attPrefix != namespacesPrefix) {
                   if (attPrefix == -1) {
                    ...
                   } else {
                       if (uri == -1) {
                       ...
                       }
                       stringPool.setURIForQName(attName, uri);
                   ...
                   }
                   if (elementDepth >= 0) {
                   ...
                   }
                   elementDepth++;
                   if (elementDepth == fElementTypeStack.length) {
                   ...
                   }
               ...
                   return contentSpecType == fCHILDRENSymbol;
               }
           }
       }
   }
}
看起来很糟糕,不是吗?不幸的是,这是最常见的反模式 :( 即使是编写此类代码的人将来也无法理解它。其他看到代码的开发人员会想,“好吧,如果它有效,那么好吧 -最好不要去碰它”。往往,一个方法一开始很简单,很透明,但是随着新的需求的加入,这个方法逐渐背负着越来越多的条件语句,变成这样一个庞然大物。如果这样的方法出现,你需要重构它要么完全重构它,要么至少重构它最混乱的部分。通常,在安排项目时,会分配时间用于重构,例如,30% 的 sprint 时间用于重构和测试。当然,这是假设没有任何匆忙(但什么时候会发生)。在这里

11. 魔法数字

幻数是一种反模式,在这种模式中,程序中使用了各种常量,但没有对它们的目的或含义进行任何解释。也就是说,它们通常命名不当,或者在极端情况下,没有注释解释注释是什么或为什么。就像意大利面条代码一样,这是最常见的反模式之一。没有编写代码的人可能知道也可能不知道幻数或它们的工作原理(及时,作者本人将无法解释它们)。结果,更改或删除数字会导致代码神奇地停止一起工作。例如,3673. 为了对抗这种反模式,我建议进行代码审查。您的代码需要由未参与代码相关部分的开发人员查看。他们的眼睛会很新鲜,他们会有疑问:这是什么,你为什么要那样做?当然,您需要使用解释性名称或发表评论。

12.复制粘贴编程

复制粘贴编程是一种反模式,在这种模式下,不加思索地复制和粘贴其他人的代码,可能会导致意想不到的副作用。例如,复制和粘贴我们不完全理解的数学计算或复杂算法的方法。它可能适用于我们的特定情况,但在某些其他情况下可能会导致麻烦。假设我需要一种方法来确定数组中的最大数。在网上翻来覆去,找到了这个解决方案:

public static int max(int[] array) {
   int max = 0;
   for(int i = 0; i < array.length; i++) {
       if (Math.abs(array[i]) > max){
           max = array[i];
       }
   }
   return max;
}
我们得到一个包含数字 3、6、1、4 和 2 的数组,该方法返回 6。太好了,让我们保留它!但是后来我们得到一个由2.5、-7、2、3组成的数组,那么我们的结果就是-7。而这个结果是不行的。这里的问题是 Math.abs() 返回绝对值。忽视这一点会导致灾难,但仅限于某些情况。如果不深入了解解决方案,很多情况下是无法验证的。复制的代码也可能超出应用程序的内部结构,无论是在风格上还是在更基本的架构层面上。这样的代码将更难阅读和维护。当然,我们不能忘记直接复制别人的代码是一种特殊的剽窃行为。

13. 重新发明轮子

重新发明轮子是一种反模式,有时也称为重新发明方轮. 本质上,这个模板与上面考虑的复制粘贴反模式相反。在这种反模式中,开发人员针对已经存在解决方案的问题实施他或她自己的解决方案。有时这些现有的解决方案比程序员发明的要好。大多数情况下,这只会导致时间的浪费和生产力的降低:程序员可能根本找不到解决方案,或者可能找到的解决方案远非最佳。也就是说,我们不能排除创建独立解决方案的可能性,因为这样做是复制粘贴编程的直接途径。程序员应该以出现的特定编程任务为指导,以便胜任地解决它们,无论是使用现成的解决方案还是创建自定义解决方案。常常,使用这种反模式的原因很简单。结果是对(搜索)现成解决方案的浅层分析。 重新发明方轮是所考虑的反模式产生负面结果的情况。也就是说,该项目需要一个自定义解决方案,开发人员创建了它,但很糟糕。同时,一个好的选择已经存在并且其他人正在成功地使用它。底线:大量的时间被浪费了。首先,我们创造了一些不起作用的东西。然后我们尝试重构它,最后我们用已经存在的东西替换它。一个例子是在已经存在大量实现的情况下实现您自己的自定义缓存。无论你作为一名程序员多么有才华,你都应该记住,重新发明一个方轮至少是在浪费时间。而且,如您所知,时间是最宝贵的资源。

14.溜溜球问题

溜溜球问题是一种反模式,其中应用程序的结构由于过度碎片化而过于复杂(例如,过度细分的继承链)。当您需要了解继承层次结构又长又复杂的程序时,就会出现“溜溜球问题”,从而创建深度嵌套的方法调用。因此,程序员需要在许多不同的类和方法之间导航,以便检查程序的行为。这个反模式的名字来源于玩具的名字。作为例子,让我们看看下面的继承链:我们有一个 Technology 接口:

public interface Technology {
   void turnOn();
}
Transport接口继承了它:

public interface Transport extends Technology {
   boolean fillUp();
}
然后我们有另一个接口,GroundTransport:

public interface GroundTransportation extends Transport {
   void startMove();
   void brake();
}
从那里,我们派生了一个抽象的 Car 类:

public abstract class Car implements GroundTransportation {
   @Override
   public boolean fillUp() {
       /* some implementation */
       return true;
   }
   @Override
   public void turnOn() {
       /* some implementation */
   }
   public boolean openTheDoor() {
       /* some implementation */
       return true;
   }
   public abstract void fixCar();
}
接下来是抽象的 Volkswagen 类:

public abstract class Volkswagen extends Car {
   @Override
   public void startMove() {
       /* some implementation */
   }
   @Override
   public void brake() {
       /* some implementation */
   }
}
最后,一个特定的模型:

public class VolkswagenAmarok extends Volkswagen {
   @Override
   public void fixCar(){
       /* some implementation */
   }
}
这个链条迫使我们寻找以下问题的答案:
  1. 有几种方法VolkswagenAmarok

  2. 为了实现最大抽象,应该插入什么类型而不是问号:

    
    ? someObj = new VolkswagenAmarok();
           someObj.brake();
    
这类问题很难快速回答——需要我们去观察和调查,而且很容易混淆。如果层次结构更大、更长、更复杂,并且有各种重载和覆盖怎么办?由于过度碎片化,我们本来的结构会变得模糊。最好的解决办法是减少不必要的分裂。在我们的例子中,我们将离开技术 → 汽车 → VolkswagenAmarok。

15.偶然的复杂性

不必要的复杂性是一种反模式,其中将不必要的复杂性引入到解决方案中。
“任何傻瓜都可以编写计算机可以理解的代码。优秀的程序员可以编写人类可以理解的代码。” — 马丁·福勒
那么什么是复杂性?它可以定义为程序中执行每个操作的难易程度。通常,复杂性可以分为两种类型。第一种复杂性是系统具有的功能数量。它只能通过一种方式减少——通过删除一些功能。需要监测现有方法。如果一个方法不再使用或仍在使用但没有带来任何价值,则应将其删除。更重要的是,您需要评估应用程序中所有方法的使用方式,以便了解哪些投资是值得的(大量代码重用)以及您可以拒绝的地方。第二种复杂性是不必要的复杂性。它只能通过专业方法治愈。而不是做一些“酷”的事情 (年轻的开发人员并不是唯一易患这种疾病的人),你需要考虑如何尽可能简单地做到这一点,因为最好的解决方案总是简单的。例如,假设我们有一些小的相关表,其中包含一些实体的描述,例如用户: 什么是反模式? 让我们看一些例子(第 2 部分)- 3因此,我们有用户的 ID、进行描述的语言的 ID 以及描述本身。同样,我们有汽车、文件、计划和客户表的辅助描述符。那么将新值插入此类表会是什么样子呢?

public void createDescriptionForElement(ServiceType type, Long languageId, Long serviceId, String description)throws Exception {
   switch (type){
       case CAR:
           jdbcTemplate.update(CREATE_RELATION_WITH_CAR, languageId, serviceId, description);
       case USER:
           jdbcTemplate.update(CREATE_RELATION_WITH_USER, languageId, serviceId, description);
       case FILE:
           jdbcTemplate.update(CREATE_RELATION_WITH_FILE, languageId, serviceId, description);
       case PLAN:
           jdbcTemplate.update(CREATE_RELATION_WITH_PLAN, languageId, serviceId, description);
       case CUSTOMER:
           jdbcTemplate.update(CREATE_RELATION_WITH_CUSTOMER, languageId, serviceId, description);
       default:
           throw new Exception();
   }
}
因此,我们有这个枚举:

public enum ServiceType {
   CAR(),
   USER(),
   FILE(),
   PLAN(),
   CUSTOMER()
}
一切看起来都很简单很好……但是其他的方法呢?事实上,它们也会有一堆switch语句和一堆几乎相同的数据库查询,这反过来又会使我们的类变得非常复杂和膨胀。如何让这一切变得更容易?让我们稍微升级一下枚举:

@Getter
@AllArgsConstructor
public enum ServiceType {
   CAR("cars_descriptions", "car_id"),
   USER("users_descriptions", "user_id"),
   FILE("files_descriptions", "file_id"),
   PLAN("plans_descriptions", "plan_id"),
   CUSTOMER("customers_descriptions", "customer_id");
   private String tableName;
   private String columnName;
}
现在每种类型都有其表的原始字段的名称。因此,创建描述的方法变为:

private static final String CREATE_RELATION_WITH_SERVICE = "INSERT INTO %s(language_id, %s, description) VALUES (?, ?, ?)";
public void createDescriptionForElement(ServiceType type, Long languageId, Long serviceId, String description) {
   jdbcTemplate.update(String.format(CREATE_RELATION_WITH_SERVICE, type.getTableName(), type.getColumnName()), languageId, serviceId, description);
   }
方便、简单、紧凑,您不觉得吗?一个好的开发人员的标志甚至不是他或她使用模式的频率,而是他或她避免反模式的频率。无知是最大的敌人,因为你需要通过视觉了解你的敌人。好吧,这就是我今天的全部内容。谢谢大家!:)
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION