Dalam bahasa berorientasi objek, kapan objek harus melakukan operasi pada diri mereka sendiri dan kapan operasi harus dilakukan pada objek?


11

Misalkan ada Pagekelas, yang mewakili satu set instruksi ke renderer halaman. Dan Misalkan ada Rendererkelas yang tahu cara membuat halaman di layar. Dimungkinkan untuk menyusun kode dengan dua cara berbeda:

/*
 * 1) Page Uses Renderer internally,
 * or receives it explicitly
 */
$page->renderMe(); 
$page->renderMe($renderer); 

/*
 * 2) Page is passed to Renderer
 */
$renderer->renderPage($page);

Apa pro dan kontra dari setiap pendekatan? Kapan seseorang akan lebih baik? Kapan yang lain akan lebih baik?


LATAR BELAKANG

Untuk menambahkan sedikit lebih banyak latar belakang - Saya menemukan diri saya menggunakan kedua pendekatan dalam kode yang sama. Saya menggunakan perpustakaan PDF pihak ke-3 yang disebut TCPDF. Di suatu tempat dalam kode saya, saya harus memiliki yang berikut agar rendering PDF berfungsi:

$pdf = new TCPDF();
$html = "some text";
$pdf->writeHTML($html);

Katakanlah saya ingin membuat representasi halaman. Saya bisa membuat templat yang berisi instruksi untuk membuat cuplikan halaman PDF seperti ini:

/*
 * A representation of the PDF page snippet:
 * a template directing how to render a specific PDF page snippet
 */
class PageSnippet
{    
    function runTemplate(TCPDF $pdf, array $data = null): void
    {
        $pdf->writeHTML($data['html']);
    }
}

/* To be used like so */
$pdf = new TCPDF();
$data['html'] = "some text";
$snippet = new PageSnippet();
$snippet->runTemplate($pdf, $data);

1) Perhatikan di sini bahwa $snippet berjalan sendiri , seperti dalam contoh kode pertama saya. Itu juga perlu tahu dan terbiasa dengan $pdf, dan dengan apa pun $dataagar bisa bekerja.

Tapi, saya bisa membuat PdfRendererkelas seperti ini:

class PdfRenderer
{
    /**@var TCPDF */
    protected $pdf;

    function __construct(TCPDF $pdf)
    {
        $this->pdf = $pdf;
    }

    function runTemplate(PageSnippet $template, array $data = null): void
    {
        $template->runTemplate($this->pdf, $data);
    }
}

dan kemudian kode saya berubah menjadi ini:

$renderer = new PdfRenderer(new TCPDF());
$renderer->runTemplate(new PageSnippet(), array('html' => 'some text'));

2) Di sini $renderermenerima PageSnippetdan semua yang $datadiperlukan untuk itu berfungsi. Ini mirip dengan contoh kode kedua saya.

Jadi, meskipun renderer menerima potongan halaman, di dalam renderer, snippet tetap berjalan sendiri . Dengan kata lain kedua pendekatan itu berperan. Saya tidak yakin apakah Anda dapat membatasi penggunaan OO hanya untuk satu atau hanya yang lain. Keduanya mungkin diperlukan, bahkan jika Anda menutupi satu sama lain.


2
Sayangnya, Anda telah berkeliaran di dunia perangkat lunak "perang agama" di sini, di sepanjang garis apakah akan menggunakan spasi atau tab, yang menahan gaya untuk digunakan, dll. Tidak ada "lebih baik" di sini, hanya pendapat kuat di kedua sisi. Lakukan pencarian di internet tentang manfaat dan kerugian dari model domain kaya dan anemik dan bentuk opini Anda sendiri.
David Arno

7
@ DavidArno Gunakan spasi Anda kafir! :)
candied_orange

1
Ha, saya benar-benar tidak mengerti situs ini di waktu Pertanyaan yang sangat bagus yang mendapatkan jawaban yang baik ditutup dalam waktu singkat sebagai berbasis opini. Namun pertanyaan yang jelas berdasarkan pendapat seperti ini muncul dan para tersangka yang biasa itu tidak dapat ditemukan. Oh well, jika Anda tidak bisa mengalahkan mereka dan semua itu ... :)
David Arno

@ Erik Eidt, dapatkah Anda membatalkan penghapusan jawaban Anda, karena saya merasakannya sebagai jawaban "sebagainya pilihan" yang sangat bagus.
David Arno

