Misalkan ada Page
kelas, yang mewakili satu set instruksi ke renderer halaman. Dan Misalkan ada Renderer
kelas 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 $data
agar bisa bekerja.
Tapi, saya bisa membuat PdfRenderer
kelas 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 $renderer
menerima PageSnippet
dan semua yang $data
diperlukan 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.
Jawaban:
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).sumber
$renderer
itu akan memutuskan bagaimana membuat. Ketika$page
pembicaraan untuk$renderer
semua itu dikatakan apa yang harus disajikan. Tidak bagaimana. Tidak$page
tahu bagaimana. Itu membuat saya dalam masalah SRP?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:
Jika Anda tertarik, David West berbicara tentang prinsip-prinsip OOP asli dalam bukunya, Object Thinking .
sumber
Di sini kita telah
page
sepenuhnya 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 ".
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:
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.
sumber
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
Page
ini 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 menyatakanThe
Page
kelas tidak peduli apakah terdapatRenderer
di alam semesta. Itu hanya peduli tentang menjadi representasi halaman. Jadi kelas atau antarmukaRenderer
tidak boleh disebutkan di dalam aPage
.JAWABAN TERBARU
Jika saya menjawab pertanyaan Anda dengan benar,
PageSnippet
kelas seharusnya hanya peduli menjadi cuplikan halaman.PdfRenderer
berkaitan dengan rendering.Penggunaan klien
Beberapa hal yang perlu dipertimbangkan:
$data
sebagai array asosiatif. Itu harus menjadi instance dari kelas.html
properti$data
array adalah detail khusus untuk domain Anda, danPageSnippet
mengetahui detail ini.sumber
printOn:aStream
metode, 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.Page
tidak menyadari $ renderer adalah hal yang mustahil. Saya menambahkan beberapa kode ke pertanyaan saya, lihatPageSnippet
kelas. 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 membuatPageSnippet
kelas yang hanya menampung larik instruksi teks ke PDF, dan meminta beberapa kelas menafsirkan instruksi tersebut. Cara yang saya dapat menghindari menyuntikkan$pdf
ke dalamPageSnippet
, dengan mengorbankan kompleksitas tambahanIdealnya, 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
Page
berisi "satu set instruksi ke perender halaman". Saya membayangkan sesuatu seperti ini:Jadi
$page->renderMe($renderer)
, karena Page perlu referensi ke renderer.Tetapi instruksi render alternatif juga bisa dinyatakan sebagai struktur data daripada panggilan langsung, misalnya.
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:
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 memilikiScreenRenderer
dan menambahkanPrintRenderer
? Halaman yang sama mungkin dibuat oleh keduanya.sumber
page
ini jelas merupakan input ke renderer, bukan output, untuk gagasan itu jelas tidak cocok.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.sumber
Saya pikir sebagian besar Kelas dapat dibagi dalam salah satu dari dua kategori:
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
isAdult
karena dapat diturunkan langsung dari kelasnyabirthDate
tetapi tidak berfungsihasBirthDay
karena membutuhkan informasi eksternal (tanggal saat ini).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,
Page
akan 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
Page
dapat 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
Renderer
adalah 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
MobileRenderer
dan aStandardRenderer
, keduanya merupakan implementasi dariRenderer
kelas tetapi dengan pengaturan yang berbeda.Jadi karena
Page
berisi data dan harus dijaga tetap bodoh, solusi terbersih dalam hal ini adalah meneruskannyaPage
keRenderer
:sumber