الكود حي ومات. الجزء الثالث. الرمز كنص

لمرافقة البرنامج ، يجب عليك قراءة الكود ، وكلما كان القيام به أسهل ، كلما بدا الأمر وكأنه لغة طبيعية - ثم سرعان ما انتقلت إلى الموضوع الرئيسي وركزت عليه.


في المادتين الأخيرتين ، أوضحت أن الكلمات المختارة بعناية تساعد على فهم جوهر ما هو مكتوب بشكل أفضل ، لكن التفكير فيها فقط لا يكفي ، لأن كل كلمة موجودة في شكلين: كما في حد ذاتها وكجزء من الجملة. لم يتم تكرار CurrentThread بعد حتى نقرأه في سياق Thread.CurrentThread .


وبالتالي ، بالاسترشاد بالملاحظات والألحان البسيطة ، سنرى الآن ماهية الموسيقى.


جدول دورة المحتويات


  1. الكائنات
  2. الإجراءات والخصائص
  3. الرمز كنص

الرمز كنص


تم تصميم معظم الواجهات بطلاقة مع التركيز على الخارج بدلاً من الداخلية ، لذلك فهي سهلة القراءة. بالطبع ، ليس مجانًا: المحتوى ضعيف إلى حد ما. لذلك ، دعنا نقول ، في حزمة FluentAssertions كتابة: (2 + 2).Should().Be(4, because: "2 + 2 is 4!") ، ونسبة إلى القراءة ، because تبدو أنيقة ، ولكن داخل Be() بدلاً من ذلك ، error أو معلمة errorMessage .


في رأيي ، هذه الإعفاءات ليست كبيرة. عندما نتفق على أن الكود عبارة عن نص ، فإن مكوناته تتوقف عن الانتماء لأنفسهم: فهي الآن جزء من نوع من "الأثير" العالمي.


سأبين بأمثلة كيف تصبح هذه الاعتبارات تجربة.


Interlocked


اسمحوا لي أن أذكرك بحالة Interlocked ، التي Interlocked.CompareExchange(ref x, newX, oldX) من Interlocked.CompareExchange(ref x, newX, oldX) إلى Interlocked.CompareExchange(ref x, newX, oldX) Atomically.Change(ref x, from: oldX, to: newX) ، باستخدام أسماء واضحة للطرائق والمعلمات.


ExceptWith


اكتب ISet<> يحتوي على طريقة تسمى ExceptWith . إذا نظرت إلى مكالمة مثل items.ExceptWith(other) ، فلن تدرك على الفور ما يحدث. لكن عليك فقط أن تكتب: items.Exclude(other) ، لأن كل شيء يقع في مكانه.


GetValueOrDefault


عند العمل مع Nullable<T> استدعاء x.Value سيؤدي إلى استثناء إذا كانت x null . إذا كنت لا تزال بحاجة إلى الحصول على Value ، استخدم x.GetValueOrDefault : إنها Value أو Value الافتراضية. مرهقة.


يطابق التعبير "أو x أو القيمة الافتراضية" x.OrDefault القصيرة والأنيقة.


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

مع OrDefault و Or هناك شيء واحد يستحق التذكر: عند العمل مع مشغل .? لا يمكنك كتابة شيء مثل x?.IsEnabled.Or(false) ، فقط (x?.IsEnabled).Or(false) (بمعنى آخر ، يقوم المشغل .? بإلغاء الجانب الأيمن بأكمله إذا كانت null في اليسار).


يمكن تطبيق القالب عند العمل مع IEnumerable<T> :


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

Math.Min و Math.Max


يمكن تطوير فكرة باستخدام Or إلى أنواع رقمية. افترض أنك تريد أن تأخذ العدد الأقصى من a و b . ثم نكتب: Math.Max(a, b) أو a > b ? a : b a > b ? a : b . يبدو كلا الخيارين مألوفًا تمامًا ، لكن لا يبدو أنه لغة طبيعية.


