JavaRush /Java блогы /Random-KK /Антипаттерндер дегеніміз не? Мысалдарды қарастырайық (1 б...
Константин
Деңгей

Антипаттерндер дегеніміз не? Мысалдарды қарастырайық (1 бөлім)

Топта жарияланған
Антипаттерндер дегеніміз не?  Мысалдарды қарастырайық (1-бөлім) - 1Баршаңызға қайырлы күн! Күні кеше менімен сұхбат жүргізілді, маған антипаттерндер туралы сұрақ қойылды: бұл қандай аң, олардың түрлері және тәжірибеде мысалдары қандай. Әрине, мен бұл сұраққа өте үстірт жауап бердім, өйткені мен бұл мәселені зерттеуге тереңірек бармадым. Сұхбаттан кейін мен интернетті шарлай бастадым, мен бұл тақырыпқа көбірек еніп кеттім. Бүгін мен ең танымал антипаттерндерге және олардың мысалдарына қысқаша шолу жасағым келеді, бұл сізге осы мәселе бойынша қажетті білім беруі мүмкін. Бастайық! Сонымен, антипаттерннің не екенін талқыламас бұрын, үлгінің не екенін еске түсірейік. Үлгі – қолданбаны құрастыру кезінде туындайтын жалпы мәселелерді немесе жағдайларды шешуге арналған қайталанатын архитектуралық дизайн. Бірақ бүгін біз олар туралы емес, олардың қарама-қайшылықтары - антипаттерндер туралы айтып отырмыз. Анти-үлгі - бұл тиімсіз, тәуекелді немесе өнімсіз жиі кездесетін мәселелер класын шешудің жалпы тәсілі. Басқаша айтқанда, бұл қате үлгісі (кейде тұзақ деп те аталады). Антипаттерндер дегеніміз не?  Мысалдарды қарастырайық (1-бөлім) - 2Әдетте, антипаттерндер келесі түрлерге бөлінеді:
  1. Архитектуралық антипаттерндер - жүйенің құрылымын жобалау кезінде пайда болатын архитектуралық антипаттерндер (әдетте сәулетші арқылы).
  2. Басқару Anti Pattern – әдетте әртүрлі менеджерлер (немесе менеджерлер тобы) кездесетін басқару саласындағы антипаттерндер.
  3. Development Anti Pattern – антипаттерндер – бұл қарапайым бағдарламашылар жүйені жазғанда пайда болатын әзірлеу мәселелері.
Антипаттерндердің экзотикасы әлдеқайда кең, бірақ біз оларды бүгін қарастырмаймыз, өйткені қарапайым әзірлеушілер үшін бұл өте қиын болады. Алдымен, менеджмент саласындағы антипаттернді мысалға алайық.

1. Аналитикалық паралич

Талдаудың салдануы классикалық ұйымдық қарсы үлгі болып саналады. Ол ешқандай шешім немесе әрекет қабылданбауы үшін жоспарлау кезінде жағдайды талдауды қамтиды, бұл негізінен дамуды тоқтатады. Бұл көбінесе жетілдіруге жету және талдау кезеңін аяқтау мақсаты болған кезде болады. Бұл анти-үлгі шеңберлерде жүрумен (тұйық циклдің бір түрі), егжей-тегжейлі үлгілерді қайта қараумен және жасаумен сипатталады, бұл өз кезегінде жұмыс процесіне кедергі келтіреді. Мысалы, сіз келесідей нәрселерді болжауға тырысып жатырсыз: егер пайдаланушы кенеттен өз атының төртінші және бесінші әріптеріне негізделген қызметкерлер тізімін, соның ішінде Жаңа жыл мен Жаңа жыл арасындағы ең көп жұмыс уақытын арнаған жобаларды жасағысы келсе ше? өткен төрт жылдағы сегізінші наурыз ? Негізінде бұл талдаудың шамадан тыс көптігі. Жақсы өмірлік мысал - талдаудың салдануы Kodak компаниясын банкротқа әкелді . Талдаудың сал ауруымен күресу үшін бірнеше жылдам кеңестер:
  1. Ұзақ мерзімді мақсатты шешім қабылдаудың шамшырағы ретінде анықтау керек, сонда сіз қабылдаған әрбір шешім сізді мақсатыңызға жақындатады және уақытты белгілеуге мәжбүрлемейді.
  2. Ұсақ-түйек нәрселерге назар аудармаңыз (неге сіздің өміріңіздегі соңғы шешім сияқты болмашы нюанс туралы шешім қабылдауға болады?)
  3. Шешім қабылдау мерзімін белгілеңіз.
  4. Тапсырманы мүлтіксіз орындауға тырыспаңыз: оны өте жақсы орындаған дұрыс.
Біз тым тереңдемейміз және басқа басқару антипаттерндерін қарастырамыз. Сондықтан, кіріспесіз, кейбір архитектуралық антипаттерндерге көшейік, өйткені бұл мақаланы менеджерлер емес, болашақ әзірлеушілер оқитын шығар.

2. Құдай қарсы

Божественный an object - бұл әртүрлі деректердің үлкен көлемін (қолданба айналатын нысан) сақтай отырып, тым көп әртүрлі функциялардың шамадан тыс шоғырлануын сипаттайтын анти-үлгі. Шағын мысал келтірейік:
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бизнес логикасы бар қасбет әдісін көреміз. Мұндай құдайлық нысан үлкен және жеткілікті қолдау үшін ебедейсіз болады. Біз оны codeтың әрбір бөлігінде өңдеуіміз керек: жүйедегі көптеген түйіндер оған сүйенеді және онымен тығыз байланысты. Мұндай codeты сақтау барған сайын қиындай түсуде. Мұндай жағдайларда оны жеке сыныптарға бөлу қажет, олардың әрқайсысында бір ғана мақсат (мақсат) болады. Бұл мысалда оны дао сыныбына бөлуге болады:
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 қағидаларының бірін бұзады - Бірыңғай жауаптылық принципі - Singleton класы өзінің тікелей міндеттерін орындаумен қатар, оның даналарының санын да бақылайды.

  3. Класс интерфейсінде әдеттегі класстың синглтонға тәуелділігі көрінбейді. Әдетте синглтонның данасы әдіс параметрлерінде берілмейтіндіктен, getInstance()класстың синглтонға тәуелділігін анықтау үшін арқылы тікелей алынғандықтан, әрбір әдісті іске асыруға тереңірек үңілу керек - жай ғана жалпыға шолу. an objectінің келісімшарты жеткіліксіз.

    Синглонның болуы жалпы қолданбаның және синглтонды пайдаланатын сыныптардың тексерілу мүмкіндігін төмендетеді. Біріншіден, синглтонның орнына Mock нысанын қоюға болмайды, екіншіден, синглтонның күйін өзгертуге арналған интерфейсі болса, сынақтар бір-біріне тәуелді болады.

    Басқаша айтқанда, синглтон қосылымды арттырады және жоғарыда айтылғандардың барлығы қосылыстың жоғарылауының салдары болып табылады.

    Егер сіз бұл туралы ойласаңыз, синглтонды пайдаланудан аулақ болуға болады. Мысалы, нысанның даналарының санын бақылау үшін зауыттардың әртүрлі түрлерін пайдалану әбден мүмкін (және қажет).

    Ең үлкен қауіп синглтондарға негізделген қолданбалы архитектураны құру әрекетінде жатыр. Бұл тәсілге көптеген тамаша баламалар бар. Ең маңызды мысал - Spring, атап айтқанда оның IoC контейнерлері: бұл жерде қызметтерді құруды бақылау мәселесі табиғи түрде шешіледі, өйткені олар шын мәнінде «стероидтардағы зауыттар».

    Қазір бұл тақырыпта көп голивар бар, сондықтан синглтонның үлгі немесе антипаттерн екенін шешу сізге байланысты.

    Біз оған тоқталмаймыз және бүгінгі дизайнның соңғы үлгісіне көшеміз - полтергейст.

4. Полтергеист

Полтергеист - бұл басқа класс әдістерін шақыру үшін пайдаланылатын немесе қажетсіз абстракция қабатын қосатын пайдалы емес класс антипаттерні. Антипаттерн күйден айырылған қысқа мерзімді an objectілер түрінде көрінеді. Бұл нысандар басқа, неғұрлым берік нысандарды инициализациялау үшін жиі пайдаланылады.
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);
   }
}
Тек делдал болып, жұмысын басқа біреуге тапсыратын нысан бізге не үшін керек? Біз оны жоямыз және ол жүзеге асыратын шағын функционалдылықты ұзақ мерзімді нысандарға жылжытамыз. Әрі қарай, біз (қарапайым әзірлеушілер ретінде) бізді ең қызықтыратын үлгілерге көшеміз - Development antipatterns .

