コードは生きていて死んでいます。 パート3。 テキストとしてコード

プログラムに付随するには、コードを読む必要があります。コードを読むのが簡単であればあるほど、自然言語のように見えます。それから、すぐに主なものを掘り下げて集中します。


最後の2つの記事では、慎重に選択された単語が書かれている内容の本質をよりよく理解するのに役立つことを示しましたが、すべての単語はそれ自体と文の一部の2つの形式で存在するため、それらについてのみ考えるだけでは不十分です。 Thread.CurrentThreadのコンテキストで読み取るまで、 CurrentThread繰り返しはまだ繰り返されていません。


したがって、音符とシンプルなメロディーに導かれ、音楽とは何かを見ることになります。


サイクルの目次


  1. オブジェクト
  2. アクションとプロパティ
  3. テキストとしてコード

テキストとしてコード


最も流fluentなインターフェイスは、内部よりも外部に重点を置いて設計されているため、読みやすくなっています。 もちろん、無料ではありません。コンテンツはある意味で弱くなっています。 だから、 FluentAssertionsパッケージにFluentAssertionsように書くFluentAssertionsができるとしFluentAssertionsう: (2 + 2).Should().Be(4, because: "2 + 2 is 4!") 、そして、読書に比べて、見た目はエレガントですが、 Be()むしろ、 errorまたはerrorMessageパラメーターがerrorMessageです。


私の意見では、そのような免除は重要ではありません。 コードがテキストであることに同意すると、そのコンポーネントはそれ自体に属しなくなります。それらは現在、一種の普遍的な「エーテル」の一部です。


このような考慮事項がどのように経験になるかを例で示します。


Interlocked


メソッドとパラメーターの明確な名前を使用して、 Interlocked.CompareExchange(ref x, newX, oldX) Atomically.Change(ref x, from: oldX, to: newX)に変更したInterlockedのケースを思い出させてください。


ExceptWith


タイプISet<>ISet<>というメソッドがExceptWithます。 items.ExceptWith(other)ような呼び出しを見ると、何が起きているかすぐにはitems.ExceptWith(other)ません。 ただし、すべて記述する必要があるため、 items.Exclude(other)を記述する必要があります。


GetValueOrDefault


Nullable<T>x.Valueする場合、 xnull場合、 x.Valueを呼び出すと例外がスローされnull 。 それでもValueを取得する必要がある場合x.GetValueOrDefaultx.GetValueOrDefault使用されます。これはValueまたはデフォルト値のいずれかです。 かさばる。


「またはx、またはデフォルト値」という表現は、短くエレガントなx.OrDefaultます。


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

OrDefaultOr覚えておく価値のあることが1つあり.?演算子を使用するときは.? x?.IsEnabled.Or(false)(x?.IsEnabled).Or(false)ようなものを書くことはできません(言い換えると、 null左側にある場合、 .?演算子は右側全体をキャンセルしnull )。


IEnumerable<T>使用する場合、テンプレートを適用できます。


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

Math.MinおよびMath.Min


Or使用したアイデアは、数値型に発展させることができます。 abから最大数を取得するとします。 それからMath.Max(a, b)またはa > b ? a : b a > b ? a : b どちらのオプションも非常によく似ていますが、それでも、自然言語のようには見えません。


次のように置き換えることができます: a.Or(b).IfLess() - b より小さい 場合 a b または b 取ります。 そのような状況に適しています:


 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 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誤って他の目的で使用されることRegexあるのに比べて、まったく無害です。 シンプルで読みやすいテキストでうまくいく場合は、何らかの理由で、複雑すぎるエントリが推奨されます。


簡単なものから始めましょう-文字列が数字のセットを表すことを決定します:


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

別のケースは、シーケンスの文字列に少なくとも1つの文字があるかどうかを調べることです。


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

間違いなく、正規表現は効果的で普遍的ですが、一目で何が起こっているのかを理解したいと思います。


SubstringRemove


行の先頭または末尾から一部を削除する必要がある場合があります。たとえば、 .txt拡張子.txt (ある場合)などです。


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

繰り返しになりますがアクションアルゴリズムはなくなり、 最後に.exe拡張子のない単純な行が残されました


Withoutメソッドは特定のWithoutExpressionを返す必要があるWithoutExpressionpath.Without("_").AtStartおよびpath.Without("Something").Anywhereさらに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); 

GetFieldsGetPropertiesについても同じことがGetPropertiesます。)


Directory.Copy


多くの場合、フォルダーとファイルの操作はすべてDirectoryUtilsFileSystemHelper一般化されFileSystemHelper 。 ファイルシステムのバイパス、クリーニング、コピーなどを実装します。 しかし、ここでもっと良いものを思いつくことができます!


「すべてのファイルを「D:\ Source」から「D:\ Target」にコピー」というテキストをコード"D:\\Source".AsDirectory().Copy().Files.To("D:\\Target") AsDirectory() - stringからDirectoryInfoを返し、 Copy() -式を作成するための一意のAPIを記述するCopyExpressionインスタンスを作成します( Copy().Files.Files呼び出すことはできませんCopy().Files.Filesなど)。 次に、すべてのファイルではなく、いくつかのファイルをコピーする機会が開きます: Copy().Files.Where(x => x.IsNotEmpty)


