祝大家有美好的一天!有一天我接受采访,有人问我一个关于反模式的问题:这是什么样的野兽,它们的类型和实践中的例子是什么。当然,我回答了这个问题,但是很肤浅,因为我没有深入研究这个问题。采访结束后,我开始上网搜索,越来越沉浸在这个话题中。今天我想对最流行的反模式及其示例进行简短回顾,阅读这些内容可能会让您对这个问题有必要的了解。让我们开始吧!因此,在讨论什么是反模式之前,让我们先记住什么是模式。 模式是一种可重复的架构设计,用于解决设计应用程序时出现的常见问题或情况。但今天我们讨论的不是它们,而是它们的对立面——反模式。 反模式 是解决一类常见问题的常用方法,这些问题无效、有风险或无成效。换句话说,它是一种错误模式(有时也称为陷阱)。 一般来说,反模式分为以下几种类型:
- 架构反模式- 设计系统结构时出现的架构反模式(通常由架构师设计)。
- 管理反模式- 管理领域的反模式,通常由各种管理者(或管理者群体)遇到。
- 开发反模式——反模式是普通程序员编写系统时出现的开发问题。
1. 分析瘫痪
分析瘫痪被认为是典型的组织反模式。它涉及在规划时过度分析情况,从而不采取任何决定或行动,从根本上使发展陷入瘫痪。当目标是达到完美并完全完成分析期时,通常会发生这种情况。这种反模式的特点是原地踏步(一种闭环)、修改和创建详细模型,这反过来又会干扰工作流程。例如,您尝试预测以下情况:如果用户突然想要根据员工姓名的第四个和第五个字母创建员工列表,包括他们在新年和新年期间投入最多工作时间的项目,该怎么办?前四年的三月八号?从本质上讲,这是一种过度的分析。现实生活中一个很好的例子是分析瘫痪如何导致柯达破产。以下是一些应对分析瘫痪的小技巧:- 你需要定义一个长期目标作为决策的灯塔,这样你做出的每一个决定都会让你更接近目标,而不是强迫你原地踏步。
- 不要把注意力集中在琐事上(为什么要在一个微小的细微差别上做出决定,就好像这是你生命中的最后一个决定一样?)
- 设定做出决定的最后期限。
- 不要试图完美地完成一项任务:最好把它做得很好。
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
带有业务逻辑的外观方法。这样一个神圣的物体变得巨大而笨拙,无法充分支撑。我们必须在每一段代码中修改它:系统中的许多节点都依赖它并与其紧密耦合。维护这样的代码变得越来越困难。在这种情况下,需要将其分为不同的类,每个类只有一个目的(目标)。在这个例子中,我们可以将其分解为一个 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.单例
单例是最简单的模式,它保证单线程应用程序中存在某个类的单个实例,并提供对该对象的全局访问点。你可以在这里读更多关于它的内容。但这是模式还是反模式? 我们来看看这个模板的缺点:-
全局状态。当我们访问类的实例时,我们不知道该类的当前状态,也不知道谁更改了它或何时更改,并且该状态可能不是我们所期望的。换句话说,使用单例的正确性取决于对其调用的顺序,这会导致子系统相互依赖,从而严重增加开发的复杂性。
-
Singleton 违反了 SOLID 原则之一——单一职责原则——Singleton 类除了执行其直接职责外,还控制其实例的数量。
-
常规类对单例的依赖关系在类接口中不可见。由于通常单例的实例不是传入方法的参数,而是直接获取,通过 来
getInstance()
识别类对单例的依赖关系,因此需要深入研究每个方法的实现——只需查看 public对象的契约是不够的。单例的存在通常降低了应用程序的可测试性,特别是使用单例的类的可测试性。首先,你不能用 Mock 对象代替单例,其次,如果单例有一个用于更改其状态的接口,那么测试将相互依赖。
换句话说,单例增加了连接性,而以上所有的一切无非是连接性增加的结果。
如果你仔细想想,使用单例是可以避免的。例如,为了控制对象实例的数量,很可能(并且有必要)使用各种工厂。
最大的危险在于试图基于单例来构建整个应用程序架构。这种方法有很多很好的替代方案。最重要的例子是 Spring,即它的 IoC 容器:在那里,控制服务创建的问题自然得到了解决,因为它们实际上是“类固醇工厂”。
现在关于这个主题有很多 holivar,所以由你来决定单例是模式还是反模式。
我们不会详细讨论它,而是转向今天的最后一个设计模式 - 吵闹鬼。
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 方法,用一个单独的方法将数据库中的用户和来更新的人的数据合并(如果来更新的人字段为空,则写为旧的)来自数据库)。例如,有一个要求,即使有空字段,记录也不应该与旧记录合并,而应该被覆盖:
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个副本;当这样的对象被获取时,它会从池中删除(如果不存在,则会创建它并立即放弃),最后有一个方法可以将对象放回原处。这些对象Resource
看起来像这样:
public class Resource {
private Map<String, String> patterns;
public Resource() {
patterns = new HashMap<>();
patterns.put("заместитель", "https://studfile.net/preview/3676297/page:3/");
patterns.put("мост", "https://studfile.net/preview/3676297/page:4/");
patterns.put("фасад", "https://studfile.net/preview/3676297/page:5/");
patterns.put("строитель", "https://studfile.net/preview/3676297/page:6/#16");
}
public Map<String, String> getPatterns() {
return patterns;
}
public void setPatterns(Map<String, String> patterns) {
this.patterns = patterns;
}
}
这里我们有一个小对象,其中包含一个映射,其中模式名称作为键,模式链接作为值,以及访问映射的方法。我们看看吧main
:
class SomeMain {
public static void main(String[] args) {
ReusablePool pool = ReusablePool.getInstance();
Resource firstResource = pool.acquireResource();
Map<String, String> firstPatterns = firstResource.getPatterns();
// ......Howим-то образом используем нашу мапу.....
pool.releaseResource(firstResource);
Resource secondResource = pool.acquireResource();
Map<String, String> secondPatterns = firstResource.getPatterns();
// ......Howим-то образом используем нашу мапу.....
pool.releaseResource(secondResource);
Resource thirdResource = pool.acquireResource();
Map<String, String> thirdPatterns = firstResource.getPatterns();
// ......Howим-то образом используем нашу мапу.....
pool.releaseResource(thirdResource);
}
}
这里的一切也很清楚:我们获取一个池对象,从中取出一个带有资源的对象,从中获取一个地图,用它做一些事情,然后将其全部放回池中以供进一步重用。瞧:这里有对象池模式。但我们谈论的是反模式,不是吗?我们来看看这个案例main
:
Resource fourthResource = pool.acquireResource();
Map<String, String> fourthPatterns = firstResource.getPatterns();
// ......Howим-то образом используем нашу мапу.....
fourthPatterns.clear();
firstPatterns.put("first","blablabla");
firstPatterns.put("second","blablabla");
firstPatterns.put("third","blablabla");
firstPatterns.put("fourth","blablabla");
pool.releaseResource(fourthResource);
在这里,再次获取资源对象,获取其带有模式的映射并对其执行某些操作,但在保存回对象池之前,映射会被清理并填充难以理解的数据,这使得该资源对象不适合重用。对象池的主要细微差别之一是,在返回对象后,必须将其返回到适合进一步重用的状态。如果对象在返回池后处于不正确或未定义的状态,则此构造称为对象污水池。存储不可重复使用的对象有什么意义?在这种情况下,您可以在构造函数中使内部映射不可变:
public Resource() {
patterns = new HashMap<>();
patterns.put("заместитель", "https://studfile.net/preview/3676297/page:3/");
patterns.put("мост", "https://studfile.net/preview/3676297/page:4/");
patterns.put("фасад", "https://studfile.net/preview/3676297/page:5/");
patterns.put("строитель", "https://studfile.net/preview/3676297/page:6/#16");
patterns = Collections.unmodifiableMap(patterns);
}
(更改内容的尝试和愿望将与 UnsupportedOperationException 一起失败)。 反模式是开发人员由于严重缺乏时间、注意力不集中、缺乏经验或管理者的踢脚而经常陷入的陷阱。通常缺乏时间和匆忙可能会给将来的应用带来很大的问题,因此需要提前了解并避免这些错误。 至此,文章第一部分就结束了:未完待续。
GO TO FULL VERSION