Kode itu hidup dan mati. Bagian tiga. Kode sebagai teks

Anda harus membaca kode untuk menemani program, dan semakin mudah untuk melakukannya, semakin terlihat seperti bahasa alami - maka Anda akan masuk lebih cepat dan fokus pada hal utama.


Dalam dua artikel terakhir, saya menunjukkan bahwa kata-kata yang dipilih dengan cermat membantu untuk lebih memahami esensi dari apa yang ditulis, tetapi hanya memikirkannya saja tidak cukup, karena setiap kata ada dalam dua bentuk: seperti dalam dirinya sendiri dan sebagai bagian dari kalimat. Mengulang CurrentThread belum berulang sampai kita membacanya dalam konteks Thread.CurrentThread .


Dengan demikian, dipandu oleh not dan melodi sederhana, kita sekarang akan melihat apa itu musik.


Siklus daftar isi


  1. Benda-benda
  2. Tindakan dan properti
  3. Kode sebagai teks

Kode sebagai teks


Kebanyakan antarmuka yang lancar dirancang dengan penekanan pada eksternal daripada internal, sehingga sangat mudah dibaca. Tentu saja, tidak gratis: isinya melemah. Jadi, katakanlah, dalam paket FluentAssertions dapat menulis: (2 + 2).Should().Be(4, because: "2 + 2 is 4!") , Dan, relatif terhadap membaca, because terlihat elegan, tetapi di dalam Be() lebih tepatnya, parameter error atau errorMessage .


Menurut pendapat saya, pengecualian seperti itu tidak signifikan. Ketika kami sepakat bahwa kode tersebut adalah teks, komponen-komponennya tidak lagi menjadi milik mereka: mereka sekarang menjadi bagian dari semacam "Eter" universal.


Saya akan menunjukkan dengan contoh bagaimana pertimbangan seperti itu menjadi pengalaman.


Interlocked


Biarkan saya mengingatkan Anda tentang Interlocked.CompareExchange(ref x, newX, oldX) Interlocked , yang kami beralih dari Interlocked.CompareExchange(ref x, newX, oldX) ke Atomically.Change(ref x, from: oldX, to: newX) , menggunakan nama metode dan parameter yang jelas.


ExceptWith


Tipe ISet<> memiliki metode yang disebut ExceptWith . Jika Anda melihat panggilan seperti items.ExceptWith(other) , Anda tidak akan segera menyadari apa yang terjadi. Tapi Anda hanya perlu menulis: items.Exclude(other) , karena semuanya jatuh pada tempatnya.


GetValueOrDefault


Saat bekerja dengan Nullable<T> memanggil x.Value akan mengeluarkan pengecualian jika x adalah null . Jika Anda masih perlu mendapatkan Value , gunakan x.GetValueOrDefault : itu adalah Value atau nilai default. Tebal.


Ekspresi "atau x, atau nilai default" cocok dengan x.OrDefault pendek dan elegan.


 int? x = null; var a = x.GetValueOrDefault(); // ,  .  . var b = x.OrDefault(); //  —  ,   . var c = x.Or(10); //     . 

Dengan OrDefault dan Or ada satu hal yang patut diingat: ketika bekerja dengan operator .? Anda tidak dapat menulis sesuatu seperti x?.IsEnabled.Or(false) , hanya (x?.IsEnabled).Or(false) (dengan kata lain, operator .? Membatalkan seluruh sisi kanan jika null di sebelah kiri).


Templat dapat diterapkan saat bekerja dengan IEnumerable<T> :


 IEnumerable<int> numbers = null; // . var x = numbers ?? Enumerable.Empty<int>(); //   . var x = numbers.OrEmpty(); 

Math.Min dan Math.Max


Sebuah ide dengan Or dapat dikembangkan menjadi tipe numerik. Misalkan Anda ingin mengambil angka maksimum dari a dan b . Kemudian kita menulis: Math.Max(a, b) atau a > b ? a : b a > b ? a : b . Kedua opsi terlihat cukup akrab, tetapi, bagaimanapun, tidak terlihat seperti bahasa alami.


Anda dapat menggantinya dengan: a.Or(b).IfLess() - ambil a atau b jika a kurang . Cocok untuk situasi seperti itu:


 Creature creature = ...; int damage = ...; //   . creature.Health = Math.Max(creature.Health - damage, 0); // Fluent. creature.Health = (creature.Health - damage).Or(0).IfGreater(); //   : creature.Health = (creature.Health - damage).ButNotLess(than: 0); 

string.Join


