Sebagaimana dinyatakan dalam beberapa jawaban dan komentar, DTOs yang tepat dan berguna dalam beberapa situasi, terutama dalam mentransfer data melintasi batas-batas (misalnya serialisasi ke JSON untuk mengirim melalui layanan web). Untuk sisa jawaban ini, saya akan sedikit banyak mengabaikan itu dan berbicara tentang kelas domain, dan bagaimana mereka dapat dirancang untuk meminimalkan (jika tidak menghilangkan) getter dan setter, dan masih berguna dalam proyek besar. Saya juga tidak akan berbicara tentang mengapa menghapus getter atau setter, atau kapan melakukannya, karena itu adalah pertanyaan mereka sendiri.
Sebagai contoh, bayangkan bahwa proyek Anda adalah permainan papan seperti Catur atau Battleship. Anda mungkin memiliki berbagai cara untuk mewakili ini dalam lapisan presentasi (aplikasi konsol, layanan web, GUI, dll.), Tetapi Anda juga memiliki domain inti. Satu kelas yang mungkin Anda miliki adalah Coordinate
, mewakili posisi di papan tulis. Cara "jahat" untuk menulisnya adalah:
public class Coordinate
{
public int X {get; set;}
public int Y {get; set;}
}
(Saya akan menulis contoh kode dalam C # daripada Java, untuk singkatnya dan karena saya lebih akrab dengannya. Mudah-mudahan itu bukan masalah. Konsepnya sama dan terjemahannya harus sederhana.)
Menghapus Setters: Immutability
Sementara getter dan setter publik keduanya berpotensi bermasalah, setter adalah yang jauh lebih "jahat" dari keduanya. Mereka juga biasanya lebih mudah dihilangkan. Prosesnya sederhana, atur nilai dari dalam konstruktor. Metode apa pun yang sebelumnya bermutasi objek seharusnya mengembalikan hasil baru. Begitu:
public class Coordinate
{
public int X {get; private set;}
public int Y {get; private set;}
public Coordinate(int x, int y)
{
X = x;
Y = y;
}
}
Perhatikan bahwa ini tidak melindungi terhadap metode lain di kelas yang bermutasi X dan Y. Agar lebih tidak dapat diubah, Anda bisa menggunakan readonly
( final
di Jawa). Tetapi bagaimanapun juga - apakah Anda membuat properti Anda benar-benar tidak dapat diubah atau hanya mencegah mutasi publik langsung melalui setter - itu memang menghilangkan trik setter publik Anda. Dalam sebagian besar situasi, ini berfungsi dengan baik.
Menghapus Getters, Bagian 1: Merancang untuk Perilaku
Di atas semua baik dan bagus untuk setter, tetapi dalam hal getter, kami benar-benar menembak diri sendiri sebelum memulai. Proses kami adalah memikirkan apa yang dimaksud dengan koordinat - data yang diwakilinya - dan membuat kelas di sekitarnya. Sebagai gantinya, kita harus mulai dengan perilaku apa yang kita butuhkan dari seorang koordinator. Omong-omong, proses ini dibantu oleh TDD, di mana kita hanya mengekstrak kelas-kelas seperti ini begitu kita membutuhkannya, jadi kita mulai dengan perilaku yang diinginkan dan bekerja dari sana.
Jadi katakanlah tempat pertama yang Anda perlukan Coordinate
adalah untuk deteksi tabrakan: Anda ingin memeriksa apakah dua potong menempati ruang yang sama di papan tulis. Inilah cara "jahat" (konstruktor dihilangkan karena singkatnya):
public class Piece
{
public Coordinate Position {get; private set;}
}
public class Coordinate
{
public int X {get; private set;}
public int Y {get; private set;}
}
//...And then, inside some class
public bool DoPiecesCollide(Piece one, Piece two)
{
return one.X == two.X && one.Y == two.Y;
}
Dan inilah cara yang baik:
public class Piece
{
private Coordinate _position;
public bool CollidesWith(Piece other)
{
return _position.Equals(other._position);
}
}
public class Coordinate
{
private readonly int _x;
private readonly int _y;
public bool Equals(Coordinate other)
{
return _x == other._x && _y == other._y;
}
}
( IEquatable
implementasi disingkat karena kesederhanaan). Dengan mendesain untuk perilaku daripada memodelkan data, kami telah berhasil menghapus getter kami.
Perhatikan ini juga relevan dengan contoh Anda. Anda mungkin menggunakan ORM, atau menampilkan informasi pelanggan di situs web atau sesuatu, dalam hal ini semacam Customer
DTO mungkin masuk akal. Tetapi hanya karena sistem Anda mencakup pelanggan dan mereka terwakili dalam model data tidak secara otomatis berarti Anda harus memiliki Customer
kelas di domain Anda. Mungkin saat Anda mendesain untuk perilaku, seseorang akan muncul, tetapi jika Anda ingin menghindari getter, jangan membuatnya terlebih dahulu.
Menghapus Getters, Bagian 2: Perilaku Eksternal
Jadi di atas adalah awal yang baik, tetapi cepat atau lambat Anda mungkin akan mengalami situasi di mana Anda memiliki perilaku yang berhubungan dengan kelas, yang dalam beberapa cara tergantung pada negara kelas, tetapi yang bukan milik atas kelas. Perilaku semacam ini adalah yang biasanya hidup di lapisan layanan aplikasi Anda.
Mengambil Coordinate
contoh kami , pada akhirnya Anda akan ingin mewakili permainan Anda kepada pengguna, dan itu mungkin berarti menggambar ke layar. Anda mungkin, misalnya, memiliki proyek UI yang digunakan Vector2
untuk mewakili suatu titik di layar. Tetapi tidak pantas bagi Coordinate
kelas untuk mengambil alih konversi dari koordinat ke titik di layar - yang akan membawa segala macam masalah presentasi ke dalam domain inti Anda. Sayangnya situasi semacam ini melekat dalam desain OO.
Pilihan pertama , yang sangat umum dipilih, adalah hanya mengekspos para pengambil sialan dan mengatakan persetan dengan itu. Ini memiliki keunggulan kesederhanaan. Tetapi karena kita berbicara tentang menghindari getter, katakanlah demi argumen, kita menolak yang ini dan melihat opsi apa yang ada.
Opsi kedua adalah menambahkan semacam .ToDTO()
metode di kelas Anda. Ini - atau yang serupa - mungkin diperlukan juga, misalnya ketika Anda ingin menyimpan permainan, Anda perlu menangkap hampir semua negara Anda. Tetapi perbedaan antara melakukan ini untuk layanan Anda dan hanya mengakses pengambil secara langsung lebih atau kurang estetika. Itu masih memiliki "kejahatan" sebanyak itu.
Opsi ketiga - yang saya lihat didukung oleh Zoran Horvat dalam beberapa video Pluralsight- adalah menggunakan versi modifikasi dari pola pengunjung. Ini adalah penggunaan yang cukup tidak biasa dan variasi dari pola dan saya pikir jarak tempuh orang akan sangat bervariasi pada apakah itu menambah kompleksitas tanpa keuntungan nyata atau apakah itu kompromi yang bagus untuk situasi tersebut. Idenya pada dasarnya adalah untuk menggunakan pola pengunjung standar, tetapi minta Visit
metode mengambil status yang mereka butuhkan sebagai parameter, bukan kelas yang mereka kunjungi. Contohnya dapat ditemukan di sini .
Untuk masalah kami, solusi menggunakan pola ini adalah:
public class Coordinate
{
private readonly int _x;
private readonly int _y;
public T Transform<T>(IPositionTransformer<T> transformer)
{
return transformer.Transform(_x,_y);
}
}
public interface IPositionTransformer<T>
{
T Transform(int x, int y);
}
//This one lives in the presentation layer
public class CoordinateToVectorTransformer : IPositionTransformer<Vector2>
{
private readonly float _tileWidth;
private readonly float _tileHeight;
private readonly Vector2 _topLeft;
Vector2 Transform(int x, int y)
{
return _topLeft + new Vector2(_tileWidth*x + _tileHeight*y);
}
}
Seperti yang Anda mungkin tahu, _x
dan _y
tidak benar - benar dienkapsulasi lagi. Kita dapat mengekstraknya dengan membuat IPositionTransformer<Tuple<int,int>>
yang baru mengembalikannya secara langsung. Tergantung pada rasanya, Anda mungkin merasa ini membuat seluruh latihan menjadi sia-sia.
Namun, dengan getter publik, sangat mudah untuk melakukan hal-hal dengan cara yang salah, hanya menarik data secara langsung dan menggunakannya melanggar Tell, Don't Ask . Sedangkan menggunakan pola ini sebenarnya lebih mudah untuk melakukannya dengan cara yang benar: ketika Anda ingin membuat perilaku, Anda akan secara otomatis mulai dengan membuat tipe yang terkait dengannya. Pelanggaran terhadap TDA akan sangat berbau dan mungkin membutuhkan solusi yang lebih sederhana dan lebih baik. Dalam praktiknya, poin-poin ini membuatnya jauh lebih mudah untuk melakukannya dengan cara yang benar, OO, daripada cara "jahat" yang didorong oleh para pengecut.
Akhirnya , bahkan jika itu awalnya tidak jelas, mungkin sebenarnya ada cara untuk mengekspos cukup dari apa yang Anda butuhkan sebagai perilaku untuk menghindari keharusan untuk mengekspos keadaan. Misalnya, menggunakan versi kami sebelumnya Coordinate
yang hanya anggota publik Equals()
(dalam praktiknya akan membutuhkan IEquatable
implementasi penuh ), Anda dapat menulis kelas berikut di lapisan presentasi Anda:
public class CoordinateToVectorTransformer
{
private Dictionary<Coordinate,Vector2> _coordinatePositions;
public CoordinateToVectorTransformer(int boardWidth, int boardHeight)
{
for(int x=0; x<boardWidth; x++)
{
for(int y=0; y<boardWidth; y++)
{
_coordinatePositions[new Coordinate(x,y)] = GetPosition(x,y);
}
}
}
private static Vector2 GetPosition(int x, int y)
{
//Some implementation goes here...
}
public Vector2 Transform(Coordinate coordinate)
{
return _coordinatePositions[coordinate];
}
}
Ternyata, mungkin secara mengejutkan, bahwa semua perilaku yang benar - benar kita butuhkan dari koordinat untuk mencapai tujuan kita adalah pengecekan kesetaraan! Tentu saja, solusi ini dirancang untuk masalah ini, dan membuat asumsi tentang penggunaan / kinerja memori yang dapat diterima. Itu hanya contoh yang sesuai dengan domain masalah khusus ini, bukan cetak biru untuk solusi umum.
Dan lagi, pendapat akan bervariasi tergantung pada apakah dalam praktiknya ini adalah kompleksitas yang tidak perlu. Dalam beberapa kasus, tidak ada solusi seperti ini yang mungkin ada, atau mungkin sangat aneh atau kompleks, dalam hal ini Anda dapat kembali ke tiga di atas.