يمكنك استبدالها بـ: a.Or(b).IfLess() - خذ a أو b إذا كان a أقل . مناسبة لمثل هذه الحالات:


 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


في بعض الأحيان تحتاج إلى تجميع تسلسل في سلسلة ، مع فصل العناصر بمسافة أو فاصلة. للقيام بذلك ، استخدم string.Join ، على سبيل المثال ، مثل هذا: string.Join(", ", new [] { 1, 2, 3 }); // "1, 2, 3". string.Join(", ", new [] { 1, 2, 3 }); // "1, 2, 3". .


قد يصبح "تقسيم رقم الفاصلة" بسيطًا فجأة "أرفق فاصلة لكل رقم من القائمة" - وهذا بالتأكيد ليس رمزًا كنص.


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

Regex


ومع ذلك ، string.Join غير ضار جدًا مقارنة بكيفية استخدام Regex أحيانًا بشكل غير صحيح ولأغراض أخرى. حيث يمكنك الحصول على نص بسيط يمكن قراءته ، لسبب ما ، يفضل إدخال معقد للغاية.


لنبدأ بسلسلة بسيطة - تحديد أن السلسلة تمثل مجموعة من الأرقام:


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

حالة أخرى هي معرفة ما إذا كان هناك حرف واحد على الأقل في السلسلة من التسلسل:


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

كلما كانت المهمة أكثر تعقيدًا ، زاد صعوبة "نمط" الحل: تقسيم سجل من "HelloWorld" إلى بضع كلمات "Hello World" ، أراد شخص ما بدلاً من خوارزمية بسيطة وجود وحش:


 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: " "); 

لا شك أن التعبيرات المنتظمة فعالة وعالمية ، لكنني أريد أن أفهم ما يحدث للوهلة الأولى.


Substring Remove


يحدث أنك تحتاج إلى إزالة جزء من السطر من البداية أو النهاية ، على سبيل المثال ، من path - الامتداد .txt ، إن وجد.


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

مرة أخرى ، اختفت الحركة والخوارزمية ، وترك سطر بسيط بدون ملحق. exe في النهاية .


نظرًا لأن الأسلوب " Without يجب أن WithoutExpression معينة ، فإنهم يتوسلون لآخر: path.Without("_").AtStart و path.Without("Something").Anywhere . ومن المثير للاهتمام أيضًا أنه يمكن إنشاء تعبير آخر بنفس الكلمة: name.Without(charAt: 1) - يحذف الحرف في الفهرس 1 ويعيد سطرًا جديدًا (مفيد في حساب التباديل). وقراءة أيضا!


Type.GetMethods


للحصول على طرق من نوع معين باستخدام الانعكاس ، استخدم:


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

(نفس الشيء ينطبق على GetFields و GetProperties .)


Directory.Copy


غالبًا ما يتم تعميم جميع العمليات مع المجلدات والملفات على DirectoryUtils ، FileSystemHelper . إنهم يقومون بتطبيق نظام الملفات الالتفافية ، التنظيف ، النسخ ، إلخ. ولكن هنا يمكنك التوصل إلى شيء أفضل!


نعرض النص "نسخ جميع الملفات من" D: \ Source "إلى" D: \ Target "" إلى الكود "D:\\Source".AsDirectory().Copy().Files.To("D:\\Target") . AsDirectory() - بإرجاع DirectoryInfo من string ، و Copy() - ينشئ مثيل لـ CopyExpression الذي يصف واجهة برمجة تطبيقات فريدة لإنشاء التعبيرات (لا يمكنك استدعاء Copy().Files.Files ، على سبيل المثال). ثم تفتح الفرصة لنسخ ليس كل الملفات ، ولكن بعضها: Copy().Files.Where(x => x.IsNotEmpty) .


GetOrderById


في المقالة الثانية ، كتبت أن IUsersRepository.GetUser(int id) لا لزوم لها ، وأفضل ، IUsersRepository.User(int id) . وفقًا لذلك ، في IOrdersRepository مماثل IOrdersRepository لم نحصل على GetOrderById(int id) ، ولكن Order(int id) . ومع ذلك ، في مثال آخر ، اقترح أن يسمى متغير هذا المستودع ليس _ordersRepository ، ولكن ببساطة _orders .


كلا التغييرين جيدان من تلقاء نفسه ، لكنهما لا يضيفان معاً في سياق القراءة: استدعاء _orders.Order(id) يبدو مطوّلًا. قد يكون من الممكن _orders.Get(id) ، لكن الطلبات تفشل ، نريد فقط تحديد الأمر الذي لديه مثل هذا المعرف . واحد هو One ، وبالتالي:


 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


في كائنات مثل IOrdersRepository ، غالبًا ما توجد طرق أخرى: AddOrder و RemoveOrder و GetOrders . يذهب التكرار الأولين ، ويتم الحصول على Add Remove (مع الإدخالات المقابلة _orders.Add(order) و _orders.Remove(order) ). مع GetOrders الصعب إعادة تسمية Orders قليلاً. لنرى:


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

تجدر الإشارة إلى أنه مع _ordersRepository القديمة _ordersRepository التكرار في GetOrders أو GetOrderById المكالمات ليست ملحوظة للغاية ، لأننا نعمل مع مستودع!


أسماء مثل One ، All مناسبة للعديد من الواجهات التي تمثل الكثير. لنفترض أن تطبيق جميع مستودعات المستخدم يشبه gitHub.Repository.GetAllForUser("John") تطبيق معروف لـ GitHub API - octokit - على الرغم من أنه أكثر منطقية - gitHub.Users.One("John").Repositories.All . في هذه الحالة ، سيكون الحصول على مستودع واحد ، على التوالي ، gitHub.Repository.Get("John", "Repo") بدلاً من gitHub.Users.One("John").Repositories.One("Repo") الواضح gitHub.Users.One("John").Repositories.One("Repo") . تبدو الحالة الثانية أطول ، ولكنها متسقة داخليًا وتعكس المنصة. بالإضافة إلى ذلك ، باستخدام طرق التمديد ، يمكن اختصارها إلى gitHub.User("John").Repository("Repo") .


Dictionary.TryGetValue


ينقسم الحصول على القيم من القاموس إلى عدة سيناريوهات تختلف فقط في ما يجب القيام به إذا لم يتم العثور على المفتاح:


  • رمي خطأ ( dictionary[key] ) ؛
  • إرجاع القيمة الافتراضية (غير GetValueOrDefault ، ولكن غالبًا ما تكتب GetValueOrDefault أو TryGetValue ) ؛
  • إرجاع شيء آخر (غير مطبق ، ولكن أتوقع GetValueOrOther ) ؛
  • اكتب القيمة المحددة في القاموس وإعادتها (لم يتم تنفيذها ، ولكن GetOrAdd ).

تتلاقى التعبيرات عند النقطة " تأخذ بعض X أو Y إذا لم تكن X ". بالإضافة إلى ذلك ، كما هو الحال في _ordersRepository ، سوف ندعو متغير القاموس ليس itemsDictionary ، ولكن items .


ثم بالنسبة إلى الجزء "take some X" ، فإن استدعاء عناصر النموذج. items.One(withKey: X) مثاليًا ، حيث يُرجع بنية بأربعة نهايات :


 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


لنلقِ نظرة على إنشاء كافة مثيلات النوع T الموجودة في التجميع:


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

وبالتالي ، في بعض الأحيان ، يكون اسم فئة ثابتة هو بداية التعبير.


يمكن العثور على شيء مشابه في NUnit: Assert.That(2 + 2, Is.EqualTo(4)) - Is لم يتم تصوره كنوع Assert.That(2 + 2, Is.EqualTo(4)) ذاتيًا.


Argument.ThrowIfNull


الآن دعونا نلقي نظرة على الاختيار المسبق:


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

