祝大家有美好的一天!有一天我接受採訪,有人問我一個關於反模式的問題:這是什麼樣的野獸,它們的類型和實踐中的例子是什麼。當然,我回答了這個問題,但是很膚淺,因為我沒有深入研究這個問題。採訪結束後,我開始上網搜索,越來越沉浸在這個主題中。今天我想對最受歡迎的反模式及其範例進行簡短回顧,閱讀這些內容可能會讓您對這個問題有必要的了解。讓我們開始吧!因此,在討論什麼是反模式之前,讓我們先記住什麼是模式。 模式是一種可重複的架構設計,用於解決設計應用程式時出現的常見問題或情況。但今天我們討論的不是它們,而是它們的對立面——反模式。 反模式 是解決一類常見問題的常用方法,這些問題無效、有風險或無成效。換句話說,它是一種錯誤模式(有時也稱為陷阱)。 一般來說,反模式分為以下幾種類型:
- 架構反模式- 設計系統結構時出現的架構反模式(通常由架構師設計)。
- 管理反模式- 管理領域的反模式,通常由各種管理者(或管理者群體)遇到。
- 發展反模式-反模式是普通程式設計師編寫系統時出現的開發問題。
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