OO dışı kod üslerinin büyüklüğü yönetiliyor mu?


27

Her zaman soyutlamanın OO'nun kod tabanını yönetmek için sağladığı çok kullanışlı bir özellik olduğunu görüyorum. Fakat büyük OO dışı kod tabanları nasıl yönetiliyor? Yoksa bunlar sonunda " Büyük Çamur Topu " olur mu?

Güncelleme:
Herkesin soyutlamanın sadece modülerleşme veya veri gizleme olduğunu düşünüyor gibiydi. Ancak IMHO, aynı zamanda bağımlılık enjeksiyon ve dolayısıyla test için zorunlu olan 'Soyut Sınıflar' veya 'Arayüzler' kullanımı anlamına gelir. OO dışı kod tabanları bunu nasıl yönetir? Ve ayrıca, soyutlama dışında, kapsülleme ayrıca, veriler ve işlevler arasındaki ilişkiyi tanımlarken ve kısıtlarken büyük kod tabanlarını yönetmede çok yardımcı olur.

C ile, sahte OO kodu yazmak çok mümkün. Diğer OO olmayan diller hakkında fazla bir şey bilmiyorum. Peki, büyük C kod tabanlarını yönetmenin yolu bu mu?


6
Dilin agnostik bir şekilde, lütfen bir nesneyi tanımlayın. Bu nedir, nasıl değiştirilir, neleri miras almalı ve ne sağlamalıdır? Linux çekirdeği, birçok yardımcı ve işlev işaretçisi bulunan tahsisli yapılarla doludur , ancak bu muhtemelen çoğu için yönlendirilen nesnenin tanımını tatmin etmeyecektir. Yine de, bakımlı bir kod tabanının en iyi örneklerinden biridir. Niye ya? Çünkü her alt sistem sorumlusu sorumluluk alanında ne olduğunu bilir.
Tim Post

Dil-onaylı bir şekilde, kod tabanlarının yönetildiğini nasıl gördüğünüzü ve OO'nun bununla ne yapması gerektiğini açıklayın.
David Thornley,

@Tim Post Linux çekirdek kaynak kodu yönetimi ile ilgileniyorum. Lütfen sistemi daha fazla tarif eder misiniz? Belki bir örnek ile bir cevap olarak?
Gulshan

7
Eski günlerde, birim testi için alay ve taslaklar için ayrı bağlantılar kullandık . Bağımlılık Enjeksiyonu, birkaç tanesi arasında sadece bir tekniktir. Koşullu derleme başka bir şeydir.
Macneil

Bence "yönetilen" olarak adlandırılan büyük kod tabanlarına (OO veya başka şekilde) başvurmak çok zor. Sorunuzdaki merkezi terimin daha iyi tanımlanması iyi olur.
tottinge

Yanıtlar:


43

OOP'un soyutlamayı başarmanın tek yolu olduğunu düşünüyor gibi görünüyorsunuz.

OOP bunu yaparken kesinlikle çok iyi olsa da, hiçbir şekilde tek yolu yok. Büyük projeler aynı zamanda taviz vermeyen modülerleşmeyle yönetilebilir (her ikisi de mükemmel olan Perl veya Python'a bakın ve ML ve Haskell gibi işlevsel diller de bakın) ve şablonlar (C ++) gibi mekanizmalar kullanarak da yönetilebilir.


27
+1 Ayrıca, ne yaptığınızı bilmiyorsanız OOP kullanarak "Büyük Çamur Topu" yazmak da mümkündür.
Larry Coleman

Peki ya C kod tabanları?
Gulshan

6
@Gulshan: Birçok büyük C kodu tabanı OOP'dir. C'nin sınıfları olmaması, OOP'un biraz çaba ile elde edilemeyeceği anlamına gelmez. Ayrıca, C başlıkları ve PIMPL deyimini kullanarak iyi bir modülerleştirme sağlar. Modern dillerdeki modüller kadar rahat veya güçlü değil, bir kez daha yeterince iyi.
Konrad Rudolph

9
C dosya seviyesinde modülerleşmeye izin verir. Arabirim .h dosyasına gider, .c dosyasında herkese açık olan işlevler ve özel değişkenler ve işlevler staticerişim değiştiriciyi iliştirir.
David Thornley,

1
@Konrad: OOP'un bunu yapmanın tek yolu olmadığına katılıyorum, ancak OP'nin muhtemelen C'nin işlevsel olmadığını ya da dinamik bir dil olmadığını akılda tuttuğunu düşünüyorum. Bu yüzden Perl ve Haskell'den bahsetmenin onun için bir faydası olacağından şüpheliyim. Aslında yorumunuzu OP için daha alakalı ve yararlı buluyorum ( bu, OOP'un biraz çaba ile gerçekleştirilemeyeceği anlamına gelmez ); ek kodlarla ayrı bir cevap olarak eklemeyi düşünebilirsiniz, belki bir kod pasajı ya da birkaç bağlantıyla desteklenir. En azından benim oyumu ve muhtemelen OP'leri kazanacaktı. :)
Groo

11

Modüller, (harici / dahili) fonksiyonlar, alt yordamlar ...

Konrad'ın dediği gibi, büyük kod tabanlarını yönetmenin tek yolu OOP değildir. Nitekim bundan önce (C ++ * 'dan önce) oldukça fazla sayıda yazılım yazılmıştır.


