CodeGym /בלוג Java /Random-HE /מהן אנטי דפוסים? בואו נסתכל על כמה דוגמאות (חלק 1)
John Squirrels
רָמָה
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, הכוללת לוגיקה עסקית. חפץ אלוהים כזה הופך עצום ומסורבל לתחזוקה נכונה. אנחנו צריכים להתעסק עם זה בכל פיסת קוד. רכיבי מערכת רבים מסתמכים עליו ומקושרים אליו באופן הדוק. זה הופך להיות קשה יותר ויותר לשמור על קוד כזה. במקרים כאלה, יש לפצל את הקוד למחלקות נפרדות, שלכל אחת מהן תהיה מטרה אחת בלבד. בדוגמה זו, אנו יכולים לפצל את אובייקט האל למחלקה של דאו:
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(), אתה צריך להיכנס ליישום של כל שיטה על מנת לזהות את התלות של המחלקה בסינגלטון - רק להסתכל על הציבור של המחלקה חוזה לא מספיק.

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

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

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

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

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

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

4. פולטרגייסט

פולטרגייסט הוא אנטי-דפוס הכולל מחלקה חסרת טעם, המשמש לקריאת שיטות של מחלקה אחרת או פשוט מוסיף שכבה מיותרת של הפשטה . האנטי-דפוס הזה מתבטא כאובייקטים קצרי מועד, נטולי מדינה. אובייקטים אלה משמשים לעתים קרובות לאתחול אובייקטים אחרים, קבועים יותר.
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());
}
יש לנו שיטת עדכון שמשתמשת בשיטה נפרדת למיזוג נתוני משתמש ממסד הנתונים עם נתוני המשתמש המועברים לשיטה (אם למשתמש שעבר לשיטת העדכון יש שדה null, אז ערך השדה הישן נלקח ממסד הנתונים) . אז נניח שישנה דרישה חדשה שאסור למזג את הרשומות עם הישנות, אלא במקום זאת, גם אם יש שדות null, הם משמשים כדי להחליף את הישנים:
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);
   }
}
הכל כאן מספיק ברור: אנחנו מקבלים אובייקט מאגר, מקבלים אובייקט עם משאבים מהבריכה, מקבלים את המפה מ-Resource Object, עושים איתו משהו, ומכניסים את כל זה למקומו בבריכה לשימוש חוזר נוסף. וואלה, זהו דפוס עיצוב בריכת האובייקטים. אבל דיברנו על אנטי-דפוסים, נכון? הבה נבחן את המקרה הבא בשיטה העיקרית:
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);
כאן, שוב, אנחנו מקבלים אובייקט משאב, אנחנו מקבלים את מפת התבניות שלו, ואנחנו עושים משהו עם המפה. אבל לפני שמירת המפה בחזרה למאגר האובייקטים, היא מנוקה ואז מאוכלסת בנתונים פגומים, מה שהופך את אובייקט המשאב ללא מתאים לשימוש חוזר. אחד הפרטים העיקריים של מאגר אובייקטים הוא שכאשר חפץ מוחזר, עליו לשחזר למצב המתאים לשימוש חוזר נוסף. אם חפצים שהוחזרו לבריכה נשארים במצב שגוי או לא מוגדר, אז העיצוב שלנו נקרא בור שופכין לאובייקט. האם יש טעם לאחסן חפצים שאינם מתאימים לשימוש חוזר? במצב זה, נוכל להפוך את המפה הפנימית לבלתי ניתנת לשינוי בבנאי:
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