CodeGym /Java Blogu /Rastgele /Anti-paternler nelerdir? Bazı örneklere bakalım (Bölüm 1)...
John Squirrels
Seviye
San Francisco

Anti-paternler nelerdir? Bazı örneklere bakalım (Bölüm 1)

grupta yayınlandı
Hepinize iyi günler! Geçen gün bir iş görüşmem vardı ve bana anti-kalıplar hakkında bazı sorular soruldu: bunlar ne, hangi türler var ve hangi pratik örnekler var? Elbette soruyu cevapladım, ancak daha önce bu konuya derinlemesine dalmadığım için çok yüzeysel olarak cevapladım. Röportajdan sonra interneti araştırmaya başladım ve kendimi konuya daha fazla kaptırdım. Anti-paternler nelerdir?  Bazı örneklere bakalım (Bölüm 1) - 1 Bugün en popüler anti-kalıplara kısa bir genel bakış sağlamak ve bazı örnekleri gözden geçirmek istiyorum. Umarım bunu okumak size bu alanda ihtiyacınız olan bilgiyi verecektir. Başlayalım! Anti-patern'in ne olduğunu tartışmadan önce, tasarım örüntüsünün ne olduğunu hatırlayalım. Bir tasarım desenibir uygulama tasarlanırken ortaya çıkan yaygın sorunlar veya durumlar için tekrarlanabilir bir mimari çözümdür. Ama bugün onlardan değil, karşıtlarından, yani anti-kalıplardan bahsediyoruz. Bir anti-patern, bir ortak sorun sınıfını çözmeye yönelik yaygın ancak etkisiz, riskli ve/veya verimsiz bir yaklaşımdır. Başka bir deyişle, bu bir hatalar modelidir (bazen tuzak olarak da adlandırılır). Kural olarak, anti-kalıplar aşağıdaki türlere ayrılır:
  1. Mimari anti-kalıplar — Bu anti-kalıplar, bir sistemin yapısı tasarlanırken (genellikle bir mimar tarafından) ortaya çıkar.
  2. Yönetim/organizasyonel anti-kalıplar — Bunlar, genellikle çeşitli yöneticilerin (veya yönetici gruplarının) karşılaştığı, proje yönetimindeki anti-kalıplardır.
  3. Geliştirme anti-kalıpları - Bu anti-kalıplar, bir sistem sıradan programcılar tarafından uygulandığında ortaya çıkar.
Anti-paternlerin tamamı çok daha egzotik, ancak bugün hepsini ele almayacağız. Sıradan geliştiriciler için bu çok fazla olurdu. Yeni başlayanlar için, örnek olarak bir yönetim anti-paternini ele alalım.

1. Analitik felç

Analiz felciklasik bir yönetim karşıtı model olarak kabul edilir. Planlama sırasında durumun aşırı analiz edilmesini içerir, böylece esasen geliştirme sürecini felce uğratan hiçbir karar veya eylem yapılmaz. Bu genellikle amaç mükemmelliğe ulaşmak ve analiz süresi boyunca kesinlikle her şeyi dikkate almak olduğunda olur. Bu anti-patern, daireler çizerek (sıradan bir kapalı döngü), gözden geçirerek ve ayrıntılı modeller oluşturarak karakterize edilir ve bu da iş akışına müdahale eder. Örneğin, bazı şeyleri bir düzeyde tahmin etmeye çalışıyorsunuz: ama ya bir kullanıcı aniden, en çok çalışma saatlerini harcadıkları projelerin listesi de dahil olmak üzere, adlarının dördüncü ve beşinci harflerine göre bir çalışanlar listesi oluşturmak isterse ne olur? Son dört yılda Yeni Yıl ve Dünya Kadınlar Günü arasında? Özünde, o' çok fazla analiz var. İşte analiz felciyle savaşmak için birkaç ipucu:
  1. Karar vermek için bir yol gösterici olarak uzun vadeli bir hedef tanımlamanız gerekir, böylece her kararınız sizi duraklatmak yerine hedefe daha da yaklaştırır.
  2. Önemsiz şeylere konsantre olmayın (önemsiz bir ayrıntı hakkında neden hayatınızın en önemli kararıymış gibi karar alasınız?)
  3. Karar için bir son tarih belirleyin.
  4. Bir görevi mükemmel bir şekilde tamamlamaya çalışmayın - onu çok iyi yapmak daha iyidir.