Terkadang Anda perlu merakit urutan menjadi string, memisahkan elemen dengan spasi atau koma. Untuk melakukan ini, gunakan string.Join , misalnya, seperti ini: string.Join(", ", new [] { 1, 2, 3 }); // "1, 2, 3". string.Join(", ", new [] { 1, 2, 3 }); // "1, 2, 3". .


Sederhana "Bagi nomor koma" tiba-tiba menjadi "Lampirkan koma ke setiap nomor dari daftar" - ini tentu bukan kode sebagai teks.


 var numbers = new [] { 1, 2, 3 }; // ""    —  . var x = string.Join(", ", numbers); //    — ! var x = numbers.Separated(with: ", "); 

Regex


Namun, string.Join sangat tidak berbahaya dibandingkan dengan bagaimana Regex kadang Regex kadang digunakan secara tidak benar dan untuk tujuan lain. Di mana Anda dapat bertahan dengan teks sederhana dan mudah dibaca, karena alasan tertentu entri yang terlalu rumit lebih disukai.


Mari kita mulai dengan yang sederhana - menentukan bahwa string mewakili sekumpulan angka:


 string id = ...; // ,  . var x = Regex.IsMatch(id, "^[0-9]*$"); // . var x = id.All(x => x.IsDigit()); // ! var x = id.IsNumer(); 

Kasus lain adalah untuk mengetahui apakah ada setidaknya satu karakter dalam string dari urutan:


 string text = ...; //   . var x = Regex.IsMatch(text, @"["<>[]'"); //   . ( .) var x = text.ContainsAnyOf('"', '<', '>', '[', ']', '\''); //  . var x = text.ContainsAny(charOf: @"["<>[]'"); 

Semakin rumit tugasnya, semakin sulit solusi "pola": untuk membagi catatan jenis "HelloWorld" menjadi beberapa kata "Hello World" , seseorang bukannya algoritma sederhana menginginkan monster:


 string text = ...; //   -   . var x = Regex.Replace(text, "([az](?=[AZ])|[AZ](?=[AZ][az]))", "$1 "); //  . var x = text.PascalCaseWords().Separated(with: " "); //   . var x = text.AsWords(eachStartsWith: x => x.IsUpper()).Separated(with: " "); 

Tidak diragukan lagi, ekspresi reguler efektif dan universal, tetapi saya ingin memahami apa yang terjadi pada pandangan pertama.


Substring dan Remove


Itu terjadi bahwa Anda perlu menghapus beberapa bagian dari baris dari awal atau akhir, misalnya, dari path - .txt ekstensi, jika ada.


 string path = ...; //    . var x = path.EndsWith(".txt") ? path.Remove(path.Length - "txt".Length) : path; //   . var x = path.Without(".exe").AtEnd; 

Sekali lagi, tindakan dan algoritme hilang, dan garis sederhana dibiarkan tanpa ekstensi .exe di akhir .


Karena metode Without harus mengembalikan WithoutExpression tertentu, mereka memohon untuk yang lain: path.Without("_").AtStart dan path.Without("_").AtStart path.Without("Something").Anywhere . Menarik juga bahwa ungkapan lain dapat dikonstruksikan dengan kata yang sama: name.Without(charAt: 1) - menghapus karakter pada indeks 1 dan mengembalikan baris baru (berguna dalam menghitung permutasi). Dan juga bisa dibaca!


Type.GetMethods


Untuk mendapatkan metode jenis tertentu menggunakan refleksi, gunakan:


 Type type = ...; //   `Get` ,   `|`.     . var x = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); // ,  . `Or`   , . var x = type.Methods(_ => _.Instance.Public.Or.NonPublic); 

(Hal yang sama berlaku untuk GetFields dan GetProperties .)


Directory.Copy


Semua operasi dengan folder dan file sering digeneralisasi ke DirectoryUtils , FileSystemHelper . Mereka menerapkan bypass sistem file, pembersihan, penyalinan, dll. Tetapi di sini Anda dapat menemukan sesuatu yang lebih baik!


Kami menampilkan teks "salin semua file dari 'D: \ Source' ke 'D: \ Target'" ke kode "D:\\Source".AsDirectory().Copy().Files.To("D:\\Target") AsDirectory() - mengembalikan DirectoryInfo dari string , dan Copy() - membuat instance CopyExpression yang menjelaskan API unik untuk membangun ekspresi (Anda tidak dapat memanggil Copy().Files.Files , misalnya). Maka kesempatan terbuka untuk menyalin tidak semua file, tetapi beberapa: Copy().Files.Where(x => x.IsNotEmpty) .


GetOrderById