5. Қатты code

Осылайша біз осы қорқынышты сөзге жеттік - қатты code. Бұл антипаттерннің мәні codeтың нақты аппараттық конфигурацияға және/немесе жүйелік ортаға қатты байланысты болуы, бұл оны басқа конфигурацияларға порттауды өте қиындатады. Бұл антипаттерн сиқырлы сандармен тығыз байланысты (олар жиі бір-бірімен араласады). Мысалы:
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;
}
Шегеленген, солай емес пе? Мұнда біз қосылым конфигурациясын тікелей орнаттық, нәтижесінде code MySQL-те ғана дұрыс жұмыс істейді, ал дерекқорды өзгерту үшін codeқа кіріп, барлығын қолмен өзгерту керек. Конфигурацияларды бөлек файлға қою жақсы шешім болар еді:
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. Қайық якорьі

Антипаттерндер контекстіндегі қайық якорьі кейбір оңтайландырудан немесе рефакторингтен кейін қалған жүйенің пайдаланылмаған бөліктерін сақтауды білдіреді. Сондай-ақ, codeтың кейбір бөліктерін қайта пайдалану қажет болған жағдайда «болашаққа» қалдыруға болады. Бұл codeты қоқыс жәшігіне айналдырады. Антипаттерндер дегеніміз не?  Мысалдарды қарастырайық (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ол енді қолданылмайды және оны жою өкінішті: егер ол (немесе оның идеясы) әлі де пайдалы болса ше? Мұндай code тек жүйелерді қиындатады және шатастырады, шын мәнінде ешқандай практикалық мән бермейді. Басқа жобаға кеткенде, «өлі кесектері» бар мұндай codeты әріптесіңізге беру қиын болатынын ұмытпауымыз керек. Қайық якорьімен жұмыс істеудің ең жақсы әдісі - codeты рефакторинг, атап айтқанда, codeтың осы бөлімдерін жою (өкінішке орай). Сондай-ақ, дамуды жоспарлау кезінде сіз осындай якорьдердің пайда болуын ескеруіңіз керек (қалдықтарды тазалауға уақыт беріңіз).

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);
   }
}
Біз бұл классты жоғарыда сипатталған синглтон үлгісі/антипатерн түрінде ұсынамыз , яғни осы типтегі бір ғана an object болуы мүмкін, ол белгілі бір an objectілерде жұмыс істейді 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);
   }
}
Мұнда бәрі түсінікті: біз бассейн нысанын аламыз, одан ресурстары бар нысанды шығарамыз, одан карта аламыз, онымен бірдеңе жасаймыз және оны әрі қарай қайта пайдалану үшін пулға қайта саламыз. Voila: мұнда сізде нысан пулының үлгісі бар. Бірақ біз антипаттерндер туралы айттық, солай емес пе? Бұл жағдайды қарастырайық 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);
Мұнда тағы да ресурс нысаны алынады, оның өрнектері бар картасы алынады және онымен бірдеңе жасалады, бірақ қайтадан нысан пулына сақтамас бұрын карта тазаланады және түсініксіз деректермен толтырылады, бұл осы Ресурс нысанын қайта пайдалануға жарамсыз етеді. Нысан пулының негізгі нюанстарының бірі an object қайтарылғаннан кейін оны одан әрі қайта пайдалану үшін қолайлы күйге қайтару қажет. Егер нысандар бассейнге қайтарылғаннан кейін дұрыс емес немесе анықталмаған күйде болса, бұл құрылым an objectінің шұңқыры деп аталады. Қайта пайдалануға болмайтын нысандарды сақтаудың мәні неде? Бұл жағдайда конструкторда ішкі картаны өзгермейтін етіп жасауға болады:
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