JavaScript >> Javascript チュートリアル >  >> JavaScript

3 リファクタリングのテクニック

ハクトーバーフェストが近づいています。オープン ソース コントリビューションに不慣れで、支援方法がわからない場合は、コードのリファクタリングを提案してもよろしいですか?不明確なコードに新鮮な視点を提供し、以前よりも良い状態にする方法を見つけることができます。

私がよく実践している 3 つのリファクタリング手法があります。

  • 名前を変更
  • 巣を取り除く
  • 抽出機能

この3つだけを適用する方法を知っているだけで、あなたは遠くまで行くことができます.それらの意味と、プロジェクトでどのように使用したか (または使用する必要があったか) について説明します。

1. 名前を変更する

この手法の目標は、コードを通じて人々がコミュニケーションできるようにすることです .

大学の教授からのこの話を覚えています。彼女はかつて、変数が野球選手にちなんで名付けられたコードを継承していました。なんで?不幸なプログラマーの悪意しか想像できません.

変数、条件、または関数が何をするかをすぐに理解できなかった場合、他の誰かも理解できない可能性があります。その機能と他のコードとの相互作用を理解したら、より適切な名前を付けてください。

変数

正しく行われた変数名は、目的を説明します。一般に、切り詰められた単語よりも完全なスペルの単語を優先する必要があります。これによりあいまいさがなくなり、コードをテキストで検索するときに推測できるようになります。

これは、Ember Times ニュースレターが Ember リポジトリに貢献したユーザーを見つけるのに役立つアプリ、whats-new-in-emberland に加えた変更です。

// Before
let conListUniq;

// After
let contributorsList;

アプリが貢献者を見つけることをお伝えしなければ、conListUniq の "conList" を解読するのに苦労したことでしょう。

変数名は型を説明することもできます。たとえば、名前を is で始めることができます または can ブール値を示し、名前を複数形にして配列を示します。複数名は、配列を反復処理するときに役立ちます。配列要素には単数名詞を使用できます。

export function filterMerged(pullRequests) {
  return pullRequests.filter(pullRequest => {
    const { isMadeByUser, isMergedThisWeek } = pullRequest;

    return isMadeByUser && isMergedThisWeek;
  });
}

変数 pullRequests の命名規則に注意してください 、 pullRequestisMadeByUser 、および isMergedThisWeek .

b.条件

条件ステートメントは、動的な値と言語固有の構文で構成されているため、一度に消化するのは難しい場合があります。これは、複合条件 (&& で結合された 2 つ以上のステートメント) の場合により当てはまります。 または ||

条件を維持するには、わかりやすい名前で一時変数を作成してみてください。一般に、複合条件の各ステートメントは、独自の変数を取得する必要があります。コードを声に出して読むと、ほとんど自然に聞こえます。

少し前に、ember-component-template-colocation-migrator に機能を追加しました。コマンドラインで実行されるため、いくつかのフラグをサポートする必要がありました。

const { argv } = require('yargs');

// Allow passing the flag, -fs (flat) or -ns (nested), to specify component structure
const changeToFlatStructure = argv.f && argv.s;
const changeToNestedStructure = argv.n && argv.s;

let structure = 'flat';

if (changeToFlatStructure) {
  structure = 'flat';

} else if (changeToNestedStructure) {
  structure = 'nested';

}

一時変数の作成には、抽象化の利点があります。後で別のライブラリを使用してフラグを処理することにした場合、変更されるのは 1 行目から 5 行目だけです。 structure の分岐ロジック

c.関数

セクション 3 では、関数がリファクタリングでどのように重要な役割を果たすかを見ていきます。

命名に関しては、関数の名前を動詞で始めることをお勧めします。 get を使いたい または find データを取得する関数を示し、set または update データを変更するものを示します。

@action async getContributors() {
  const fetchRequests = this.mergedPRs.map(pullRequest => pullRequest.user);

  let users = await all(fetchRequests);
  users = this.identifyUsers(users);
  users = this.sortUsers(users);

  this.updateContributorsList(users);
}

getContributors の動詞の使用に注意してください 、 identifyUserssortUsers 、および updateContributorsList .それらの実装は表示されませんが、それぞれが何をすべきかを推測できる場合があります。

2. 巣を取り除く

ネストを削除することは、コード構造を平坦化することです .不要なインデントを削除することで、残ったインデントで関連するコードのグループを明確に示すことができます。

コードのインデントはスタイリングの選択の一部であるため、ネストを削除することがなぜ重要なのか疑問に思うかもしれません。ネストされた条件と promise を見て、その欠点を確認します。

ネストされた条件

時間の経過とともに、ネストされた if ステートメントが混乱する可能性があります。ビジネス ロジックは常に変化します。時間のプレッシャーにより、全体的な解決策を見つけるために、コードをリファクタリングするのではなく、新しいロジックを許可する例外を追加することがあります.

最高の架空の例は、Sandi Metz の 2014 RailsConf 講演、All the Little Things です。 Sandi が Gilded Rose 問題について語っています。このコードを考えると、

def tick
  if @name != 'Aged Brie' && @name != 'Backstage passes to a TAFKAL80ETC concert'
    if @quality > 0
      if @name != 'Sulfuras, Hand of Ragnaros'
        @quality -= 1
      end
    end
  else
    if @quality < 50
      @quality += 1
      if @name == 'Backstage passes to a TAFKAL80ETC concert'
        if @days_remaining < 11
          if @quality < 50
            @quality += 1
          end
        end
        if @days_remaining < 6
          if @quality < 50
            @quality += 1
          end
        end
      end
    end
  end
  if @name != 'Sulfuras, Hand of Ragnaros'
    @days_remaining -= 1
  end
  if @days_remaining < 0
    if @name != 'Aged Brie'
      if @name != 'Backstage passes to a TAFKAL80ETC concert'
        if @quality > 0
          if @name != 'Sulfuras, Hand of Ragnaros'
            @quality -= 1
          end
        end
      else
        @quality = @quality - @quality
      end
    else
      if @quality < 50
        @quality += 1
      end
    end
  end
end

tick を更新する必要があります あと 1 つの機能だけを処理し、すべてのテストが引き続きパスするようにします。どこから始めますか?

tick リファクタリングの鍵 早期退出することです 、ガード句とも呼ばれます。条件が true の場合にのみ発生する可能性のあるコードが表示された場合は、現在いる場所からすぐに離れます (return を使用) 、 break 、または continue ) false と評価された場合。

// Before
function myExample() {
  if (condition) {
    /* Complex code omitted */
  }
}

// After
function myExample() {
  if (!condition) {
    return;
  }

  /* Complex code omitted */
}

複雑なコードから 1 つのインデント レベルを削除したことに注意してください。いくつかの早期終了を行うことができると想像してください。終了しないたびに、複雑なコードが単純になり、他のリファクタリングが可能になります。また、一連の if を読み取ることで、 ステートメントを上から下に並べると、次のコードがいつ実行されるかが正確にわかります。

一連の小さなリファクタリングを経て、Sandi は次のコードにたどり着きました。このコードをより簡単に理解して変更できると思います。

def tick
  case name
  when 'normal'
    normal_tick
  when 'Aged Brie'
    brie_tick
  when 'Sulfuras, Hand of Ragnaros'
    sulfuras_tick
  when 'Backstage passes to a TAFKAL80ETC concert'
    backstage_tick
  end
end


def normal_tick
  @days_remaining -= 1
  return if @quality == 0

  @quality -= 1
  @quality -= 1 if @days_remaining <= 0
end


def brie_tick
  @days_remaining -= 1
  return if @quality >= 50

  @quality += 1
  @quality += 1 if @days_remaining <= 0
end


def sulfuras_tick
end


def backstage_tick
  @days_remaining -= 1
  return              if @quality >= 50
  return @quality = 0 if @days_remaining < 0

  @quality += 1
  @quality += 1 if @days_remaining < 10
  @quality += 1 if @days_remaining < 5
end

私の実際の例は、セクション 3c に譲ります。 ember-component-template-colocation-migrator からフラグ コードを更新する方法を示します。

b.ネストされた Promise

前に、入れ子になった if ステートメントを推論して変更するのが難しい場合があることを確認しました。同様に、ネストされた promise も避けたいと考えています。

以下は、私が初期の頃に書いたサーバー コードです。

router.get('/my-example', function(req, res) {
  Writer.findOne(/* Query options omitted */)
    .then(user => {
      // Get the user's profile
      const profile = user.dataValues;

      // Serialize the user's stories
      const stories = user.Stories.map(story => ({
        id: story.id,
        title: story.title,
        writer: {
          id: profile.id,
          fullName: profile.fullName
        },
        photos: story.Photos.map(photo => ({
          id: photo.id,
          url: photo.url,
          caption: photo.caption
        }))
      }));

      // Serialize the user's readers
      const readers = user.Readers.map(reader => ({
        readerId: reader.reader_id
      }));

      // Serialize the user's writers
      Reader.findAll(/* Query options omitted */)
        .then(results => {
          const writers = results.map(writer => ({
            writerId: writer.writer_id
          }));

          // Send the user's profile, stories, readers, and writers
          res.json({
            profile,
            stories,
            readers,
            writers
          });
        });
    });
});