1
Selain dari prinsip-prinsip SOLID, Anda dapat melihat GRASP , terutama pada bagian Expert . Pertanyaannya, manakah yang memiliki informasi bagi Anda untuk memenuhi tanggung jawab?
OnesimusUnbound

Jawaban:


13

Ini sepenuhnya tergantung pada apa yang Anda pikirkan tentang OO .

Untuk OOP = SOLID, operasi harus menjadi bagian dari kelas jika itu adalah bagian dari Tanggung Jawab Tunggal kelas.

Untuk OO = virtual dispatch / polymorphism, operasi harus menjadi bagian dari objek jika harus dikirim secara dinamis, yaitu jika dipanggil melalui antarmuka.

Untuk OO = enkapsulasi, operasi harus menjadi bagian dari kelas jika menggunakan keadaan internal yang tidak ingin Anda tampilkan.

Untuk OO = "Saya suka antarmuka yang lancar", pertanyaannya adalah varian mana yang dibaca lebih alami.

Untuk OO = memodelkan entitas dunia nyata, entitas dunia nyata mana yang melakukan operasi ini?


Semua sudut pandang itu biasanya salah dalam isolasi. Tetapi kadang-kadang satu atau lebih dari perspektif ini sangat membantu untuk sampai pada keputusan desain.

Misalnya menggunakan sudut pandang polimorfisme: Jika Anda memiliki strategi render yang berbeda (seperti format output yang berbeda, atau mesin render yang berbeda), maka $renderer->render($page)sangat masuk akal. Tetapi jika Anda memiliki jenis halaman berbeda yang harus dirender secara berbeda, $page->render()mungkin lebih baik. Jika output tergantung pada tipe halaman dan strategi rendering, Anda dapat melakukan pengiriman ganda melalui pola pengunjung.

Jangan lupa bahwa dalam banyak bahasa, fungsi tidak harus menjadi metode. Sebuah fungsi sederhana seperti render($page)jika seringkali merupakan solusi yang sangat baik (dan sangat sederhana).


Eh tunggu sebentar. Saya masih bisa mendapatkan render polimorfik jika halaman tersebut memiliki referensi ke renderer tetapi tidak tahu renderer mana yang dipegangnya. Ini hanya berarti polimorfisme sedikit lebih jauh ke bawah lubang kelinci. Saya juga dapat memilih dan memilih apa yang harus diserahkan kepada pemberi render. Saya tidak harus melewati seluruh halaman.
candied_orange

@CandiedOrange Itu poin yang bagus, tapi saya akan memesan argumen Anda di bawah SRP: itu akan menjadi tanggung jawab R-capital Page untuk memutuskan bagaimana itu diberikan, mungkin menggunakan semacam strategi rendering polimorfik.
amon

Saya pikir $rendereritu akan memutuskan bagaimana membuat. Ketika $pagepembicaraan untuk $renderersemua itu dikatakan apa yang harus disajikan. Tidak bagaimana. Tidak $pagetahu bagaimana. Itu membuat saya dalam masalah SRP?
candied_orange

Saya benar-benar tidak berpikir kita tidak setuju. Saya mencoba mengurutkan komentar pertama Anda ke dalam kerangka kerja konseptual dari jawaban ini, tetapi saya mungkin menggunakan kata-kata canggung. Satu hal yang Anda ingatkan kepada saya tentang hal yang tidak saya sebutkan dalam jawabannya: aliran data tell-don't-ask juga heuristik yang baik.
amon

Hmm baiklah. Kamu benar. Apa yang saya bicarakan akan mengikuti cerewet. Sekarang perbaiki saya jika saya salah. Strategi lain, di mana pemberi render mengambil referensi halaman, berarti renderer harus berbalik dan meminta halaman untuk barang-barang, menggunakan halaman getter.
candied_orange

2

Menurut Alan Kay , objek adalah swasembada, "dewasa" dan organisme yang bertanggung jawab. Orang dewasa melakukan sesuatu, mereka tidak dioperasi. Artinya, transaksi keuangan bertanggung jawab untuk menyelamatkan dirinya sendiri , halaman bertanggung jawab untuk merender dirinya sendiri , dll, dll. Lebih singkatnya, enkapsulasi adalah hal besar dalam OOP. Secara khusus, ini dimanifestasikan melalui prinsip Tell don't ask yang terkenal (yang suka disebutkan oleh @CandiedOrange setiap saat :)) dan penolakan publik terhadap getter dan setter .

