Mengapa implementasi standar antarmuka bermanfaat?

Dalam posting terakhir saya , saya berjanji untuk berbicara tentang beberapa kasus di mana, saya pikir, masuk akal untuk mempertimbangkan penggunaan implementasi default di antarmuka. Fitur ini, tentu saja, tidak membatalkan banyak konvensi yang ada untuk menulis kode, tetapi saya menemukan bahwa dalam beberapa situasi menggunakan implementasi standar mengarah ke kode yang lebih bersih dan lebih mudah dibaca (setidaknya menurut saya).

Memperluas Antarmuka dengan Kompatibilitas Mundur


Dokumentasi mengatakan:
Skenario yang paling umum adalah menambahkan metode ke antarmuka dengan aman yang sudah diterbitkan dan digunakan oleh banyak klien.
Masalah yang harus dipecahkan adalah bahwa setiap kelas yang diwarisi dari antarmuka harus menyediakan implementasi untuk metode baru. Ini tidak terlalu sulit ketika antarmuka hanya digunakan oleh kode Anda sendiri, tetapi jika itu ada di perpustakaan umum atau digunakan oleh perintah lain, maka menambahkan elemen antarmuka baru dapat menyebabkan sakit kepala besar.

Pertimbangkan sebuah contoh:

interface ICar { string Make { get; } } public class Avalon : ICar { public string Make => "Toyota"; } 

Jika saya ingin menambahkan metode GetTopSpeed ​​() baru ke antarmuka ini, saya perlu menambahkan implementasinya di Avalon :

 interface ICar { string Make { get; } int GetTopSpeed(); } public class Avalon : ICar { public string Make => "Toyota"; public int GetTopSpeed() => 130; } 

Namun, jika saya membuat implementasi default metode GetTopSpeed ​​() di ICar , maka saya tidak perlu menambahkannya ke setiap kelas yang diwarisi.

 interface ICar { string Make { get; } public int GetTopSpeed() => 150; } public class Avalon : ICar { public string Make => "Toyota"; } 

Jika perlu, saya masih bisa kelebihan implementasi di kelas yang defaultnya tidak cocok:

 interface ICar { string Make { get; } public int GetTopSpeed() => 150; } public class Avalon : ICar { public string Make => "Toyota"; public int GetTopSpeed() => 130; } 

Penting untuk mempertimbangkan bahwa metode default GetTopSpeed ​​() akan tersedia hanya untuk variabel yang dilemparkan ke ICar dan tidak akan tersedia untuk Avalon jika tidak memiliki kelebihan beban. Ini berarti bahwa teknik ini paling berguna jika Anda bekerja dengan antarmuka (jika tidak, kode Anda akan dibanjiri banyak pemain ke antarmuka untuk mendapatkan akses ke implementasi standar metode ini).

Campuran dan sifat (atau sesuatu seperti itu)


Konsep bahasa yang sama dari mixin dan sifat menggambarkan cara untuk memperluas perilaku objek melalui komposisi tanpa perlu pewarisan berganda.

Wikipedia melaporkan hal berikut tentang mixin:
Mixin juga dapat dianggap sebagai antarmuka dengan metode default
Apakah itu terdengar seperti itu?

Namun, meskipun demikian, bahkan dengan implementasi standar, antarmuka dalam C # bukanlah mixin. Perbedaannya adalah bahwa mereka juga dapat berisi metode tanpa implementasi, mendukung warisan dari antarmuka lain, dapat dikhususkan (tampaknya, ini mengacu pada pembatasan template. - approx. Terjemahan.) Dan seterusnya. Namun, jika kita membuat antarmuka yang hanya berisi metode dengan implementasi default, itu sebenarnya akan menjadi mixin tradisional.