最終的な目標は非常に単純です:ユーザーについて知っていることをクライアントに送信します (35 ~ 40 行目)。では、なぜコードがごちゃごちゃしているように感じるのでしょうか?

1 つの理由は、ネストされた promise です。 (他にもありますが、セクション 3a で説明します。) 非常に多くのインデントがあるため、コードの開始位置と終了位置、および 1 つの promise から別の promise にまたがる変数を確認するのは困難です。さらに、コードは障害点を想定していません。

async が広く採用される前 と await 、promise チェーンを使用してこのコードをリファクタリングした可能性があります。ただし、Promise チェーンに問題がないわけではありません。

async の使用 と await 、次のようにコードを書き換えることができます。

router.get('/my-example', async function(req, res) {
  try {
    const user = await Writer.findOne(/* Query options omitted */);

    if (!user) {
      throw new Error('Could not find user.');
    }

    // Get user's profile
    const profile = user.dataValues;

    // Serialize user's stories
    const stories = user.Stories.map(story => ({
      id: story.id,
      title: story.title,
      writer: {
        id: profile.id,
        fullName: profile.fullName
      },
      photos: story.Photos.map(photo => ({
        id: photo.id,
        url: photo.url,
        caption: photo.caption
      }))
    }));

    // Serialize user's readers
    const readers = user.Readers.map(reader => ({
      readerId: reader.reader_id
    }));

    // Serialize user's writers
    const results = await Reader.findAll(/* Query options omitted */);
    const writers = results.map(writer => ({
      writerId: writer.writer_id
    }));

    // Send the user's profile, stories, readers, and writers
    res.json({
      profile,
      stories,
      readers,
      writers
    });

  } catch (error) {
    /* Error handling omitted */

  }
});

profile に注意してください 、 storiesreaders 、および writers 同じインデント レベルで定義されるようになりました。これは、コードが伝えるストーリーの結末をたどるのに役立ちます。最後に、データをクライアントに送信しますが、データはどこから来るのでしょうか?上にスクロールしましょう。

3. 関数の抽出

いよいよグランドフィナーレ。場合によっては、1 つのこと (これは良いことです) を実行する関数に遭遇することがありますが、多くのコード行 (おそらく悪いこと) があります。実際、セクション 2b で見ました。

関数には、順番に実行されるいくつかの重要なステップが含まれています。あなたの目標は:

  • 重要なステップを特定する
  • 各ステップの関数を作成する
  • 各関数にわかりやすい名前を付ける

大きな関数を小さな関数に分割するこのプロセス 抽出といいます。特に外部システムと通信する必要がない場合 (API リクエストの作成やインデックスの検索など) の一部の小さな関数は、単体テストできるようになりました。

最も有用なリファクタリング手法を選ぶとしたら、それは抽出でしょう。

例 1

セクション 2b では、ネストされた promise を削除することができました。関数を抽出して、コードをさらにリファクタリングしましょう。

重要なステップをどのように特定しますか?良い指標は、コードが何をするかを説明するために残されたコメントです。コメントに基づいて関数に名前を付けることもできます。

API を書き直すとしたら、次のようになると思います。

import { serialize } from '../some-path';

router.get('/my-example', async function(req, res) {
  try {
    const user = await Writer.findOne(/* query options omitted */);

    if (!user) {
      throw new Error('Could not find user.');
    }

    const profile = getProfile(user);
    const stories = serialize({ name: 'Story', data: user.Stories });
    const readers = serialize({ name: 'Reader', data: user.Readers });
    const writers = await getWriters(user);

    res.json({
      profile,
      stories,
      readers,
      writers
    });

  } catch (error) {
    /* Handle error */

  }
});

function getProfile(user) {/* ... */}

async function getWriters(user) {/* ... */}

b.例 2

whats-new-in-emberland で model を見つけました フック、PR (プル リクエスト) と RFC (コメントのリクエスト) を取得する関数で、次のようになります。