Dalam praktiknya ini menghasilkan objek yang memiliki semua sumber daya yang diperlukan untuk melakukan pekerjaan mereka, seperti fasilitas basis data, fasilitas render, dll.

Jadi dengan mempertimbangkan contoh Anda, versi OOP saya akan terlihat seperti berikut:

class Page
{
    private $data;
    private $renderer;

    public function __construct(ICanRender $renderer, $data)
    {
        $this->renderer = $renderer;
        $this->data = $data;
    }

    public function render()
    {
        $this->renderer->render($this->data);
    }
}

Jika Anda tertarik, David West berbicara tentang prinsip-prinsip OOP asli dalam bukunya, Object Thinking .


1
Terus terang, siapa yang peduli apa yang dikatakan seseorang tentang sesuatu yang berkaitan dengan pengembangan perangkat lunak, 15 tahun yang lalu, kecuali karena minat historis?
David Arno

1
" Aku peduli apa yang dikatakan orang yang menemukan konsep berorientasi objek tentang objek apa itu. " Kenapa? Di luar membuai Anda menggunakan kekeliruan "naik banding ke otoritas" dalam argumen Anda, apa kemungkinan yang dapat dipikirkan oleh penemu istilah pada aplikasi yang diajukan 15 tahun kemudian?
David Arno

2
@Zapadlo: Anda tidak menyajikan argumen mengapa pesannya dari Halaman ke Renderer dan bukan sebaliknya. Mereka berdua objek, dan karenanya keduanya orang dewasa, kan?
JacquesB

1
" Banding terhadap kekeliruan wewenang tidak dapat diterapkan di sini " ... " Jadi rangkaian konsep yang menurut Anda mewakili OOP, sebenarnya salah [karena ini merupakan distorsi dari definisi asli] ". Saya kira Anda tidak tahu apa banding ke kekeliruan otoritas? Petunjuk: Anda menggunakannya di sini. :)
David Arno

1
@ David Arno Jadi, apakah semua banding ke otoritas salah? Apakah Anda lebih suka "Banding menurut pendapat saya?" Setiap kali seseorang mengutip Paman Bobisme, apakah Anda akan mengeluh tentang naik banding ke pihak berwenang? Zapadio menyediakan sumber yang sangat dihormati. Anda dapat tidak setuju, atau mengutip sumber yang bertentangan, tetapi pengulang mengeluh bahwa seseorang telah memberikan kutipan tidak konstruktif.
user949300

2

$page->renderMe();

Di sini kita telah pagesepenuhnya bertanggung jawab untuk menyerahkan diri. Itu mungkin telah disediakan dengan render melalui konstruktor, atau mungkin memiliki fungsionalitas bawaan.

Saya akan mengabaikan case pertama (disertakan dengan render via constructor) di sini, karena sangat mirip dengan melewatinya sebagai parameter. Sebagai gantinya saya akan melihat pro dan kontra dari fungsionalitas yang sedang dibangun.

Pro adalah memungkinkan enkapsulasi tingkat tinggi. Halaman tidak perlu mengungkapkan apa-apa tentang keadaan batinnya secara langsung. Itu hanya memaparkannya melalui rendering itu sendiri.

Yang kontra adalah bahwa itu melanggar prinsip tanggung jawab tunggal (SRP). Kami memiliki kelas yang bertanggung jawab untuk merangkum keadaan halaman dan juga sulit dikodekan dengan aturan tentang cara membuat dirinya sendiri dan dengan demikian kemungkinan seluruh jajaran tanggung jawab lainnya sebagai objek harus "melakukan sesuatu untuk diri mereka sendiri, tidak melakukan hal-hal kepada mereka oleh orang lain ".

$page->renderMe($renderer);