Pada artikel kedua, saya menulis bahwa IUsersRepository.GetUser(int id) berlebihan, dan lebih baik, IUsersRepository.User(int id) . Dengan demikian, dalam IOrdersRepository serupa IOrdersRepository kami belum GetOrderById(int id) , tetapi Order(int id) . Namun, dalam contoh lain, disarankan agar variabel repositori seperti itu disebut bukan _ordersRepository , tetapi hanya _orders .


Kedua perubahan itu baik pada mereka sendiri, tetapi mereka tidak dijumlahkan bersama dalam konteks membaca: menelepon _orders.Order(id) terlihat bertele-tele. Mungkin untuk _orders.Get(id) , tetapi pesanan gagal, kami hanya ingin menentukan yang memiliki pengidentifikasi seperti itu . Satu itu adalah One , oleh karena itu:


 IOrdersRepository orders = ...; int id = ...; //   . var x = orders.GetOrderById(id); //      : var x = orders.Order(id); //     ,    . var x = orders.One(id); //    : var x = orders.One(with: id); 

GetOrders


Dalam objek seperti IOrdersRepository , metode lain sering ditemukan: AddOrder , RemoveOrder , GetOrders . Dua pengulangan pertama hilang, dan Add dan Remove diperoleh (dengan entri _orders.Add(order) dan _orders.Remove(order) ) yang sesuai. Dengan GetOrders agak sulit untuk mengubah nama Orders sedikit. Mari kita lihat:


 IOrdersRepository orders = ...; //   . var x = orders.GetOrders(); //  `Get`,  . var x = orders.Orders(); // ! var x = orders.All(); 

Perlu dicatat bahwa dengan _ordersRepository lama _ordersRepository pengulangan di GetOrders atau GetOrderById tidak begitu terlihat, karena kami bekerja dengan repositori!


Nama seperti One , All cocok untuk banyak antarmuka yang mewakili banyak. Katakanlah, dalam implementasi API GitHub yang terkenal - octokit - mendapatkan semua repositori pengguna terlihat seperti gitHub.Repository.GetAllForUser("John") , meskipun lebih logis - gitHub.Users.One("John").Repositories.All . Dalam hal ini, mendapatkan satu repositori akan menjadi, masing-masing, gitHub.Repository.Get("John", "Repo") alih-alih gitHub.Users.One("John").Repositories.One("Repo") jelas. gitHub.Users.One("John").Repositories.One("Repo") . Kasus kedua terlihat lebih panjang, tetapi konsisten secara internal dan mencerminkan platform. Selain itu, dengan menggunakan metode ekstensi, dapat disingkat menjadi gitHub.User("John").Repository("Repo") .


Dictionary.TryGetValue


Memperoleh nilai dari kamus dibagi menjadi beberapa skenario yang hanya berbeda dalam apa yang perlu dilakukan jika kunci tidak ditemukan:


  • melempar kesalahan ( dictionary[key] );
  • mengembalikan nilai default (tidak diterapkan, tetapi sering menulis GetValueOrDefault atau TryGetValue );
  • kembalikan sesuatu yang lain (tidak diterapkan, tapi saya harapkan GetValueOrOther );
  • tulis nilai yang ditentukan ke kamus dan kembalikan (tidak diterapkan, tetapi GetOrAdd ).

Ekspresi bertemu pada titik " ambil X, atau Y jika X tidak ." Selain itu, seperti dalam kasus _ordersRepository , kita akan memanggil variabel kamus bukan itemsDictionary , tetapi items .


Kemudian untuk bagian "ambil beberapa X" , panggilan items.One(withKey: X) formulir. items.One(withKey: X) ideal, mengembalikan struktur dengan empat ujung :


 Dictionary<int, Item> items = ...; int id = ...; //  ,   : var x = items.GetValueOrDefault(id); var x = items[id]; var x = items.GetOrAdd(id, () => new Item()); //    : var x = items.One(with: id).OrDefault(); var x = items.One(with: id).Or(Item.Empty); var x = items.One(with: id).OrThrow(withMessage: $"Couldn't find item with '{id}' id."); var x = items.One(with: id).OrNew(() => new Item()); 

Assembly.GetTypes


Mari kita lihat cara membuat semua contoh tipe T dalam perakitan:


 // . var x = Assembly .GetAssembly(typeof(T)) .GetTypes() .Where(...) .Select(Activator.CreateInstance); // "" . var x = TypesHelper.GetAllInstancesOf<T>(); // . var x = Instances.Of<T>(); 

Jadi, kadang-kadang, nama kelas statis adalah awal dari sebuah ekspresi.


Sesuatu yang serupa dapat ditemukan di NUnit: Assert.That(2 + 2, Is.EqualTo(4)) - Is dan tidak dipahami sebagai tipe mandiri.