* Ve evet, C ++ 'ın OOP'yi destekleyen tek kişi olmadığını biliyorum, ancak bir şekilde bu yaklaşım atalet almaya başladı.
Rook


6

Gerçekçi olmayan ya da sık sık değişmeyen (Sosyal Güvenlik emeklilik hesaplamaları düşünün) ve / veya derin kökleşmiş bilgiler, çünkü sistem gibi bakımları uzun süredir yapıyorlar (alaycı iş, iş güvenliğidir).

Daha iyi çözümler, otomatik test (örneğin birim testi) ve öngörülen adımları (örneğin, regresyon testi) izleyen insan testi, "tıklamak ve neyin kırıldığını görmek yerine" demek istediğim, tekrarlanabilir doğrulamadır.

Mevcut bir kod temeli ile bir tür otomatik teste geçmeye başlamak için Michael Feather'ın Eski Kod ile Etkili Bir Şekilde Çalışmasını okumanızı öneririm ; bu, mevcut kod tabanlarını bir tür tekrarlanabilir test çerçevesi OO'suna kadar getirme yaklaşımlarını ayrıntılarıyla açıklar. Bu, modülerleştirme gibi başkalarının yanıtladığı fikirlerin ortaya çıkmasına neden olur, ancak kitap, bir şeyleri bozmazken buna doğru bir yaklaşım tarif eder.


Michael Feather’ın kitabı için + 1. Büyük, çirkin bir kod tabanı hakkında depresyonda olduğunuzda, (tekrar) okuyun :)
Matthieu

5

Arayüzlere veya soyut sınıflara dayanan bağımlılık enjeksiyonunun test yapmanın çok güzel bir yolu olmasına rağmen, bu gerekli değildir. Neredeyse herhangi bir dilin, bir arabirim veya soyut bir sınıfla yapabileceğiniz her şeyi yapabilen bir işlev işaretçisi veya bir değerlendirmesi olduğunu unutmayın (sorun, birçok kötü şey dahil olmak üzere daha fazlasını yapabilmeleri ve kullanmamalarıdır). t kendi içinde meta veri sağlar). Böyle bir program aslında bu mekanizmalarla bağımlılık enjeksiyonunu başarabilir.

Çok yararlı olması için meta verilerle titiz olduğumu tespit ettim. OO dillerinde, kod bitleri arasındaki ilişkiler (bir dereceye kadar) sınıf yapısına göre, bir yansıma API'si gibi şeylere sahip olacak şekilde standartlaştırılmış şekilde tanımlanır. Usul dillerinde bunları kendiniz icat etmek yardımcı olabilir.

Ayrıca, kod oluşturmanın bir yordamsal dilde (nesneye yönelik bir dile kıyasla) çok daha faydalı olduğunu da gördüm. Bu, meta-verinin kodla eşzamanlı olduğunu (onu üretmek için kullanıldığından) garanti eder ve size en boyuna yönelik programlamanın kesme noktalarına benzer bir şey verir - ihtiyacınız olduğunda kodu enjekte edebileceğiniz bir yer. Bazen DRY programlama yapabildiğim böyle bir ortamda bunu yapmanın tek yolu budur.


3