Di sini, kami masih memerlukan halaman untuk dapat membuat sendiri, tetapi kami menyediakannya dengan objek pembantu yang dapat melakukan rendering yang sebenarnya. Dua skenario dapat muncul di sini:

  1. Halaman ini hanya perlu mengetahui aturan rendering (metode mana yang dipanggil dalam urutan mana) untuk membuat render itu. Enkapsulasi dipertahankan, tetapi SRP masih rusak karena halaman masih harus mengawasi proses rendering, atau
  2. Halaman tersebut hanya memanggil satu metode pada objek penyaji, meneruskan detailnya. Kami semakin mendekati untuk menghormati SRP, tetapi kami sekarang telah melemahkan enkapsulasi.

$renderer->renderPage($page);

Di sini, kami sepenuhnya menghormati SRP. Objek halaman bertanggung jawab untuk menyimpan informasi pada halaman dan renderer bertanggung jawab untuk merender halaman itu. Namun, kami sekarang telah benar-benar melemahkan enkapsulasi objek halaman karena harus membuat seluruh keadaannya menjadi publik.

Kami juga telah menciptakan masalah baru: renderer sekarang tergabung erat dengan kelas halaman. Apa yang terjadi ketika kami ingin merender sesuatu yang berbeda ke halaman?

Mana yang terbaik? Tidak satupun dari mereka. Mereka semua memiliki kekurangan.


Tidak setuju bahwa V3 menghormati SRP. Renderer memiliki setidaknya 2 alasan untuk berubah: jika Halaman berubah, atau jika cara Anda mengubahnya. Dan yang ketiga, yang Anda liput, jika Renderer perlu merender objek selain Halaman. Kalau tidak, analisis yang bagus.
user949300

2

Jawaban atas pertanyaan ini tegas. Ini adalah $renderer->renderPage($page);implementasi yang benar. Untuk memahami bagaimana kita sampai pada kesimpulan ini, kita perlu memahami enkapsulasi.

Apa itu halaman? Ini adalah representasi dari tampilan yang akan dikonsumsi seseorang. "Seseorang" itu bisa manusia atau bot. Perhatikan bahwa Pageini adalah representasi, dan bukan tampilan itu sendiri. Apakah representasi ada tanpa diwakili? Apakah halaman itu sesuatu tanpa renderer? Jawabannya Ya, representasi bisa eksis tanpa diwakili. Mewakili adalah tahap selanjutnya.

Apa itu renderer tanpa halaman? Bisakah renderer me-render tanpa halaman? Tidak. Jadi antarmuka Renderer memang perlu renderPage($page);metode ini.

Ada apa dengan ini $page->renderMe($renderer);?

Itu adalah fakta yang renderMe($renderer)masih harus menelepon secara internal $renderer->renderPage($page);. Ini melanggar Hukum Demeter yang menyatakan

Setiap unit hanya memiliki pengetahuan terbatas tentang unit lain

The Pagekelas tidak peduli apakah terdapat Rendererdi alam semesta. Itu hanya peduli tentang menjadi representasi halaman. Jadi kelas atau antarmuka Renderertidak boleh disebutkan di dalam a Page.


JAWABAN TERBARU

Jika saya menjawab pertanyaan Anda dengan benar, PageSnippetkelas seharusnya hanya peduli menjadi cuplikan halaman.

class PageSnippet
{    
    /** string */
    private $html;

    function __construct($data = ['html' => '']): void
    {
        $this->html = $data['html'];
    }

   public function getHtml()
   {
       return $this->html;
   }
}

PdfRenderer berkaitan dengan rendering.

class PdfRenderer
{
    /**@var TCPDF */
    protected $pdf;

    function __construct(TCPDF $pdf = new TCPDF())
    {
        $this->pdf = $pdf;
    }

    function runTemplate(string $html): void
    {
        $this->pdf->writeHTML($html);
    }
}

Penggunaan klien

$renderer = new PdfRenderer();
$snippet = new PageSnippet(['html' => '<html />']);
$renderer->runTemplate($snippet->getHtml());

Beberapa hal yang perlu dipertimbangkan:

  • Praktek yang buruk untuk dilewatkan $datasebagai array asosiatif. Itu harus menjadi instance dari kelas.
  • Fakta bahwa format halaman terkandung di dalam htmlproperti $dataarray adalah detail khusus untuk domain Anda, dan PageSnippetmengetahui detail ini.

Tetapi, bagaimana jika, selain Halaman, Anda memiliki Gambar, Artikel, dan Triptichs? Dalam skema Anda, Renderer harus tahu tentang mereka semua. Itu banyak kebocoran. Hanya makanan untuk dipikirkan.
user949300