Argument.ThrowIfNull


Sekarang lihat pada pemeriksaan prasyarat:


 //  . Argument.ThrowIfNull(x); Guard.CheckAgainstNull(x); // . x.Should().BeNotNull(); // ,  ...  ? Ensure(that: x).NotNull(); 

Ensure.NotNull(argument) - bagus, tetapi tidak cukup bahasa Inggris. Hal lain adalah Ensure(that: x).NotNull() ditulis di atas. Kalau saja ada ...


Ngomong-ngomong, kamu bisa! Kami menulis Contract.Ensure(that: argument).IsNotNull() dan impor jenis Contract using static . Jadi segala macam Ensure(that: type).Implements<T>() , Ensure(that: number).InRange(from: 5, to: 10) , dll Ensure(that: number).InRange(from: 5, to: 10) .


Gagasan impor statis membuka banyak pintu. Contoh yang bagus untuk kepentingan: alih-alih Remove(x, from: items) . Remove(x, from: items) items.Remove(x) tulis Remove(x, from: items) . Namun rasa ingin tahu adalah pengurangan enum dan properti yang mengembalikan fungsi.


 IItems items = ...; // . var x = items.All(where: x => x.IsWeapon); //  . // `ItemsThatAre.Weapons`  `Predicate<bool>`. var x = items.All(ItemsThatAre.Weapons); // `using static`  !  . var x = items.All(Weapons); 

Find Eksotis


Dalam C # 7.1 dan lebih tinggi, Anda dapat menulis bukan Find(1, @in: items) , tetapi Find(1, in items) , di mana Find didefinisikan sebagai Find<T>(T item, in IEnumerable<T> items) . Contoh ini tidak praktis, tetapi menunjukkan bahwa semua sarana bagus dalam perjuangan untuk keterbacaan.


Total


Pada bagian ini, saya melihat beberapa cara untuk bekerja dengan keterbacaan kode. Semuanya dapat digeneralisasi ke:


  • Parameter bernama sebagai bagian dari ekspresi adalah Should().Be(4, because: "") , Atomically.Change(ref x, from: oldX, to: newX) . Atomically.Change(ref x, from: oldX, to: newX) .
  • Nama sederhana alih-alih detail teknis Separated(with: ", ") , Exclude .
  • Metode sebagai bagian dari variabel adalah x.OrDefault() , x.Or(b).IfLess() , orders.One(with: id) , orders.All .
  • Metode sebagai bagian dari ekspresi adalah path.Without(".exe").AtEnd .
  • Jenis sebagai bagian dari ekspresi adalah Instances.Of , Is.EqualTo .
  • Metode sebagai bagian dari ekspresi ( using static ) adalah Ensure(that: x) , items.All(Weapons) .

Dengan demikian, yang eksternal dan yang direnungkan dibawa ke permukaan. Mula-mula ia dianggap, dan kemudian inkarnasi spesifiknya dipikirkan, tidak begitu penting, selama kode itu dibaca sebagai teks. Oleh karena itu, juri tidak begitu menyukai bahasa - ia menentukan perbedaan antara item.GetValueOrDefault dan item.OrDefault .


Epilog


Mana yang lebih baik, jelas, tetapi bukan metode kerja, atau bekerja, tetapi tidak bisa dipahami? Kastil berwarna putih salju tanpa perabotan dan kamar atau gudang dengan gaya Louis XIV? Kapal pesiar mewah tanpa mesin atau tongkang mengerang dengan komputer kuantum yang tidak dapat digunakan siapa pun?


Jawaban kutub tidak cocok, tetapi "di suatu tempat di tengah" juga.


Menurut pendapat saya, kedua konsep itu tidak dapat dipisahkan: dengan hati-hati memilih sampul buku, kami ragu-ragu melihat kesalahan dalam teks, dan sebaliknya. Saya tidak ingin The Beatles memainkan musik berkualitas rendah, tetapi mereka juga harus disebut MusicHelper .


Hal lain adalah bahwa mengerjakan kata sebagai bagian dari proses pembangunan adalah hal yang diremehkan, hal yang tidak biasa, dan oleh karena itu semacam penilaian ekstrem masih diperlukan. Siklus ini adalah bentuk dan gambar yang ekstrem.


Terima kasih atas perhatian Anda!


Referensi


Siapa pun yang tertarik melihat lebih banyak contoh dapat ditemukan di GitHub saya, misalnya, di perpustakaan Pocket.Common . (tidak untuk penggunaan di seluruh dunia dan universal)

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


All Articles