Aslında, son zamanlarda keşfettiğiniz gibi , birinci dereceden fonksiyonlar, bağımlılık inversiyonu için ihtiyacınız olan her şeydir.

C birinci dereceden fonksiyonları destekler ve hatta bir dereceye kadar kapanır . Ve C makroları, gerekli özenle kullanılırsa genel programlama için güçlü bir özelliktir.

Hepsi orada. SGLIB , C'nin tekrar kullanılabilir kodları yazmak için nasıl kullanılabileceğine güzel bir örnektir. Ve orada daha çok olduğuna inanıyorum.


2

Soyutlama olmasa da çoğu program bir tür bölüme ayrılmıştır. Bu bölümler genellikle belirli görevler veya faaliyetlerle ilgilidir ve bunlar üzerinde soyutlanmış programların en spesifik bitlerinde çalıştığınız gibi çalışırsınız.

Küçük ve orta büyüklükteki projelerde bunu bazen daha saf bir OO uygulamasıyla yapmak daha kolaydır.


2

Soyutlama, soyut sınıflar, bağımlılık enjeksiyonu, kapsülleme, arayüzler ve diğerleri, büyük kod tabanlarını kontrol etmenin tek yolu değildir; bu sadece ve nesne yönelimli bir yoldur.

Asıl sır, OOP olmayanları kodlarken OOP düşünmekten kaçınmaktır.

Modülerlik, OO dışı dillerde anahtardır. C'de bu, tıpkı David Thornley'nin az önce söylediği gibi başarılır:

Arabirim .h dosyasına gider, .c dosyasında genel olarak kullanılabilir işlevler, özel değişkenler ve işlevler de statik erişim değiştiriciyi ekler.


1

Kodu yönetmenin bir yolu, onu MVC (model-görünüm denetleyicisi) mimarisinin hatları boyunca aşağıdaki kod türlerine ayırmaktır.

  • Giriş işleyicileri - Bu kod, fare, klavye, ağ bağlantı noktası gibi giriş aygıtlarıyla veya sistem olayları gibi üst düzey soyutlamalar ile ilgilidir.
  • Çıktı işleyicileri - Bu kod, monitörler, ışıklar, ağ bağlantı noktaları vb. Gibi harici cihazları yönetmek için veri kullanımıyla ilgilidir.
  • Modeller - Bu kod, kalıcı verilerinizin yapısını bildirmeyi, kalıcı verileri doğrulama kurallarını ve kalıcı verileri diske (veya diğer kalıcı veri cihazına) kaydetmeyi içerir.
  • Görünümler - Bu kod, web tarayıcıları (HTML / CSS), GUI, komut satırı, iletişim protokolü veri biçimleri (örn. JSON, XML, ASN.1, vb.) Gibi çeşitli görüntüleme yöntemlerinin gereksinimlerini karşılamak için verileri biçimlendirme ile ilgilidir.
  • Algoritmalar - Bu kod tekrar tekrar bir girdi verisini bir çıktı verisine mümkün olan en hızlı şekilde dönüştürür.
  • Kontrolörler - Bu kod giriş işleyicileri vasıtasıyla girişleri alır, algoritmalar kullanarak girişleri ayrıştırır ve ardından girişleri kalıcı verilerle birleştirerek veya sadece girişleri dönüştürerek ve isteğe bağlı olarak dönüştürülmüş verileri model aracılığıyla kalıcı olarak kaydederek verileri diğer algoritmalarla dönüştürür yazılımı ve isteğe bağlı olarak verileri bir çıktı aygıtına dönüştürmek için görüntüleme yazılımı yoluyla dönüştürme.

Bu kod düzenleme yöntemi, herhangi bir OO veya OO olmayan dilde yazılmış yazılımlar için iyi çalışır, çünkü ortak tasarım desenleri genellikle her bir alanda ortaktır. Ayrıca, bu tür kod sınırları genellikle algoritmalar hariç en gevşek şekilde birleştirilir, çünkü bunlar veri formatlarını girdilerden modele ve sonra da çıktılara bağlarlar.

Sistem gelişmeleri genellikle yazılımınızın daha fazla giriş türünü veya daha fazla çıkış türünü işlemesini sağlamak biçimindedir, ancak modeller ve görünümler aynıdır ve kontrolörler çok benzer şekilde davranır. Veya bir sistemin, girişler, modeller, algoritmalar aynı ve kontrolörler ve görünümler benzer olmasına rağmen, daha fazla ve daha farklı çıkış türlerini desteklemesi gerekebilir. Veya aynı girdiler, benzer çıktılar ve benzer görünümler için yeni modeller ve algoritmalar eklemek üzere bir sistem artırılabilir.