GetOrderById


2番目の記事では、 IUsersRepository.GetUser(int id)は冗長であり、より良いのはIUsersRepository.User(int id)であるとIUsersRepository.User(int id) 。 したがって、同様のIOrdersRepositoryIOrdersRepository GetOrderById(int id)ではなくOrder(int id)ます。 ただし、別の例では、そのようなリポジトリの変数は_ordersRepositoryではなく、単に_ordersと呼ばれることが提案され_orders


両方の変更はそれ自体で有効ですが、読み取りコンテキストでは_orders.Order(id)されません_orders.Order(id)呼び出しは冗長に見えます。 _orders.Get(id)は可能ですが、注文は失敗します。 そのような識別子を持つもののみを指定します。 したがって、 OneOneです。


 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などのオブジェクトでは、他のメソッドAddOrderRemoveOrderGetOrdersがよく見られます。 最初の2回の繰り返しはなくなり、 AddおよびRemoveが取得されます_orders.Add(order)対応するエントリ_orders.Add(order)および_orders.Remove(order) )。 GetOrdersを使用すると、 Orders名前を少し変更するGetOrdersが難しくなります。 見てみましょう:


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

古い_ordersRepository 、リポジトリで作業しているため、 GetOrdersまたはGetOrderById呼び出しの繰り返しはそれほど目立たないことに注意してください。


OneAllなどの名前は、多くを表す多くのインターフェースに適しています。 よく知られているGitHub APIの実装gitHub.Repository.GetAllForUser("John")では、すべてのユーザーリポジトリの取得はgitHub.Repository.GetAllForUser("John")ように見えますが、より論理的ですgitHub.Users.One("John").Repositories.All この場合、 1つのリポジトリを取得すると、それぞれ、明らかなgitHub.Users.One("John").Repositories.One("Repo")ではなく、 gitHub.Repository.Get("John", "Repo")になりgitHub.Users.One("John").Repositories.One("Repo") 。 2番目のケースは長く見えますが、内部的に一貫性があり、プラットフォームを反映しています。 さらに、拡張メソッドを使用して、 gitHub.User("John").Repository("Repo")短縮できます。


Dictionary.TryGetValue


辞書から値を取得することは、キーが見つからない場合に必要ことだけが異なるいくつかのシナリオに分けられます。



XがXでない場合、XまたはYを取ります」という点で収束します。 さらに、 _ordersRepositoryの場合のように、ディクショナリ変数をitemsDictionaryではなくitemsと呼びitems


次に、 「Xを取る」部分では、 items.One(withKey: X)形式の呼び出しitems.One(withKey: X)理想的で、4つの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)) -自給自足型として考えられていました。


Argument.ThrowIfNull


次に、前提条件チェックを見てみましょう。


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

Ensure.NotNull(argument) -素晴らしいですが、完全に英語ではありません。 もう1つは、上記のEnsure(that: x).NotNull()です。 そこにしかできなかったら...


ところで、できます! Contract.Ensure(that: argument).IsNotNull()を記述using staticContractタイプusing staticインポートusing static 。 したがって、すべての種類のEnsure(that: type).Implements<T>()Ensure(that: number).InRange(from: 5, to: 10)などが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


C#7.1以降では、 Find(1, @in: items)ではなくFind(1, in items)を記述できFind(1, @in: items)Find<T>(T item, in IEnumerable<T> items)として定義されていFind<T>(T item, in IEnumerable<T> items) 。 この例は実用的ではありませんが、すべての手段が読みやすさのために苦労していることを示しています。


合計


このパートでは、コードを読みやすくするためのいくつかの方法を検討しました。 それらはすべて一般化できます:



したがって、外部および想定されるものが前面に表示されます。 最初に考えられ、コードがテキストとして読み取られる限り、その具体的な化身が考えられますが、それほど重要ではありません。 裁判官は言語ほど味ではないということになる-彼はitem.GetValueOrDefaultitem.OrDefault違いを決定します。


エピローグ


どちらが良い、明確であるが、動作する方法ではないか、動作しているが理解できないか? 家具や部屋のない雪のように白い城、またはルイ14世のスタイルのソファのある小屋? エンジンのない贅沢なヨット、または誰も使用できない量子コンピューターを搭載したうめき台船?


Polarの回答は適合しませんが、 「中間のどこかに」もあります。


私の意見では、両方の概念は密接に結びついています。本の表紙を慎重に選択し、テキストの誤りを疑いで調べ、その逆も同様です。 ビートルズに低品質の音楽再生させたくありませんが、 MusicHelperと呼ばれるべきです。


別のことは、開発プロセスの一部として単語に取り組むことは過小評価された異常なことであり、したがって、何らかの極端な判断が依然として必要であるということです。 このサイクルは、フォームと画像の極端です。


ご清聴ありがとうございました!


参照資料


もっと多くの例を見ることに興味がある人は、私のGitHubで、例えばPocket.Commonライブラリで見つけることができます。 (世界的および普遍的な使用ではありません)



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


All Articles