Ensure.NotNull(argument) - لطيفة ، ولكن ليس الإنجليزية تماما. شيء آخر هو Ensure(that: x).NotNull() مكتوب أعلاه. إذا كان هناك فقط يمكن ...


بالمناسبة ، يمكنك! نكتب Contract.Ensure(that: argument).IsNotNull() واستيراد نوع Contract using static . لذا ، يتم الحصول على كل أنواع " Ensure(that: type).Implements<T>() من Ensure(that: type).Implements<T>() ، Ensure(that: number).InRange(from: 5, to: 10) ، وما إلى ذلك.


فكرة الاستيراد الثابت تفتح العديد من الأبواب. مثال جميل من أجل: بدلاً من items.Remove(x) اكتب Remove(x, from: items) . ولكن الغريب هو الحد من enum والخصائص التي ترجع الوظائف.


 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 الغريبة


في Find(1, @in: items) C # 7.1 والإصدارات الأحدث ، يمكنك كتابة ليس Find(1, @in: items) ، ولكن Find(1, in items) ، حيث يتم تعريف Find<T>(T item, in IEnumerable<T> items) أنه Find<T>(T item, in IEnumerable<T> items) . هذا المثال غير عملي ، لكنه يظهر أن جميع الوسائل جيدة في النضال من أجل القراءة.


في المجموع


في هذا الجزء ، نظرت إلى عدة طرق للعمل مع إمكانية قراءة التعليمات البرمجية. يمكن تعميمها جميعًا على:


  • المعلمة المسماة كجزء من التعبير هي Should().Be(4, because: "") ، Atomically.Change(ref x, from: oldX, to: newX) .
  • اسم بسيط بدلاً من التفاصيل الفنية Separated(with: ", ") ، Exclude .
  • الطريقة كجزء من المتغير هي x.OrDefault() ، x.Or(b).IfLess() ، orders.One(with: id) ، orders.All .
  • الطريقة كجزء من التعبير هي path.Without(".exe").AtEnd .
  • النوع كجزء من التعبير هو Instances.Of ، Is.EqualTo .
  • الطريقة كجزء من التعبير ( using static ) هي Ensure(that: x) ، items.All(Weapons) .

وبالتالي ، يتم وضع الخارجية والمفكر في الصدارة. في البداية يتم التفكير فيه ، وبعد ذلك يتم التفكير في تجسيده المحدد ، وليس مهمًا ، طالما يتم قراءة الرمز كنص. ويترتب على ذلك أن القاضي ليس هو المذاق بقدر اللغة - فهو يحدد الفرق بين item.GetValueOrDefault و item.OrDefault .


خاتمة


أيهما أفضل ، أو غير واضحة ، أو غير صالحة للعمل ، أو غير مفهومة؟ قلعة بيضاء الثلج بدون أثاث وغرف أو سقيفة مع أرائك على طراز لويس الرابع عشر؟ يخت فاخر بدون محرك أو بارجة يئن بها كمبيوتر كمومي لا يمكن لأحد استخدامه؟


الإجابات القطبية لا تناسب ، ولكن "في مكان ما في الوسط" أيضا.


في رأيي ، كلا المفهومين لا ينفصمان: باختيار غلاف كتاب بعناية ، فنحن بلا شك ننظر إلى الأخطاء في النص ، والعكس صحيح. لا أريد أن تقوم فرقة البيتلز بتشغيل موسيقى منخفضة الجودة ، ولكن يجب أيضًا أن تسمى MusicHelper .


شيء آخر هو أن العمل على كلمة كجزء من عملية التطوير هو شيء أقل من تقديره ، وهو أمر غير عادي ، وبالتالي لا تزال هناك حاجة إلى نوع من الحكم الشديد. هذه الدورة هي أقصى شكل وصورة.


شكرا لكم جميعا على اهتمامكم!


مراجع


يمكن العثور على أي شخص مهتم برؤية المزيد من الأمثلة على GitHub ، على سبيل المثال ، في مكتبة Pocket.Common . (ليس للاستخدام العالمي والعالمي)

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


All Articles