OO programlamanın kod organizasyonunu zorlaştırmasının bir yolu, bazı sınıfların kalıcı veri yapılarına derinlemesine bağlı olması ve bazılarının değil. Kalıcı veri yapıları basamaklı 1: N ilişkileri veya m: n ilişkileri gibi şeylerle yakından ilişkiliyse, haklı olduğunu bilmeden önce sisteminizin önemli ve anlamlı bir bölümünü kodlayana kadar sınıf sınırlarına karar vermek çok zordur. . Kalıcı veri yapılarına bağlı herhangi bir sınıf, kalıcı verinin şeması değiştiğinde değişmesi zor olacaktır. Algoritmalar, formatlama ve ayrıştırma işlemlerini yapan sınıfların, kalıcı veri yapılarının şemalarındaki değişikliklere karşı daha az hassas olmaları olasıdır. Bir MVC tür kod organizasyonu kullanmak, en kötü kod değişikliklerini model kodunda daha iyi izole eder.


0

Dahili yapı ve organizasyon özelliklerinden yoksun (örneğin ad alanları, paketler, montajlar vb.) Olmayan dillerde çalışırken veya bunların bu boyutta bir kod tabanını kontrol altında tutmak için yetersiz kalmaları durumunda, doğal cevap Kodu düzenlemek için kendi stratejilerimiz.

Bu organizasyon stratejisi muhtemelen farklı dosyaların nerede tutulması gerektiği, belirli işlemlerden önce / sonra yapılması gerekenler ve konvansiyonların ve diğer kodlama standartlarının isimlendirilmesi ile ilgili standartların yanı sıra çok fazla "bu şekilde nasıl yapıldığını da içerir. - onunla uğraşma! " yorum yazın - nedenini açıkladıkları sürece geçerlidir!

Stratejinin büyük olasılıkla projenin özel ihtiyaçlarına (insanlar, teknolojiler, çevre vb.) Göre uyarlanacağı için, büyük kod tabanlarını yönetmek için tek bedene uygun bir çözüm vermek zordur.

Bu nedenle, en iyi tavsiyenin projeye özgü stratejiyi benimsemek ve onu yönetmeyi kilit bir öncelik haline getirmek olduğuna inanıyorum: yapıyı, neden böyle olduğunu, değişiklik yapma süreçlerini belgelemek, uyulduğundan emin olmak için denetlemek, ve en önemlisi: değişmesi gerektiğinde değiştirin.

Çoğunlukla yeniden sınıflandırma sınıflarını ve yöntemlerini biliyoruz, ancak böyle bir dilde büyük bir kod temeli ile gerektiğinde ve gerektiğinde yeniden yapılandırılması gereken organizasyon stratejisinin kendisi (dokümantasyonla birlikte).

Akıl yürütme, yeniden yapılanma ile aynıdır: genel organizasyonunun bir karmaşa olduğunu düşünüyorsanız ve sonunda bozulmasına izin verecekseniz, sistemin küçük parçaları üzerinde çalışmaya yönelik zihinsel bir blok geliştirirsiniz (en azından benim işim budur) o).

Uyarılar da aynıdır: regresyon testi kullanın, yeniden yapılandırma yanlış giderse kolayca geri döndüğünüzden emin olun ve ilk başta yeniden düzenlemeyi kolaylaştırmak için tasarlayın (veya sadece yapmazsınız!).

Bunun doğrudan kodu yeniden düzenlemekten çok daha zor olduğunu ve bunun neden yapılması gerektiğini anlamayacak olan yöneticilerden / müşterilerden zamanı doğrulamak / gizlemenin daha zor olduğunu kabul ediyorum, ancak bunlar aynı zamanda yazılım çürümesine en yatkın proje türleri. esnek olmayan üst düzey tasarımların neden olduğu ...


0