@ user949300: Ya, jika Renderer harus bisa membuat gambar dll, maka jelas perlu tahu tentang mereka.
JacquesB

1
Smalltalk Best Practice Patterns oleh Kent Beck memperkenalkan pola Metode Reversing , di mana keduanya didukung. Artikel yang ditautkan menunjukkan bahwa suatu objek mendukung suatu printOn:aStreammetode, tetapi yang dilakukannya hanyalah memberi tahu aliran untuk mencetak objek tersebut. Analogi dengan jawaban Anda adalah bahwa tidak ada alasan Anda tidak dapat memiliki halaman yang dapat dirender menjadi penyaji dan penyaji yang dapat merender halaman, dengan satu implementasi dan pilihan antarmuka yang nyaman.
Graham Lee

2
Anda harus mematahkan / memalsukan SRP dalam hal apa pun, tetapi jika Renderer perlu tahu cara merender banyak hal yang berbeda, itu benar-benar "banyak tanggung jawab", dan, jika mungkin, harus dihindari.
user949300

1
Saya suka jawaban Anda tetapi saya tergoda untuk berpikir bahwa Pagetidak menyadari $ renderer adalah hal yang mustahil. Saya menambahkan beberapa kode ke pertanyaan saya, lihat PageSnippetkelas. Ini adalah halaman yang efektif, tetapi tidak dapat ada tanpa membuat semacam referensi ke $pdf, yang sebenarnya merupakan renderer PDF pihak ke-3 dalam hal ini. .. Namun, saya kira meskipun saya bisa membuat PageSnippetkelas yang hanya menampung larik instruksi teks ke PDF, dan meminta beberapa kelas menafsirkan instruksi tersebut. Cara yang saya dapat menghindari menyuntikkan $pdfke dalam PageSnippet, dengan mengorbankan kompleksitas tambahan
Dennis

1

Idealnya, Anda ingin sesedikit mungkin ketergantungan antar kelas, karena ini mengurangi kompleksitas. Kelas seharusnya hanya memiliki ketergantungan ke kelas lain jika benar-benar membutuhkannya.

Anda menyatakan Pageberisi "satu set instruksi ke perender halaman". Saya membayangkan sesuatu seperti ini:

renderer.renderLine(x, y, w, h, Color.Black)
renderer.renderText(a, b, Font.Helvetica, Color.Black, "bla bla...")
etc...

Jadi $page->renderMe($renderer), karena Page perlu referensi ke renderer.

Tetapi instruksi render alternatif juga bisa dinyatakan sebagai struktur data daripada panggilan langsung, misalnya.

[
  Line(x, y, w, h, Color.Black), 
  Text(a, b, Font.Helvetica, Color.Black, "bla bla...")
]

Dalam hal ini Renderer yang sebenarnya akan mendapatkan struktur data ini dari Halaman dan memprosesnya dengan menjalankan instruksi render yang sesuai. Dengan pendekatan seperti itu dependensi akan dibalik - Halaman tidak perlu tahu tentang Renderer, tetapi Renderer harus diberikan Halaman yang kemudian dapat di render. Jadi opsi dua:$renderer->renderPage($page);

Jadi mana yang terbaik? Pendekatan pertama mungkin paling sederhana untuk diimplementasikan, sedangkan yang kedua jauh lebih fleksibel dan kuat, jadi saya kira itu tergantung pada kebutuhan Anda.

Jika Anda tidak dapat memutuskan, atau Anda pikir Anda mungkin mengubah pendekatan di masa depan, Anda dapat menyembunyikan keputusan di balik lapisan tipuan, sebuah fungsi:

renderPage($page, $renderer)

Satu-satunya pendekatan yang saya tidak akan merekomendasikan adalah $page->renderMe()karena itu menyarankan sebuah halaman hanya dapat memiliki satu renderer. Tetapi bagaimana jika Anda memiliki ScreenRendererdan menambahkan PrintRenderer? Halaman yang sama mungkin dibuat oleh keduanya.


Dalam konteks EPUB atau HTML, konsep halaman tidak ada tanpa renderer.
mouviciel

1
@mouviciel: Saya tidak yakin saya mengerti apa yang Anda maksud. Tentunya Anda dapat memiliki halaman HTML tanpa merendernya? Misalnya halaman proses perayap Google tanpa merendernya.
JacquesB