Burada çok derine inmeye gerek yok, bu yüzden diğer yönetimsel anti-kalıpları dikkate almayacağız. Bu nedenle, herhangi bir giriş yapmadan, bazı mimari anti-kalıplara geçeceğiz, çünkü bu makale büyük olasılıkla yöneticilerden ziyade geleceğin geliştiricileri tarafından okunacaktır.

2. Tanrı nesnesi

Bir Tanrı nesnesi , her türlü işlevin aşırı konsantrasyonunu ve büyük miktarlarda farklı verileri (uygulamanın etrafında döndüğü bir nesne) tanımlayan bir anti-kalıptır. Küçük bir örnek alın:

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();
   }
}
Burada her şeyi yapan devasa bir sınıf görüyoruz. Veritabanı sorgularının yanı sıra bazı verileri de içerir. İş mantığını içeren findAllWithoutPageEn cephe yöntemini de görüyoruz. Böyle bir Tanrı nesnesi muazzam hale gelir ve düzgün bir şekilde sürdürülmesi garipleşir. Her kod parçasında onunla uğraşmak zorundayız. Birçok sistem bileşeni ona güvenir ve sıkı sıkıya bağlıdır. Bu tür bir kodu korumak gittikçe zorlaşıyor. Bu gibi durumlarda, kod, her birinin yalnızca bir amacı olacak ayrı sınıflara bölünmelidir. Bu örnekte, Tanrı nesnesini bir Dao sınıfına ayırabiliriz:

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());
   }
  
                               ........
}
Verileri ve verilere erişmek için yöntemleri içeren bir sınıf:

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;
   }
                    ....
Ve iş mantığına sahip yöntemi bir servise taşımak daha uygun olacaktır:

private List<User> findAllWithoutPageEn(Long permissionId, Type type) {
   switch (type) {
       case USERS:
           return findAllEnUsers(permissionId);
       case CUSTOMERS:
           return findAllEnCustomers(permissionId);
       default:
           return findAllEn();
   }
}

3. Tek Kişilik

Bir singleton en basit modeldir. Tek iş parçacıklı bir uygulamada bir sınıfın tek bir örneğinin olmasını sağlar ve bu nesneye küresel bir erişim noktası sağlar. Ama bu bir model mi yoksa anti-desen mi? Bu modelin dezavantajlarına bakalım:
  1. Global durum Sınıfın örneğine eriştiğimizde, bu sınıfın mevcut durumunu bilmiyoruz. Kimin ne zaman değiştirdiğini bilmiyoruz. Devlet beklediğimiz gibi bir şey olmayabilir. Başka bir deyişle, bir singleton ile çalışmanın doğruluğu, ona erişim sırasına bağlıdır. Bu, alt sistemlerin birbirine bağımlı olduğu ve bunun sonucunda bir tasarımın ciddi anlamda daha karmaşık hale geldiği anlamına gelir.

  2. Bir singleton, SOLID ilkelerini - tek sorumluluk ilkesini ihlal eder: doğrudan görevlerine ek olarak, singleton sınıfı ayrıca örnek sayısını da kontrol eder.

  3. Sıradan bir sınıfın bir singleton'a bağımlılığı, sınıfın arayüzünde görünmez. Bir tekil örnek genellikle bir yöntem bağımsız değişkeni olarak iletilmediğinden, bunun yerine doğrudan getInstance() aracılığıyla elde edildiğinden, sınıfın tekil örnek üzerindeki bağımlılığını belirlemek için her bir yöntemin uygulanmasına girmeniz gerekir; sözleşme yeterli değil.

    Bir singleton'ın varlığı, uygulamanın bir bütün olarak ve özellikle singleton kullanan sınıfların test edilebilirliğini azaltır. Her şeyden önce, singleton'u sahte bir nesneyle değiştiremezsiniz. İkinci olarak, eğer bir singleton durumunu değiştirmek için bir arayüze sahipse, testler birbirine bağlı olacaktır.

    Başka bir deyişle, bir singleton eşleşmeyi artırır ve yukarıda bahsedilen her şey, artan eşleşmenin bir sonucundan başka bir şey değildir.

    Ve eğer düşünürseniz, bir singleton kullanmaktan kaçınabilirsiniz. Örneğin, bir nesnenin örnek sayısını kontrol etmek için çeşitli fabrika türlerini kullanmak oldukça mümkündür (ve aslında gereklidir).

    En büyük tehlike, tüm uygulama mimarisini tekil kodlara dayalı olarak oluşturma girişiminde yatmaktadır. Bu yaklaşımın tonlarca harika alternatifi var. En önemli örnek Spring'dir, yani onun IoC kapsayıcılarıdır: aslında "steroid fabrikaları" olduklarından, hizmetlerin oluşturulmasını kontrol etme sorununa doğal bir çözümdürler.

    Artık bu konuda sonu gelmeyen ve uzlaştırılamaz birçok tartışma yaşanıyor. Bir singleton'un model mi yoksa anti-desen mi olduğuna karar vermek size kalmış.

    Üzerinde durmayacağız. Bunun yerine, bugünün son tasarım modeline geçeceğiz - poltergeist.

