JavaRush /בלוג Java /Random-HE /מהן אנטי דפוסים? בואו נסתכל על דוגמאות (חלק 1)

מהן אנטי דפוסים? בואו נסתכל על דוגמאות (חלק 1)

פורסם בקבוצה
מהן אנטי דפוסים?  בואו נסתכל על דוגמאות (חלק 1) - 1יום טוב לכולם! לפני כמה ימים התראיינתי, ונשאלתי שאלה על אנטי דפוסים: איזו מין חיה זו, מה הסוגים והדוגמאות שלהם בפועל. כמובן שעניתי על השאלה, אבל בצורה מאוד שטחית, שכן לא הלכתי לעומק בלימוד הנושא הזה. לאחר הראיון, התחלתי לשוטט באינטרנט, והשתקעתי יותר ויותר בנושא זה. היום אני רוצה לערוך סקירה קצרה של האנטי-דפוסים הפופולריים ביותר והדוגמאות שלהם, קריאה שעשויה לתת לך את הידע הדרוש בנושא זה. בואו נתחיל! אז, לפני שנדון במה זה אנטי-תבנית, בואו נזכור מהי דפוס. דפוס הוא עיצוב אדריכלי שניתן לחזור עליו לפתרון בעיות או מצבים נפוצים המתעוררים בעת תכנון יישום. אבל היום אנחנו לא מדברים עליהם, אלא על הניגודים שלהם - אנטי דפוסים. אנטי-דפוס היא גישה נפוצה לפתרון סוג של בעיות שנתקלות בהן בדרך כלל שאינה יעילה, מסוכנת או לא פרודוקטיבית. במילים אחרות, מדובר בדפוס שגיאה (נקרא לפעמים גם מלכודת). מהן אנטי דפוסים?  בואו נסתכל על דוגמאות (חלק 1) - 2ככלל, אנטי דפוסים מחולקים לסוגים הבאים:
  1. אנטי דפוסים אדריכליים - אנטי דפוסים אדריכליים שעולים בעת תכנון מבנה מערכת (בדרך כלל על ידי אדריכל).
  2. Management Anti Pattern - אנטי דפוסים בתחום הניהול, בהם נתקלים בדרך כלל מנהלים שונים (או קבוצות מנהלים).
  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עם היגיון עסקי. חפץ אלוהי כזה הופך לעצום ומגושם לתמיכה נאותה. אנחנו צריכים להתעסק עם זה בכל פיסת קוד: צמתים רבים במערכת מסתמכים עליו ומקושרים אליו באופן הדוק. שמירה על קוד כזה הופכת קשה יותר ויותר. במקרים כאלה, יש לחלק אותו למחלקות נפרדות, שלכל אחת מהן תהיה מטרה אחת (מטרה). בדוגמה זו, אתה יכול לחלק את זה למחלקת 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) - 3בואו נסתכל על החסרונות של תבנית זו:
  1. מצב גלובלי. כאשר אנו ניגשים למופע של מחלקה, איננו יודעים את המצב הנוכחי של אותה מחלקה או מי שינה אותה או מתי, וייתכן שהמצב הזה אינו מה שאנו מצפים. במילים אחרות, נכונות העבודה עם סינגלטון תלויה בסדר הקריאות אליו, מה שגורם לתתי המערכות להיות תלויות זו בזו וכתוצאה מכך מגביר באופן רציני את מורכבות הפיתוח.

  2. Singleton מפר את אחד מעקרונות SOLID – Single Responsibility Principle – מחלקת Singleton, בנוסף לביצוע האחריות המיידית שלה, שולטת גם במספר המופעים שלה.

  3. התלות של מחלקה רגילה בסינגלטון אינה נראית בממשק המחלקה. מכיוון שבדרך כלל מופע של יחיד לא מועבר בפרמטרים של מתודה, אלא מתקבל ישירות, דרך getInstance(), כדי לזהות את התלות של מחלקה בסינגלטון, צריך להתעמק ביישום של כל שיטה - פשוט צפייה בציבור חוזה החפץ אינו מספיק.

    הנוכחות של סינגלטון מפחיתה את יכולת הבדיקה של האפליקציה בכלל והמחלקות המשתמשות בסינגלטון בפרט. ראשית, אתה לא יכול לשים אובייקט Mock במקום יחיד, ושנית, אם ליחיד יש ממשק לשינוי מצבו, הבדיקות יהיו תלויות זו בזו.

    במילים אחרות, יחיד מגביר את הקישוריות, וכל האמור לעיל אינו אלא תוצאה של קישוריות מוגברת.

    ואם חושבים על זה, ניתן להימנע משימוש בסינגלטון. לדוגמה, כדי לשלוט במספר המופעים של אובייקט, בהחלט אפשרי (והכרחי) להשתמש במפעלים מסוגים שונים.

    הסכנה הגדולה ביותר טמונה בניסיון לבנות את כל ארכיטקטורת האפליקציה על בסיס יחידות. יש הרבה אלטרנטיבות נהדרות לגישה זו. הדוגמה החשובה ביותר היא Spring, כלומר מיכלי ה-IoC שלו: שם נפתרת בעיית השליטה ביצירת השירותים באופן טבעי, שכן הם, למעשה, "מפעלים לסטרואידים".

    עכשיו יש הרבה הוליבר בנושא הזה, אז זה תלוי בך להחליט אם סינגלטון הוא דפוס או אנטי-תבנית.

    ולא נתעכב על זה ונעבור לדפוס העיצוב האחרון להיום - פולטרגייסט.

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. עוגן סירה

עוגן סירה בהקשר של אנטי-דפוסים פירושו אחסון חלקים שאינם בשימוש של מערכת שנותרו לאחר אופטימיזציה או עיבוד מחדש. כמו כן, חלקים מסוימים של הקוד עשויים להישאר "לעתיד", למקרה שתצטרך להשתמש בהם שוב. זה בעצם הופך את הקוד לפח אשפה. מהן אנטי דפוסים?  בואו נסתכל על דוגמאות (חלק 1) - 4דוגמא:
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());
}
יש לנו שיטת עדכון שמשתמשת בשיטה נפרדת למיזוג הנתונים של המשתמש מהמסד ושל זה שבא לעדכון (אם למי שהגיע לעדכון יש שדה ריק אז הוא כתוב בתור הישן מתוך מסד הנתונים). ולדוגמה, הייתה דרישה שאין למזג רשומות עם ישנות, אלא לדרוס, גם אם יש שדות ריקים:
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);
   }
}
אנו מציגים את המחלקה הזו בצורה של תבנית יחידה /antipattern המתוארת לעיל , כלומר, יכול להיות רק אובייקט אחד מסוג זה, הוא פועל על אובייקטים מסוימים 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). אנטי דפוסים הם מלכודות שמפתחים נופלים אליהן לעתים קרובות עקב חוסר זמן חריף, חוסר תשומת לב, חוסר ניסיון או בעיטות של מנהלים. המחסור הרגיל בזמן והחיפזון עלולים לגרום לבעיות גדולות עבור האפליקציה בעתיד, ולכן יש להכיר את הטעויות הללו ולהימנע מראש. מהן אנטי דפוסים?  בואו נסתכל על דוגמאות (חלק 1) - 6בכך הגיע לסיומו החלק הראשון של המאמר: להמשך .
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION