ソフトウェアは常に複雑になっています。 アプリケーション拡張機能の安定性とシンプルさは、コードの品質に直接依存します。
残念なことに、私を含め、ほとんどすべての開発者は、彼の仕事で低品質のコードに直面しています。 そして、これは沼地です。 このようなコードには有毒な症状があります。
- 機能が長すぎ、タスクが多すぎる
- 関数には多くの場合、特定するのが困難な、時にはデバッグするのが難しい副作用があります。
- 関数と変数のわかりにくい名前
- 脆弱なコード:小さな変更により、他のアプリケーションコンポーネントが予期せず破損する
- テストのコードカバレッジが低いか、まったくない
「このコードの仕組みがわからない」、「妄想的なコード」、「このコードを変更するのは難しい」などのステートメントは誰もが知っています。
ある日、私の同僚は、保守が困難なRuby REST APIに対処しようとしていたため、辞職しました。 彼は以前の開発チームからこのプロジェクトを受け取りました。
現在のエラーを修正すると新しいエラーが発生し、新しい関数を追加すると新しいエラーが発生します(脆弱なコード)。 クライアントは、アプリケーションを再構築し、便利な構造にしたくなかったため、開発者は正しい決定を下しました-終了しました。
このような状況は頻繁に発生し、悲しいことです。 しかし、何をすべきか?
まず、覚えておいてください:動作するアプリケーションを作成し、コードの品質を管理することは別のタスクです。
一方では、アプリケーション要件を実装しています。 ただし、一方で、時間を費やして、関数に掛かるタスクが多すぎるかどうかを確認したり、変数や関数に意味のある名前を付けたり、副作用のある関数を避けたりする必要があります。
関数(オブジェクトメソッドを含む)は、アプリケーションを機能させる小さな歯車です。 最初は、その構造と構成に注目する必要があります。 この記事では、シンプルで理解しやすく、テストしやすい機能を作成するための最良のアプローチについて説明します。
1.機能は小さくなければなりません。 とても小さい。
多くのタスクを持つ肥大化した機能は避けてください。いくつかの小さな機能を実行することをお勧めします。 隠された意味を持つ肥大化した関数は、理解、変更、および特にテストが困難です。
関数が配列、マップ、または単純なJavaScriptオブジェクトの要素の合計を返す状況を想像してください。 量は、プロパティ値を追加して計算されます。
-
null
またはundefined
場合は1ポイント - プリミティブ型の場合は2ポイント
- オブジェクトまたは機能に対して4ポイント
たとえば、配列[null, 'Hello World', {}]
の合計は、 1
( null
ごと)+ 2
(行ごと、プリミティブタイプ)+ 4
(オブジェクトごと)= 7
ように計算されます。
ステップ0:主要な主要機能
最悪の方法から始めましょう。 アイデアは、1つの大きなgetCollectionWeight()
関数を使用してコードを記述することです。
function getCollectionWeight(collection) { let collectionValues; if (collection instanceof Array) { collectionValues = collection; } else if (collection instanceof Map) { collectionValues = [...collection.values()]; } else { collectionValues = Object.keys(collection).map(function (key) { return collection[key]; }); } return collectionValues.reduce(function(sum, item) { if (item == null) { return sum + 1; } if (typeof item === 'object' || typeof item === 'function') { return sum + 4; } return sum + 2; }, 0); } let myArray = [null, { }, 15]; let myMap = new Map([ ['functionKey', function() {}] ]); let myObject = { 'stringKey': 'Hello world' }; getCollectionWeight(myArray); // => 7 (1 + 4 + 2) getCollectionWeight(myMap); // => 4 getCollectionWeight(myObject); // => 2
問題ははっきりと見えます。 getCollectionWeight()
関数は肥大化しすぎて、驚きに満ちたブラックボックスのように見えます。
おそらく、一見すると、彼女の仕事が何であるかを理解することは困難です。 そして、アプリケーションでこのような関数のセットを想像してください。
このようなコードを使用すると、時間と労力を無駄にします。 高品質のコードを使用しても不快感はありません。 短くて説明のつかない機能を備えた高品質のコードは、読みやすく、保守が容易です。
ステップ1:タイプウェイトを削除してマジックナンバーを削除する
ここでの目標は、長い関数を小さく独立した再利用可能な関数に分割することです。 最初のステップは、タイプごとに値の合計を決定するコードを抽出することです。 この新しい関数はgetWeight()
と呼ばれます。
また、この量のマジックナンバー1
、および4
も注意してください。 ストーリー全体を理解せずにこれらの数字を読むだけでは、有用な情報は得られません。 幸いなことに、ES2015ではconst
を読み取り専用として宣言できるため、意味のある名前で定数を簡単に作成し、マジックナンバーを排除できます。
小さなgetWeightByType()
関数を作成し、同時にgetCollectionWeight()
改善しましょう。
// Code extracted into getWeightByType() function getWeightByType(value) { const WEIGHT_NULL_UNDEFINED = 1; const WEIGHT_PRIMITIVE = 2; const WEIGHT_OBJECT_FUNCTION = 4; if (value == null) { return WEIGHT_NULL_UNDEFINED; } if (typeof value === 'object' || typeof value === 'function') { return WEIGHT_OBJECT_FUNCTION; } return WEIGHT_PRIMITIVE; } function getCollectionWeight(collection) { let collectionValues; if (collection instanceof Array) { collectionValues = collection; } else if (collection instanceof Map) { collectionValues = [...collection.values()]; } else { collectionValues = Object.keys(collection).map(function (key) { return collection[key]; }); } return collectionValues.reduce(function(sum, item) { return sum + getWeightByType(item); }, 0); } let myArray = [null, { }, 15]; let myMap = new Map([ ['functionKey', function() {}] ]); let myObject = { 'stringKey': 'Hello world' }; getCollectionWeight(myArray); // => 7 (1 + 4 + 2) getCollectionWeight(myMap); // => 4 getCollectionWeight(myObject); // => 2
良く見えますか?
getWeightByType()
関数は、 getWeightByType()
金額を単純に決定する独立したコンポーネントです。 また、他の機能内で実行できるため、再利用可能です。
getCollectionWeight()
もう少し軽量になっています
WEIGHT_NULL_UNDEFINED
、 WEIGHT_PRIMITIVE
およびWEIGHT_OBJECT_FUNCTION
は、金額の種類を説明する説明不要の定数です。 数字1
、および4
意味を推測する必要はありません。
ステップ2:分離を続行し、機能を拡張する
更新されたバージョンにはまだ欠陥があります。
一般にSetまたは別の任意のコレクションの値の比較を実装する計画があると想像してください。 getCollectionWeight()
は、ロジックが値を収集するため、急速にサイズが大きくなります。
マップgetMapValues()
および単純なJavaScriptオブジェクトgetPlainObjectValues()
から値を収集するコードを個別の関数に抽出してみましょう。 改善されたバージョンを見てください:
function getWeightByType(value) { const WEIGHT_NULL_UNDEFINED = 1; const WEIGHT_PRIMITIVE = 2; const WEIGHT_OBJECT_FUNCTION = 4; if (value == null) { return WEIGHT_NULL_UNDEFINED; } if (typeof value === 'object' || typeof value === 'function') { return WEIGHT_OBJECT_FUNCTION; } return WEIGHT_PRIMITIVE; } // Code extracted into getMapValues() function getMapValues(map) { return [...map.values()]; } // Code extracted into getPlainObjectValues() function getPlainObjectValues(object) { return Object.keys(object).map(function (key) { return object[key]; }); } function getCollectionWeight(collection) { let collectionValues; if (collection instanceof Array) { collectionValues = collection; } else if (collection instanceof Map) { collectionValues = getMapValues(collection); } else { collectionValues = getPlainObjectValues(collection); } return collectionValues.reduce(function(sum, item) { return sum + getWeightByType(item); }, 0); } let myArray = [null, { }, 15]; let myMap = new Map([ ['functionKey', function() {}] ]); let myObject = { 'stringKey': 'Hello world' }; getCollectionWeight(myArray); // => 7 (1 + 4 + 2) getCollectionWeight(myMap); // => 4 getCollectionWeight(myObject); // => 2
getCollectionWeight()
を読むと、この関数が何をするのかを理解しやすくなります。 興味深い話のように見えます。
各機能は明らかでわかりやすいです。 そのようなコードが何をするのかを理解しようとして時間を無駄にしません。 それはそれがいかにきれいであるかです。
ステップ3:改善を止めない
この段階でも、品質を改善する多くの機会があります!
if / elseステートメントを含み、コレクションタイプをgetCollectionValues()
別個のgetCollectionValues()
を作成できます。
function getCollectionValues(collection) { if (collection instanceof Array) { return collection; } if (collection instanceof Map) { return getMapValues(collection); } return getPlainObjectValues(collection); }
その後、 getCollectionValues()
コレクションの値を取得して合計レデューサーを適用するだけなので、 getCollectionWeight()
は本当に簡単になります。
別のリダクション関数を作成することもできます:
function reduceWeightSum(sum, item) { return sum + getWeightByType(item); }
理想的には、 getCollectionWeight()
は関数を定義しないでください。
最終的に、最初の大きな関数は小さな関数に変わります。
function getWeightByType(value) { const WEIGHT_NULL_UNDEFINED = 1; const WEIGHT_PRIMITIVE = 2; const WEIGHT_OBJECT_FUNCTION = 4; if (value == null) { return WEIGHT_NULL_UNDEFINED; } if (typeof value === 'object' || typeof value === 'function') { return WEIGHT_OBJECT_FUNCTION; } return WEIGHT_PRIMITIVE; } function getMapValues(map) { return [...map.values()]; } function getPlainObjectValues(object) { return Object.keys(object).map(function (key) { return object[key]; }); } function getCollectionValues(collection) { if (collection instanceof Array) { return collection; } if (collection instanceof Map) { return getMapValues(collection); } return getPlainObjectValues(collection); } function reduceWeightSum(sum, item) { return sum + getWeightByType(item); } function getCollectionWeight(collection) { return getCollectionValues(collection).reduce(reduceWeightSum, 0); } let myArray = [null, { }, 15]; let myMap = new Map([ ['functionKey', function() {}] ]); let myObject = { 'stringKey': 'Hello world' }; getCollectionWeight(myArray); // => 7 (1 + 4 + 2) getCollectionWeight(myMap); // => 4 getCollectionWeight(myObject); // => 2
これは、小さくシンプルな機能を作成する技術です!
すべてのコード品質の最適化の後、いくつかの利点が表示されます。
-
getCollectionWeight()
の可読性は 、コードなしのコードで簡素化getCollectionWeight()
れました。 -
getCollectionWeight()
サイズが大幅に減少しました -
getCollectionWeight()
関数は、他のタイプのコレクションでの作業を実装する場合の急速な成長から保護されるようになりました - 抽出された関数は、 グループ化されていない再利用可能なコンポーネントです。 同僚から、これらの優れた機能を別のプロジェクトにインポートするように求められる場合がありますが、これは簡単に行えます。
- 関数が誤ってエラーを生成した場合、関数の名前が含まれているため、呼び出しスタックはより正確になります。 ほとんどすぐに、問題を引き起こす関数を見つけることができます。
- 分離された機能は、テストがはるかに簡単で、テストで高レベルのコードカバレッジを実現します。 考えられるすべてのシナリオで1つの肥大化した機能をテストする代わりに、テストを構成し、各小さな機能を個別にテストできます。
- CommonJSまたはES2015モジュール形式を使用できます。 抽出された関数から個々のモジュールを作成します。 これにより、プロジェクトファイルが軽量で構造化されます。
これらの利点は、複雑なアプリケーション構造で生き残るのに役立ちます。
一般規則-関数は20行を超えるコードであってはなりません。 少ないほど良い。
公正な質問があると思います。 「コードの各行に関数を作成したくありません。 停止する必要がある場合の基準はありますか?」これは次の章のトピックです。
2.関数はシンプルでなければなりません
少し脱線して、アプリケーションが何であるかを考えてみましょう。
各アプリケーションは一連の要件を実装しています。 開発者のタスクは、これらの要件を、明確に定義された操作を実行する小さな実行可能コンポーネント(スコープ、クラス、関数、コードブロック)に分割することです。
コンポーネントは他の小さなコンポーネントで構成されています。 コンポーネントのコードを作成する場合は、以前の抽象化レベルでのみコンポーネントからコードを作成する必要があります。
言い換えれば、関数をより小さなステップに分解する必要がありますが、それらはすべて同じ、以前の、抽象化レベルにある必要があります。 関数がシンプルになり、「1つのタスクを実行し、これが高品質の実装である」ことを意味するため、これは重要です。
何が必要ですか? 単純な機能は明らかです。 証拠とは、簡単に読んだり修正したりすることを意味します。
追ってみよう。 配列の素数(2、3、5、7、11など)のみを保存し、残り(1、4、6、8など)を削除する関数を実装するとします。 関数は次のように呼び出されます。
getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
getOnlyPrime()
関数を実装するには、前のレベルの抽象化のどのステップが必要ですか? このようにしましょう:
getOnlyPrime()
を実装するには、IsPrime()
関数を使用して数値の配列をフィルターします。
フィルターIsPrime()
を配列に適用するだけです。
このレベルでIsPrime()
詳細を実装する必要はありますか? いいえgetOnlyPrime()
関数には異なるレベルの抽象化からのステップがあるためです。 関数は非常に多くのタスクを引き受けます。
この単純なアイデアを忘れずに、 getOnlyPrime()
関数の本体を実装しましょう。
function getOnlyPrime(numbers) { return numbers.filter(isPrime); } getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
ご覧のとおり、 getOnlyPrime()
は基本関数です。 抽象化の1つのレベルからの手順が含まれています。配列の.filter()
メソッドとIsPrime()
です。
それでは、前のレベルの抽象化に移りましょう。
.filter()
配列メソッドはJavaScriptの一部であり、そのまま使用されます。 もちろん、標準はメソッドが何をするかを正確に記述しています。
これで、 IsPrime()
実装方法を指定できます。
数値nが素数かどうかをチェックするIsPrime()
関数を実装するには、nが2
からMath.sqrt(n)
までの任意の数で割り切れるかどうかをチェックする必要があります。
このアルゴリズムを使用してIsPrime()
関数のコードを記述しましょう(まだ効率的ではありません。簡単にするために使用しました)。
function isPrime(number) { if (number === 3 || number === 2) { return true; } if (number === 1) { return false; } for (let divisor = 2; divisor <= Math.sqrt(number); divisor++) { if (number % divisor === 0) { return false; } } return true; } function getOnlyPrime(numbers) { return numbers.filter(isPrime); } getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11]
getOnlyPrime()
は小さく基本的です。 これには、前のレベルの抽象化で厳密に必要な手順のみが含まれています。
ルールに従ってルールを明確にすれば、複雑な関数の読み取りを大幅に簡素化できます。 抽象化の各レベルのコードが細心の注意を払って書かれている場合、これにより不便なコードの大きな塊の生成が防止されます。
3.コンパクトな関数名を使用する
関数名はコンパクトでなければなりません:それ以上でもそれ以下でもありません。 理想的には、名前は実装の詳細を調べることなく、関数が何をするかを明確に示す必要があります。
関数名には、小文字で始まるキャメルケース形式を使用しaddItem()
: addItem()
、 saveToStore()
またはgetFirstName()
。
関数はアクションであるため、その名前には少なくとも1つの動詞が含まれている必要があります。 たとえば、 deletePage()
、 verifyCredentials()
。 プロパティを取得または設定するには、setおよびgetプレフィックスgetLastName()
またはsetLastName()
ます。
本番環境では、 Foo()
、 bar()
、 ()
、 fun()
などの混乱を招く名前は避けてください。 そのような名前は意味がありません。
関数が小さくシンプルで、名前がコンパクトな場合:コードは良い本のようになります。
4.結論
もちろん、上記の例は簡単です。 実際に存在するアプリケーションはより複雑です。 前のレベルの抽象化の単純な関数を書くことは退屈な作業であると不満を言うかもしれません。 しかし、プロジェクトの最初からそれを行う場合、それほど面倒ではありません。
アプリケーションの機能が既に肥大化している場合、コードの再構築はおそらく困難になります。 そして、多くの場合、合理的な時間間隔では不可能です。 少なくとも小さなものから始めましょう。できる限りのことをしてください。
もちろん、正しい決定は最初からアプリケーションを正しく実装することです。 そして、実装だけでなく、機能の正しい構造にも時間を投資します:それらを小さくシンプルにする。
7回測定し、1回切ります。
ES2015には、小さな機能が優れた実践であることを明確に示す優れたモジュラーシステムがあります。
整理整頓されたコードには常に時間がかかることを忘れないでください。 あなたにとって難しいかもしれません。 たくさん練習する必要があるかもしれません。 関数を何度も返して変更することができます。
汚いコードほど悪いものはありません。
コードを整理するためにどのような方法を使用しますか?
( ナタリアベースによる翻訳)