2
Ada gagasan berbeda dari halaman kata: hasil dari proses pagination ketika halaman HTML diformat untuk dicetak, mungkin itulah yang ada dalam pikiran @mouviciel. Namun, dalam pertanyaan pageini jelas merupakan input ke renderer, bukan output, untuk gagasan itu jelas tidak cocok.
Doc Brown

1

Bagian D kata SOLID

"Abstraksi tidak harus bergantung pada detail. Detail harus bergantung pada abstraksi."

Jadi, antara Page dan Renderer, yang lebih cenderung menjadi abstraksi yang stabil, lebih kecil kemungkinannya untuk berubah, mungkin mewakili sebuah antarmuka? Sebaliknya, mana yang merupakan "detail"?

Dalam pengalaman saya, abstraksi biasanya adalah Renderer. Misalnya, mungkin Stream atau XML sederhana, sangat abstrak dan stabil. Atau tata letak yang cukup standar. Halaman Anda lebih cenderung menjadi objek bisnis khusus, "detail". Dan Anda memiliki objek bisnis lain untuk dirender, seperti "gambar", "laporan", "bagan" dll ... (Mungkin bukan "tryptich" seperti dalam komentar saya)

Tapi itu jelas tergantung pada desain Anda. Halaman bisa abstrak, misalnya yang setara <article>dengan tag HTML dengan sub-bagian standar. Dan Anda memiliki banyak "pemberi render" pelaporan bisnis khusus yang berbeda. Dalam hal itu, Pengirim harus bergantung pada Halaman.


0

Saya pikir sebagian besar Kelas dapat dibagi dalam salah satu dari dua kategori:

  • Kelas yang berisi data (bisa berubah atau tidak berubah tidak masalah)

Ini adalah kelas yang hampir tidak memiliki ketergantungan pada hal lain. Mereka biasanya bagian dari domain Anda. Mereka seharusnya tidak mengandung logika atau hanya logika yang dapat diturunkan langsung dari kondisinya. Kelas Karyawan dapat memiliki fungsi isAdultkarena dapat diturunkan langsung dari kelasnya birthDatetetapi tidak berfungsi hasBirthDaykarena membutuhkan informasi eksternal (tanggal saat ini).

  • Kelas yang menyediakan layanan

Jenis kelas ini beroperasi pada kelas lain yang berisi data. Mereka biasanya dikonfigurasi sekali dan tidak dapat diubah (sehingga mereka selalu melakukan jenis fungsi yang sama). Namun jenis-jenis kelas ini mungkin masih menyediakan contoh pembantu singkat yang menyatakan keadaan untuk melakukan operasi yang lebih kompleks yang memerlukan mempertahankan beberapa negara bagian untuk periode yang singkat (seperti kelas Builder).

Contoh anda

Dalam contoh Anda, Pageakan menjadi kelas yang berisi data. Seharusnya memiliki fungsi untuk mendapatkan data ini dan mungkin memodifikasinya jika kelas seharusnya bisa berubah. Tetaplah bodoh, sehingga bisa digunakan tanpa banyak ketergantungan.

Data, atau dalam hal ini Anda Pagedapat diwakili dalam banyak cara. Itu bisa diterjemahkan sebagai halaman web, ditulis ke disk, disimpan dalam database, dikonversi ke JSON, apa pun. Anda tidak ingin menambahkan metode ke kelas semacam itu untuk masing-masing kasus ini (dan membuat dependensi pada semua jenis kelas lain, meskipun kelas Anda seharusnya hanya berisi data).

Anda Rendereradalah kelas jenis layanan yang khas. Itu dapat beroperasi pada set data tertentu dan mengembalikan hasilnya. Itu tidak memiliki banyak negara sendiri, dan negara apa yang biasanya tidak dapat diubah, dapat dikonfigurasi sekali dan kemudian digunakan kembali.

Misalnya, Anda dapat memiliki a MobileRendererdan a StandardRenderer, keduanya merupakan implementasi dari Rendererkelas tetapi dengan pengaturan yang berbeda.

Jadi karena Pageberisi data dan harus dijaga tetap bodoh, solusi terbersih dalam hal ini adalah meneruskannya Pageke Renderer:

$renderer->renderPage($page)

2
Logika yang sangat prosedural.
user949300
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.