async model() {
  const store = this.store;
  const startOfWeek = this.startOfWeek;

  const projectFetches = CONSTANTS.REPOS.map((repo) => {
    return store.findRecord('github-organization', repo);
  });

  let orgs = await all(projectFetches);

  const prFetches = orgs.map((org) => {
    return fetch(`https://api.github.com/search/issues?q=is:pr+org:${org.id}+created:>=${moment(startOfWeek).format('YYYY-MM-DD')}`, {
      headers: {
        'Authorization': `token ${this.githubSession.githubAccessToken}`,
      },
    })
    .then((response) => response.json())
    .then((pulls) => this.store.pushPayload('github-pull', { githubPull: pulls.items }));
  });

  const rfcFetches = ['ember-cli/rfcs', 'emberjs/rfcs'].map((repo) => {
    return store.query('github-pull', { repo, state: 'all' });
  });

  await all(prFetches);
  let pulls = this.store.peekAll('github-pull').toArray();
  let rfcSets = await all(rfcFetches);

  let mergedPulls = pulls.filter((pull) => {
    return moment(pull.get('mergedAt')) > moment(startOfWeek);
  }).reduce((previousValue, item) => previousValue.concat(item), []);

  let newPulls = pulls.filter((pull) => {
    return moment(pull.get('createdAt')) > moment(startOfWeek) && !pull.get('mergedAt');
  }).reduce((previousValue, item) => previousValue.concat(item), []);

  let newRfcs = rfcSets.map((pulls) => {
    return pulls.filter((pull) => {
      return moment(pull.get('createdAt')) > moment(startOfWeek);
    });
  }).reduce((previousValue, item) => previousValue.concat(item), []);

  let mergedRfcs = rfcSets.map((pulls) => {
    return pulls.filter((pull) => {
      return moment(pull.get('mergedAt')) > moment(startOfWeek);
    });
  }).reduce((previousValue, item) => previousValue.concat(item), []);

  return hash({
    orgs,
    mergedPulls,
    newPulls,
    mergedRfcs,
    newRfcs
  });
}

model リファクタリングの鍵 一度に1つずつ関数を抽出することでした。 5-19 行目と 25-26 行目から、model ということがわかりました。 PR を取得します。それは素晴らしいことです!関数を抽出しました。同様に、21 ~ 23 行目と 27 行目から model が見つかりました。 RFC をフェッチします。それはまた別の抽出です。

興味深いことに、29 行目から 47 行目 (合計 4 つの関数) から関数を抽出するには、効果的にリファクタリングするために Ember の知識が必要でした。 model に加えて フック、Ember は setupController を提供します 針。 model からのデータを後処理することができます .たとえば、配列をフィルタリングできます。

行 29 ~ 47 を setupController に移動しました 関心をよりよく分離するために、関数を抽出し、さらにコードを簡素化しました。最後に、次の美しいコードを発見しました。

model() {
  return hash({
    prs: this.fetchPRs(),
    rfcs: this.fetchRFCs()
  });
}

setupController(controller, model) {
  super.setupController(controller, model);

  const { prs, rfcs } = model;

  controller.mergedPRs = filterMerged(prs);
  controller.newPRs = filterNew(prs);
  controller.mergedRFCs = filterMerged(rfcs);
  controller.newRFCs = filterNew(rfcs);
}

c.例 3

ember-component-template-colocation-migrator では、メイン関数 execute からいくつかの関数を抽出しました 、機能を追加する前。その結果、この機能により execute に予測可能な小さな変更が加えられました。 (以下の 9 ~ 10 行目):

async execute() {
  let templateFilePaths = this.findClassicComponentTemplates();
  templateFilePaths = this.skipTemplatesUsedAsLayoutName(templateFilePaths);
  templateFilePaths = this.skipTemplatesUsedAsPartial(templateFilePaths);

  if (this.structure === 'flat') {
    this.changeComponentStructureToFlat(templateFilePaths);

  } else if (this.structure === 'nested') {
    this.changeComponentStructureToNested(templateFilePaths);

  }

  await this.removeEmptyClassicComponentDirectories();
}

まだ行われていない別の例 (Hacktoberfest の準備中です!) は、前に見た flags コードから関数を抽出することです。

const { argv } = require('yargs');

function getStructure() {
  const changeToFlatStructure = argv.f && argv.s;
  const changeToNestedStructure = argv.n && argv.s;

  if (changeToFlatStructure) {
    return 'flat';
  }

  if (changeToNestedStructure) {
    return 'nested';
  }

  return 'flat';
}

セクション 2 で学んだリファクタリング手法である早期終了に注目してください。

4. 結論

コードをリファクタリングすることで、オープン ソース プロジェクトに影響を与えることができます。 名前の変更、ネストの削除、関数の抽出の 3 つのテクニックを実践するだけで — 新しい貢献者がコードを理解するのを助け、プロジェクトの寿命を延ばすことができます。

コードを適切に管理すると、コードがどのようなものになるかを示す例をいくつか見てきました。学んだことを適用し、これらのテクニックを他の人と共有することをお勧めします.


  1. loadable-components を使用した Gatsby での動的コード分割を使用したパフォーマンスの向上

  2. React ハンドブック – 初心者向けの React を学ぶ

  3. 最新の Javascript 開発環境