Büyük bir kod tabanının yönetimini soruyorsanız, kod tabanınızı nispeten kaba bir seviyede (kütüphaneler / modüller / alt sistemler oluşturma / ad alanlarını kullanma / doğru belgelerin doğru yerlerde bulundurma) nasıl iyi bir şekilde yapılandırılmış tutmanız gerektiğini soruyorsunuz. vb.). OO ilkeleri, özellikle de 'soyut sınıflar' veya 'arayüzler', kodunuzu dahili olarak çok detaylı bir düzeyde temiz tutmak için prensiplerdir. Bu nedenle, büyük bir kod tabanını yönetilebilir tutma teknikleri, OO veya OO kodu olmayanlar için farklı değildir.


0

Nasıl kullanıldığı, kullandığınız öğelerin sınırlarını bulmanızdır. Örneğin, C ++ 'da bulunan aşağıdaki öğelerin net bir sınırı vardır ve sınır dışındaki bağımlılıklar dikkatlice düşünülmelidir:

  1. serbest işlev
  2. üye işlevi
  3. sınıf
  4. nesne
  5. arayüzey
  6. ifade
  7. yapıcı çağrı / nesne oluşturma
  8. işlev çağrısı
  9. şablon parametre türü

Bu öğeleri birleştirerek ve sınırlarını yeniden düzenleyerek, c ++ içinde istediğiniz hemen hemen her programlama stilini oluşturabilirsiniz.

Bunun bir işlevi olduğu için, bir işlevden diğer işlevleri çağırmanın kötü olduğunu yeniden tanımlamak gerekir, çünkü bağımlılığa neden olur, bunun yerine, yalnızca orijinal işlev parametrelerinin üye işlevlerini çağırmalısınız.


-1

En büyük teknik zorluk isim alanı problemidir. Bu soruna geçici bir çözüm bulmak için kısmi bağlantı kullanılabilir. Daha iyi yaklaşım, kodlama standartlarını kullanarak tasarım yapmaktır. Aksi takdirde, tüm semboller karışıklık haline gelir.


-2

Emacs buna iyi bir örnektir:

Emacs Mimarisi

Emacs Bileşenleri

Emacs Lisp testlerinde özellik algılama ve test fikstürlerinin kullanımı skip-unlessve let-bindyapılması:

