Pola Umum untuk Delegasi
Delegasi menyediakan mekanisme yang memungkinkan desain perangkat lunak yang melibatkan koupling minimal antar komponen.
Salah satu contoh yang sangat baik untuk desain semacam ini adalah LINQ. Pola Ekspresi Kueri LINQ bergantung pada delegasi untuk semua fiturnya. Pertimbangkan contoh pencarian sederhana ini:
var smallNumbers = numbers.Where(n => n < 10);
Ini memfilter urutan angka hanya untuk yang kurang dari nilai 10.
Metode ini Where
menggunakan delegasi yang menentukan elemen urutan mana yang melewati filter. Saat Anda membuat kueri LINQ, Anda menyediakan implementasi delegasi untuk tujuan khusus ini.
Prototipe untuk metode Where adalah:
public static IEnumerable<TSource> Where<TSource> (this IEnumerable<TSource> source, Func<TSource, bool> predicate);
Contoh ini diulang dengan semua metode yang merupakan bagian dari LINQ. Mereka semua mengandalkan delegasi untuk kode yang mengelola kueri tertentu. Pola desain API ini adalah pola yang kuat untuk dipelajari dan dipahami.
Contoh sederhana ini menggambarkan bagaimana delegasi membutuhkan sangat sedikit kopling antar komponen. Anda tidak perlu membuat kelas yang berasal dari kelas dasar tertentu. Anda tidak perlu mengimplementasikan antarmuka tertentu. Satu-satunya persyaratan adalah menyediakan implementasi satu metode yang mendasar untuk tugas yang ditangani.
Membangun Komponen Anda Sendiri dengan Delegasi
Mari kita buat contoh tersebut dengan membuat komponen menggunakan desain yang bergantung pada delegasi.
Mari kita tentukan komponen yang dapat digunakan untuk pesan log dalam sistem besar. Komponen pustaka dapat digunakan di banyak lingkungan yang berbeda, pada beberapa platform yang berbeda. Ada banyak fitur umum dalam komponen yang mengelola log. Ini harus menerima pesan dari komponen apa pun dalam sistem. Pesan-pesan tersebut akan memiliki prioritas yang berbeda, yang dapat dikelola komponen inti. Pesan harus memiliki tanda waktu dalam formulir terakhir yang diarsipkan. Untuk skenario yang lebih canggih, Anda dapat memfilter pesan menurut komponen sumber.
Ada satu aspek fitur yang akan sering berubah: di mana pesan ditulis. Di beberapa lingkungan, mereka mungkin ditulis ke konsol kesalahan. Di yang lain, file. Kemungkinan lain termasuk penyimpanan database, log peristiwa OS, atau penyimpanan dokumen lainnya.
Ada juga kombinasi output yang mungkin digunakan dalam skenario yang berbeda. Anda mungkin ingin menulis pesan ke konsol dan ke file.
Desain berdasarkan delegasi akan memberikan banyak fleksibilitas, dan memudahkan untuk mendukung mekanisme penyimpanan yang mungkin ditambahkan di masa depan.
Di bawah desain ini, komponen log utama dapat menjadi kelas non-virtual, bahkan disegel. Anda dapat mencolokkan sekumpulan delegasi untuk menulis pesan ke media penyimpanan yang berbeda. Dukungan bawaan untuk delegasi multicast memudahkan untuk mendukung skenario di mana pesan harus ditulis ke beberapa lokasi (file, dan konsol).
Implementasi pertama
Mari kita mulai dari yang kecil: implementasi awal akan menerima pesan baru, dan menulisnya menggunakan delegasi terlampir. Anda dapat memulai dengan satu delegasi yang menulis pesan ke konsol.
public static class Logger
{
public static Action<string>? WriteMessage;
public static void LogMessage(string msg)
{
if (WriteMessage is not null)
WriteMessage(msg);
}
}
Kelas statis di atas adalah hal paling sederhana yang dapat bekerja. Kita perlu menulis implementasi tunggal untuk metode yang menulis pesan ke konsol:
public static class LoggingMethods
{
public static void LogToConsole(string message)
{
Console.Error.WriteLine(message);
}
}
Terakhir, Anda perlu menghubungkan delegasi dengan melampirkannya ke delegasi WriteMessage yang dideklarasikan dalam pencatat:
Logger.WriteMessage += LoggingMethods.LogToConsole;
Praktek
Sampel kami sejauh ini cukup sederhana, tetapi masih menunjukkan beberapa pedoman penting untuk desain yang melibatkan delegasi.
Menggunakan jenis delegasi yang ditentukan dalam kerangka kerja inti memudahkan pengguna untuk bekerja dengan delegasi. Anda tidak perlu menentukan jenis baru, dan pengembang yang menggunakan pustaka Anda tidak perlu mempelajari jenis delegasi baru yang khusus.
Antarmuka yang digunakan minimal dan fleksibel mungkin: Untuk membuat pencatat output baru, Anda harus membuat satu metode. Metode tersebut mungkin metode statis, atau metode instans. Ini mungkin memiliki akses apa pun.
Output format
Mari kita buat versi pertama ini sedikit lebih kuat, dan kemudian mulai membuat mekanisme pengelogan lainnya.
Selanjutnya, mari kita tambahkan beberapa argumen ke LogMessage()
metode sehingga kelas log Anda membuat pesan yang lebih terstruktur:
public enum Severity
{
Verbose,
Trace,
Information,
Warning,
Error,
Critical
}
public static class Logger
{
public static Action<string>? WriteMessage;
public static void LogMessage(Severity s, string component, string msg)
{
var outputMsg = $"{DateTime.Now}\t{s}\t{component}\t{msg}";
if (WriteMessage is not null)
WriteMessage(outputMsg);
}
}
Selanjutnya, mari kita gunakan Severity
argumen tersebut untuk memfilter pesan yang dikirim ke output log.
public static class Logger
{
public static Action<string>? WriteMessage;
public static Severity LogLevel { get; set; } = Severity.Warning;
public static void LogMessage(Severity s, string component, string msg)
{
if (s < LogLevel)
return;
var outputMsg = $"{DateTime.Now}\t{s}\t{component}\t{msg}";
if (WriteMessage is not null)
WriteMessage(outputMsg);
}
}
Praktek
Anda telah menambahkan fitur baru ke infrastruktur pengelogan. Karena komponen pencatat sangat longgar digabungkan dengan mekanisme output apa pun, fitur baru ini dapat ditambahkan tanpa dampak pada kode apa pun yang menerapkan delegasi pencatat.
Saat Anda terus membangun ini, Anda akan melihat lebih banyak contoh bagaimana kondangan longgar ini memungkinkan fleksibilitas yang lebih besar dalam memperbarui bagian situs tanpa perubahan pada lokasi lain. Bahkan, dalam aplikasi yang lebih besar, kelas output pencatat mungkin berada di perakitan yang berbeda, dan bahkan tidak perlu dibangun kembali.
Membangun Mesin Output Kedua
Komponen Log akan datang dengan baik. Mari kita tambahkan satu mesin output lagi yang mencatat pesan ke file. Ini akan menjadi mesin output yang sedikit lebih terlibat. Ini akan menjadi kelas yang merangkum operasi file, dan memastikan bahwa file selalu ditutup setelah setiap penulisan. Itu memastikan bahwa semua data dibersihkan ke disk setelah setiap pesan dibuat.
Berikut adalah pencatat berbasis file:
public class FileLogger
{
private readonly string logPath;
public FileLogger(string path)
{
logPath = path;
Logger.WriteMessage += LogMessage;
}
public void DetachLog() => Logger.WriteMessage -= LogMessage;
// make sure this can't throw.
private void LogMessage(string msg)
{
try
{
using (var log = File.AppendText(logPath))
{
log.WriteLine(msg);
log.Flush();
}
}
catch (Exception)
{
// Hmm. We caught an exception while
// logging. We can't really log the
// problem (since it's the log that's failing).
// So, while normally, catching an exception
// and doing nothing isn't wise, it's really the
// only reasonable option here.
}
}
}
Setelah membuat kelas ini, Anda dapat membuat instans dan melampirkan metode LogMessage-nya ke komponen Pencatat:
var file = new FileLogger("log.txt");
Keduanya tidak sama-sama ekslusif. Anda dapat melampirkan metode log dan menghasilkan pesan ke konsol dan file:
var fileOutput = new FileLogger("log.txt");
Logger.WriteMessage += LoggingMethods.LogToConsole; // LoggingMethods is the static class we utilized earlier
Kemudian, bahkan dalam aplikasi yang sama, Anda dapat menghapus salah satu delegasi tanpa masalah lain ke sistem:
Logger.WriteMessage -= LoggingMethods.LogToConsole;
Praktek
Sekarang, Anda telah menambahkan handler output kedua untuk subsistem pengelogan. Yang satu ini membutuhkan sedikit lebih banyak infrastruktur untuk mendukung sistem file dengan benar. Delegasi adalah metode instans. Ini juga merupakan metode privat. Tidak perlu aksesibilitas yang lebih besar karena infrastruktur delegasi dapat menghubungkan delegasi.
Kedua, desain berbasis delegasi memungkinkan beberapa metode output tanpa kode tambahan. Anda tidak perlu membangun infrastruktur tambahan apa pun untuk mendukung beberapa metode output. Mereka hanya menjadi metode lain dalam daftar pemanggilan.
Beri perhatian khusus pada kode dalam metode output pengelogan file. Ini dikodekan untuk memastikan bahwa itu tidak melemparkan pengecualian apa pun. Meskipun ini tidak selalu diperlukan, ini sering kali merupakan praktik yang baik. Jika salah satu metode delegasi memberikan pengecualian, delegasi yang tersisa yang ada di pemanggilan tidak akan dipanggil.
Sebagai catatan terakhir, pencatat file harus mengelola sumber dayanya dengan membuka dan menutup file pada setiap pesan log. Anda dapat memilih untuk menjaga file tetap terbuka dan menerapkan IDisposable
untuk menutup file ketika Anda selesai.
Setiap metode memiliki kelebihan dan kekurangannya. Keduanya memang membuat sedikit lebih banyak konupling antara kelas.
Tidak ada kode di kelas yang Logger
perlu diperbarui untuk mendukung salah satu skenario.
Menangani Delegasi Null
Terakhir, mari kita perbarui metode LogMessage sehingga kuat untuk kasus tersebut ketika tidak ada mekanisme output yang dipilih. Implementasi saat ini akan melempar ketika NullReferenceException
WriteMessage
delegasi tidak memiliki daftar pemanggilan yang terlampir.
Anda mungkin lebih suka desain yang diam-diam berlanjut ketika tidak ada metode yang terpasang. Ini mudah menggunakan operator bersyarkat null, dikombinasikan Delegate.Invoke()
dengan metode :
public static void LogMessage(string msg)
{
WriteMessage?.Invoke(msg);
}
Operator bersyarat null (?.
) short-circuits ketika operand kiri (WriteMessage
dalam hal ini) null, yang berarti tidak ada upaya yang dilakukan untuk mencatat pesan.
Anda tidak akan menemukan metode yang Invoke()
tercantum dalam dokumentasi untuk System.Delegate
atau System.MulticastDelegate
. Pengkompilasi menghasilkan metode aman Invoke
jenis untuk setiap jenis delegasi yang dideklarasikan. Dalam contoh ini, itu berarti Invoke
mengambil satu string
argumen, dan memiliki jenis pengembalian yang batal.
Ringkasan Praktik
Anda telah melihat awal komponen log yang dapat diperluas dengan penulis lain, dan fitur lainnya. Dengan menggunakan delegasi dalam desain, komponen yang berbeda ini digabungkan secara longgar. Pendekatan ini memberikan beberapa keuntungan. Sangat mudah untuk membuat mekanisme output baru dan melampirkannya ke sistem. Mekanisme lain ini hanya memerlukan satu metode: metode yang menulis pesan log. Ini adalah desain yang tangguh ketika fitur baru ditambahkan. Kontrak yang diperlukan untuk setiap penulis adalah menerapkan satu metode. Metode itu bisa menjadi metode statis atau instance. Ini bisa bersifat publik, pribadi, atau akses hukum lainnya.
Kelas Logger dapat membuat sejumlah peningkatan atau perubahan tanpa memperkenalkan perubahan yang melanggar. Seperti kelas lainnya, Anda tidak dapat memodifikasi API publik tanpa risiko melanggar perubahan. Tapi, karena kopling antara logger dan mesin output hanya melalui delegasi, tidak ada jenis lain (seperti antarmuka atau kelas dasar) yang terlibat. Kopling sekecil mungkin.