Pertimbangkan kode berikut, yang menambahkan fungsionalitas ke objek "gerakan" dan melacak lokasinya (misalnya, dalam pengembang game):

 public interface IMovable { public (int, int) Location { get; set; } public int Angle { get; set; } public int Speed { get; set; } // ,         public void Move() => Location = ...; } public class Car : IMovable { public string Make => "Toyota"; } 

Aduh! Ada masalah dalam kode ini yang tidak saya perhatikan sampai saya mulai menulis posting ini dan mencoba untuk mengkompilasi contoh. Antarmuka (bahkan yang memiliki implementasi default) tidak dapat menyimpan status. Oleh karena itu, antarmuka tidak mendukung properti otomatis. Dari dokumentasi :
Antarmuka tidak dapat menyimpan keadaan instance. Meskipun bidang statis sekarang diizinkan di antarmuka, bidang contoh masih tidak dapat digunakan. Oleh karena itu, Anda tidak dapat menggunakan properti otomatis, karena mereka secara implisit menggunakan bidang tersembunyi.
Dalam antarmuka C # ini bertentangan dengan konsep mixin (sejauh yang saya mengerti, mixin secara konseptual dapat menyimpan keadaan), tetapi kita masih dapat mencapai tujuan awal:

 public interface IMovable { public (int, int) Location { get; set; } public int Angle { get; set; } public int Speed { get; set; } // A method that changes location // using angle and speed public void Move() => Location = ...; } public class Car : IMovable { public string Make => "Toyota"; // ,         public (int, int) Location { get; set; } public int Angle { get; set; } public int Speed { get; set; } } 

Dengan demikian, kami mencapai apa yang kami inginkan dengan membuat metode Move () dan implementasinya tersedia untuk semua kelas yang mengimplementasikan antarmuka IMovable . Tentu saja, kelas masih perlu menyediakan implementasi untuk properti, tetapi setidaknya mereka dideklarasikan di antarmuka IMovable , yang memungkinkan implementasi default Move () untuk bekerja dengannya dan memastikan bahwa setiap kelas yang mengimplementasikan antarmuka memiliki keadaan yang benar.

Sebagai contoh yang lebih lengkap dan praktis, pertimbangkan mixin untuk logging:

 public interface ILogger { public void LogInfo(string message) => LoggerFactory .GetLogger(this.GetType().Name) .LogInfo(message); } public static class LoggerFactory { public static ILogger GetLogger(string name) => new ConsoleLogger(name); } public class ConsoleLogger : ILogger { private readonly string _name; public ConsoleLogger(string name) { _name = name ?? throw new ArgumentNullException(nameof(name)); } public void LogInfo(string message) => Console.WriteLine($"[INFO] {_name}: {message}"); } 

Sekarang di kelas mana pun saya dapat mewarisi dari antarmuka ILogger :

 public class Foo : ILogger { public void DoSomething() { ((ILogger)this).LogInfo("Woot!"); } } 

Dan kode seperti itu:

 Foo foo = new Foo(); foo.DoSomething(); 

Output:

 [INFO] Foo: Woot! 

Mengganti Metode Ekstensi


Aplikasi paling berguna yang saya temukan adalah mengganti sejumlah besar metode ekstensi. Mari kita kembali ke contoh pencatatan sederhana:

 public interface ILogger { void Log(string level, string message); } 

Sebelum implementasi standar muncul di antarmuka, sebagai aturan, saya akan menulis banyak metode ekstensi untuk antarmuka ini, sehingga di kelas yang diwariskan Anda hanya perlu menerapkan satu metode, karena itu pengguna akan memiliki akses ke banyak ekstensi:

 public static class ILoggerExtensions { public static void LogInfo(this ILogger logger, string message) => logger.Log("INFO", message); public static void LogInfo(this ILogger logger, int id, string message) => logger.Log("INFO", $"[{id}] message"); public static void LogError(this ILogger logger, string message) => logger.Log("ERROR", message); public static void LogError(this ILogger logger, int id, string message) => logger.Log("ERROR", $"[{id}] {message}"); public static void LogError(this ILogger logger, Exception ex) => logger.Log("ERROR", ex.Message); public static void LogError(this ILogger logger, int id, Exception ex) => logger.Log("ERROR", $"[{id}] {ex.Message}"); } 

Pendekatan ini bekerja dengan baik, tetapi bukan tanpa cacat. Misalnya, ruang nama kelas dengan ekstensi dan antarmuka tidak selalu cocok. Ditambah suara visual yang mengganggu dalam bentuk parameter dan tautan ke instance logger:

 this ILogger logger logger.Log 

Sekarang saya dapat mengganti ekstensi dengan implementasi default:

 public interface ILogger { void Log(string level, string message); public void LogInfo(string message) => Log("INFO", message); public void LogInfo(int id, string message) => Log("INFO", $"[{id}] message"); public void LogError(string message) => Log("ERROR", message); public void LogError(int id, string message) => Log("ERROR", $"[{id}] {message}"); public void LogError(Exception ex) => Log("ERROR", ex.Message); public void LogError(int id, Exception ex) => Log("ERROR", $"[{id}] {ex.Message}"); } 

Saya menemukan implementasi ini lebih bersih dan lebih mudah dibaca (dan mendukung).

Menggunakan implementasi default juga memiliki beberapa keunggulan dibandingkan metode ekstensi:

  • Dapat menggunakan ini
  • Anda dapat memberikan tidak hanya metode, tetapi juga elemen lain: misalnya, pengindeks
  • Implementasi default mungkin kelebihan beban untuk memperjelas perilaku.

Yang membingungkan saya dalam kode di atas adalah tidak begitu jelas anggota antarmuka mana yang memiliki implementasi default dan yang merupakan bagian dari kontrak yang harus diimplementasikan oleh kelas yang diwarisi. Sebuah komentar yang memisahkan dua blok mungkin membantu, tapi saya suka kejelasan yang ketat tentang metode ekstensi dalam hal ini.

Untuk mengatasi masalah ini, saya mulai mendeklarasikan antarmuka yang memiliki anggota dengan implementasi default sebagai parsial (kecuali mungkin yang sangat sederhana). Kemudian saya meletakkan implementasi default dalam file terpisah dengan konvensi penamaan bentuk "ILogger.LogInfoDefaults.cs" , "ILogger.LogErrorDefaults.cs" dan seterusnya. Jika ada beberapa implementasi standar dan tidak perlu untuk pengelompokan tambahan, maka saya beri nama file "ILogger.Defaults.cs" .

Ini memisahkan anggota dengan implementasi standar dari kontrak yang tidak dapat diimplementasikan, yang diwajibkan untuk diterapkan oleh kelas yang diwariskan. Selain itu, ini memungkinkan Anda untuk memotong file yang sangat panjang. Ada juga trik rumit dengan rendering ASP.NET- gaya file terlampir dalam proyek format apa pun. Untuk melakukan ini, tambahkan ke file proyek atau di Directory.Build.props :

 <ItemGroup> <ProjectCapability Include="DynamicDependentFile"/> <ProjectCapability Include="DynamicFileNesting"/> </ItemGroup> 

Sekarang Anda dapat memilih "File Nesting" di Solution Explorer dan semua file .Defaults.cs Anda akan muncul sebagai turunan dari file antarmuka "utama".

Kesimpulannya, masih ada beberapa situasi di mana metode penyuluhan lebih disukai:

  • Jika Anda biasanya bekerja dengan kelas, bukan antarmuka (karena Anda harus membuang objek ke antarmuka untuk mengakses implementasi standar)
  • Jika Anda sering menggunakan ekstensi dengan templat: public static T SomeExt < T > (T foo ini) (mis. Dalam API Lancar )

Source: https://habr.com/ru/post/id467949/


All Articles