Bazen, eksik ön koşullar nedeniyle bir test yapmanın anlamı yoktur. Gerekli bir Emacs özelliği derlenmemiş olabilir, test edilecek fonksiyon test makinesinde bulunmayabilecek harici bir ikiliyi çağırabilir. Bu durumda, makro skip-unlesstesti atlamak için kullanılabilir:

 (ert-deftest test-dbus ()
   "A test that checks D-BUS functionality."
   (skip-unless (featurep 'dbusbind))
   ...)

Bir test yapmanın sonucu, ortamın mevcut durumuna bağlı olmamalı ve her bir test ortamını içinde bulunduğu aynı durumda bırakmalıdır. Özellikle, bir test Emacs kişiselleştirme değişkenlerine veya kancalarına bağlı olmamalıdır. Emac’ın durumuna veya Emac’ın dışına (dosya sistemi gibi) harici olarak herhangi bir değişiklik yapması gerekiyorsa, geçip geçmemesine bakılmaksızın geri dönmeden önce bu değişiklikleri geri almalıdır.

Testler çevreye bağlı olmamalıdır, çünkü bu tür bağımlılıklar testi kırılgan hale getirebilir veya yalnızca belirli koşullar altında meydana gelen ve yeniden üretilmesi zor olan arızalara neden olabilir. Tabii ki, test edilen kod davranışını etkileyen ayarlara sahip olabilir. Bu durumda, test let-bindsüresi boyunca belirli bir konfigürasyon ayarlamak için testi bu gibi tüm ayar değişkenlerini yapmak en iyisidir . Test ayrıca birkaç farklı konfigürasyon ayarlayabilir ve her biriyle test edilen kodu çalıştırabilir.

SQLite gibi. İşte tasarımı:

  1. sqlite3_open () → Yeni veya mevcut bir SQLite veritabanına bağlantı açın. Sqlite3 için yapıcı.

  2. sqlite3 → Veri tabanı bağlantı nesnesi. Sqlite3_open () tarafından düzenlendi ve sqlite3_close () tarafından yok edildi.

  3. sqlite3_stmt → Hazırlanan ifade nesnesi. Sqlite3_prepare () tarafından düzenlendi ve sqlite3_finalize () tarafından yok edildi.

  4. sqlite3_prepare () → SQL metnini veri tabanını sorgulama veya güncelleme işini yapacak bayt kodunda derleyin. Sqlite3_stmt için yapıcı.

  5. sqlite3_bind () → Uygulama verilerini orijinal SQL parametrelerine kaydedin.

  6. sqlite3_step () → Bir sonraki sonuç satırına veya tamamlanmasına bir sqlite3_stmt ilerletin.

  7. sqlite3_column () → Bir sqlite3_stmt için geçerli sonuç satırındaki sütun değerleri.

  8. sqlite3_finalize () → sqlite3_stmt için yıkıcı.

  9. sqlite3_exec () → Bir veya daha fazla SQL ifadesi dizesi için sqlite3_prepare (), sqlite3_step (), sqlite3_column () ve sqlite3_finalize () işlevlerini içeren bir sarmalayıcı işlevi.

  10. sqlite3_close () → sqlite3 için yıkıcı.

sqlite3 mimarisi

Tokenizer, Parser ve Code Generator bileşenleri, SQL deyimlerini işlemek ve bunları sanal bir makine dilinde veya bayt kodunda yürütülebilir programlara dönüştürmek için kullanılır. Kabaca söylemek gerekirse, bu ilk üç katman sqlite3_prepare_v2 () öğesini uygular . İlk üç katman tarafından oluşturulan bayt kodu hazırlanmış bir ifadedir.. Sanal Makine modülü, SQL deyimi bayt kodunu çalıştırmaktan sorumludur. B-Tree modülü, bir veritabanı dosyasını, sipariş edilen anahtarlar ve logaritmik performans ile birden fazla anahtar / değer deposunda düzenler. Çağrı modülü, veritabanı dosyasının sayfalarını belleğe yüklemek, işlemleri uygulamak ve kontrol etmek ve bir çökme ya da elektrik kesintisinin ardından veritabanı bozulmasını önleyen günlük dosyaları oluşturmaktan ve korumaktan sorumludur. İşletim Sistemi Arayüzü, SQLite'ı farklı işletim sistemlerinde çalışacak şekilde uyarlamak için ortak bir rutin seti sağlayan ince bir soyutlamadır. Kabaca konuşmak gerekirse, alt dört katman sqlite3_step () öğesini uygular .

sqlite3 sanal tablo

Sanal tablo, açık bir SQLite veritabanı bağlantısıyla kaydedilen bir nesnedir. Bir SQL ifadesinin perspektifinden bakıldığında, sanal tablo nesnesi diğer tablo veya görünümlere benzer. Ancak perde arkasında sanal bir tablodaki sorgular ve güncellemeler, veritabanı dosyasını okumak ve yazmak yerine sanal tablo nesnesinin geri çağrı yöntemlerini çağırır.

Sanal tablo, bellek içi veri yapılarını temsil edebilir. Veya SQLite biçiminde olmayan diskteki verilerin görünümünü temsil edebilir. Veya uygulama talep üzerine sanal tablonun içeriğini hesaplayabilir.

Sanal tablolar için mevcut ve ileride kullanılan kullanımlar:

Tam metin arama arayüzü
R-Trees kullanarak mekansal endeksler
Bir SQLite veritabanı dosyasının disk içeriğini inceleyin (dbstat sanal tablosu)
Virgülle ayrılmış değer (CSV) dosyasının içeriğini okuyun ve / veya yazın
Ana bilgisayarın dosya sistemine sanki veritabanı tablosu gibi eriş
R gibi istatistik paketlerinde verilerin SQL manipülasyonunu etkinleştirme

SQLite, aşağıdakiler de dahil olmak üzere çeşitli test tekniklerini kullanır:

Bağımsız olarak geliştirilen üç test kablo demeti
Dağıtılmış bir yapılandırmada% 100 branş testi kapsamı
Milyonlarca ve milyonlarca test durumu
Bellek dışı sınamalar
G / Ç hata testleri
Çarpma ve güç kaybı testleri
Fuzz testleri
Sınır değer testleri
Engelli optimizasyon testleri
Regresyon testleri
Hatalı biçimlendirilmiş veritabanı testleri
Assert () ve çalışma zamanı kontrollerinin yoğun kullanımı
Valgrind analizi
Tanımsız davranış kontrolleri
Denetim Listeleri

Referanslar

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.