4. Hortlak

Bir poltergeist, başka bir sınıfın yöntemlerini çağırmak için kullanılan anlamsız bir sınıfı içeren veya basitçe gereksiz bir soyutlama katmanı ekleyen bir anti-kalıptır. Bu anti-kalıp, kendisini durumdan yoksun, kısa ömürlü nesneler olarak gösterir. Bu nesneler genellikle diğer, daha kalıcı nesneleri başlatmak için kullanılır.

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);
   }
}
Neden sadece aracı olan ve işini başka birine devreden bir nesneye ihtiyacımız var? Onu ortadan kaldırıyoruz ve sahip olduğu küçük işlevselliği uzun ömürlü nesnelere aktarıyoruz. Ardından, (sıradan geliştiriciler olarak) bizi en çok ilgilendiren kalıplara, yani geliştirme anti-kalıplarına geçiyoruz .

5. Sabit kodlama

Böylece bu korkunç kelimeye ulaştık: zor kodlama. Bu anti-kalıpın özü, kodun belirli bir donanım yapılandırmasına ve/veya sistem ortamına güçlü bir şekilde bağlı olmasıdır. Bu, kodun diğer yapılandırmalara taşınmasını büyük ölçüde karmaşıklaştırır. Bu anti-kalıp, sihirli sayılarla yakından ilişkilidir (bu anti-kalıplar genellikle iç içe geçmiştir). Örnek:

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;
}
Acıyor, değil mi? Burada bağlantı ayarlarımızı kodluyoruz. Sonuç olarak, kod yalnızca MySQL ile doğru şekilde çalışacaktır. Veritabanını değiştirmek için, koda girmemiz ve her şeyi manuel olarak değiştirmemiz gerekecek. Yapılandırmayı ayrı bir dosyaya koymak iyi bir çözüm olabilir:

spring:
  datasource:
    jdbc-url:jdbc:mysql://localhost:3306/someDb?characterEncoding=UTF-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username:  user01
    password:  12345qwert
Başka bir seçenek de sabitleri kullanmaktır.

6. Tekne çapası

Anti-kalıplar bağlamında, bir tekne çapası , bazı optimizasyon veya yeniden düzenleme yapıldıktan sonra sistemin artık kullanılmayan parçalarını tutmak anlamına gelir. Ayrıca, aniden ihtiyaç duymanız durumunda kodun bazı bölümleri "ileride kullanılmak üzere" saklanabilir. Esasen bu, kodunuzu bir çöp kutusuna dönüştürür. Örnek:

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());
}
Veritabanındaki kullanıcı verilerini, yönteme iletilen kullanıcı verileriyle birleştirmek için ayrı bir yöntem kullanan bir güncelleme yöntemimiz var (güncelleme yöntemine geçen kullanıcının boş bir alanı varsa, o zaman eski alan değeri veritabanından alınır) . Ardından, kayıtların eskilerle birleştirilmemesi gerektiğine dair yeni bir gereksinim olduğunu varsayalım, bunun yerine, boş alanlar olsa bile, bunlar eskilerin üzerine yazmak için kullanılıyor:

public User update(Long id, User request) {
   return userDAO.update(user);
}
Bu, mergeUser'ın artık kullanılmadığı anlamına gelir, ancak onu silmek yazık olur - ya bu yöntem (veya bu yöntemin fikri) bir gün işe yararsa? Bu tür kodlar yalnızca sistemleri karmaşıklaştırır ve temelde hiçbir pratik değere sahip olmayan kafa karışıklığına neden olur. Başka bir proje için ayrıldığınızda, "ölü parçalar" içeren bu tür bir kodu bir meslektaşınıza aktarmanın zor olacağını unutmamalıyız. Tekne çapalarıyla baş etmenin en iyi yolu, kodu yeniden düzenlemek, yani kod bölümlerini silmektir (yürek burkan, biliyorum). Ek olarak, geliştirme programını hazırlarken bu tür çapaları hesaba katmak gerekir (toplama için zaman ayırmak için).

7. Nesne çöplüğü

Bu anti-patern'i tanımlamak için, önce nesne havuzu paternini tanımanız gerekir . Nesne havuzu (kaynak havuzu) , başlatılmış ve kullanıma hazır nesnelerden oluşan yaratıcı bir tasarım modelidir . Bir uygulama bir nesneye ihtiyaç duyduğunda, yeniden oluşturulmak yerine bu havuzdan alınır. Bir nesne artık gerekli olmadığında, yok edilmez. Bunun yerine havuza geri verilir. Bu model genellikle, bir veritabanına bağlanırken olduğu gibi, her ihtiyaç duyulduğunda oluşturulması zaman alan ağır nesneler için kullanılır. Küçük ve basit bir örneğe bakalım. İşte bu modeli temsil eden bir sınıf:

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);
   }
}
Bu sınıf, yukarıdaki tekil kalıp/anti-kalıp şeklinde sunulur , yani bu türden yalnızca bir nesne olabilir. Belirli nesneleri kullanır Resource. Yapıcı, varsayılan olarak havuzu 4 örnekle doldurur. Bir nesne elde ettiğinizde, havuzdan kaldırılır (mevcut nesne yoksa, bir tane oluşturulur ve hemen döndürülür). Ve sonunda, nesneyi geri koymak için bir yöntemimiz var. Kaynak nesneleri şöyle görünür:

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;
   }
}
Burada, anahtar olarak tasarım deseni adları ve değer olarak karşılık gelen Wikipedia bağlantılarının yanı sıra haritaya erişim yöntemleri içeren bir harita içeren küçük bir nesnemiz var. Ana konuya bir göz atalım:

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);
   }
}
Buradaki her şey yeterince açık: bir havuz nesnesi alıyoruz, havuzdan kaynakları olan bir nesne alıyoruz, Kaynak nesnesinden haritayı alıyoruz, onunla bir şeyler yapıyoruz ve tüm bunları daha sonra tekrar kullanmak üzere havuzdaki yerine koyuyoruz. Voila, bu nesne havuzu tasarım modelidir. Ama anti-kalıplardan bahsediyorduk, değil mi? Ana yöntemde aşağıdaki durumu ele alalım:

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);
Burada yine bir Kaynak nesnesi elde ediyoruz, onun kalıp haritasını alıyoruz ve harita ile bir şeyler yapıyoruz. Ancak haritayı nesneler havuzuna geri kaydetmeden önce temizlenir ve sonra bozuk verilerle doldurulur, bu da Kaynak nesnesini yeniden kullanım için uygun hale getirmez. Bir nesne havuzunun ana ayrıntılarından biri, bir nesne döndürüldüğünde, daha sonra yeniden kullanım için uygun bir duruma geri yüklenmesi gerektiğidir. Havuza döndürülen nesneler yanlış veya tanımlanmamış bir durumda kalırsa, tasarımımıza nesne fosseptik adı verilir. Yeniden kullanıma uygun olmayan nesneleri saklamanın bir anlamı var mı? Bu durumda, yapıcıda dahili haritayı sabit hale getirebiliriz:

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);
}
Oluşturacakları UnsupportedOperationException sayesinde, haritanın içeriğini değiştirme girişimleri ve isteği ortadan kalkacaktır. Anti-kalıplar, geliştiricilerin akut zaman eksikliği, dikkatsizlik, deneyimsizlik veya proje yöneticilerinden gelen baskı nedeniyle sıklıkla karşılaştığı tuzaklardır. Yaygın olan acele etme, gelecekte uygulama için büyük sorunlara yol açabilir, bu nedenle bu hataları önceden bilmeniz ve bunlardan kaçınmanız gerekir. Bu, makalenin ilk bölümünü sonlandırıyor. Devam edecek...
Yorumlar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION