📘

関数内の抽象レベルを合わせる👀

に公開

はじめに

リファクタリング技術を勉強していた際に関数内の抽象レベルについてテーマに挙がっていたので、

今回はこれを記事にしてみたいと思います。

抽象レベルとは

抽象レベルとはコードに記載された内容がどれだけ抽象的、

つまり大きな流れに基づいていた全体的で大局的視点(大所高所な視点)にあるのか、

はたまた具象的、

つまり具体的な手段や手続き、全体に対する詳細部分といった踏み込んだ内容で局所的視点にあるのか、

と言うことを表す基準になります。

ECサイトを例に挙げると、「注文を処理する」と言うコードは高レベルに抽象化されているので前者に該当し、

比較して「そのためのクエリ(DB操作)」は低レベルに抽象化されているので後者に該当します。

これを踏まえて、一つの関数内では複数の抽象レベルの処理を混在させるのではなく、

可読性やメンテナンス性のためにもどちらか一つにすることを推奨されています。

実際のコードを見てみる

言葉だけだと分かりづらいので、これもコードに置き換えて見てみましょう💪

まずは以下のコードを見てください。

抽象度が混在している場合

// 抽象度が混在しているコード

class Item {
  final double price;
  final int quantity;

  Item(this.price, this.quantity);
}

class Order {
  final List<Item> items;

  Order(this.items);

  double calculateTotal() {
    return items.fold(0.0, (total, currentItem) => total + (currentItem.price * currentItem.quantity));
  }
}

void processAndSendOrder(Order order) {
  // 抽象レベル1: 高レベルの処理(注文処理の流れ)
  // この関数に注文オブジェクトを渡し、最終的な処理を実行する
  _sendOrderToWarehouse(order);

  // 抽象レベル2: 低レベルの処理(具体的な計算)
  // ここで直接、注文オブジェクトのメソッドを呼び出し、合計金額を計算する
  double totalAmount = order.calculateTotal();

  print('注文合計金額: $totalAmount');
}

void _sendOrderToWarehouse(Order order) {
  print('倉庫に注文を送信しました。');
}

void main() {
  final items = [
    Item(100, 2),
    Item(50, 3),
  ];
  final myOrder = Order(items);
  processAndSendOrder(myOrder);
}

コード自体は非常にシンプルですが、匂う部分がありますね。

そう、、、

void processAndSendOrder(Order order) {
  // 抽象レベル1: 高レベルの処理(注文処理の流れ)
  // この関数に注文オブジェクトを渡し、最終的な処理を実行する
  _sendOrderToWarehouse(order);

  // 抽象レベル2: 低レベルの処理(具体的な計算)
  // ここで直接、注文オブジェクトのメソッドを呼び出し、合計金額を計算する
  double totalAmount = order.calculateTotal();

  print('注文合計金額: $totalAmount');
}

ここです。

メソッド名の時点で何だか嫌な予感がしますが、やはりその通りでしたね😅

一つのメソッド内で異なるレベルの抽象度が混在しています。

一つは抽象度の高いもの、もう一つは抽象度が低いものになっていますね。

この程度のメソッドであれば然程問題にはならないかと思われますが、

もっと見通しが良くなるコードに出来るので以下のようにリファクタリングしてみました💪

抽象度が揃っている場合

// 抽象度が揃っているコード

class Item {
  final double price;
  final int quantity;

  Item(this.price, this.quantity);
}

class Order {
  final List<Item> items;

  Order(this.items);

  double calculateTotal() {
    return items.fold(0.0, (total, currentItem) => total + (currentItem.price * currentItem.quantity));
  }
}

void processOrder(Order order) {
  // 高レベルの処理に特化
  // 注文処理という、より大きな流れに焦点を当てる
  _sendOrderToWarehouse(order);
  _displayOrderSummary(order);
}

void _sendOrderToWarehouse(Order order) {
  // 低レベルの具体的な処理
  print('倉庫に注文を送信しました。');
}

void _displayOrderSummary(Order order) {
  // 低レベルの具体的な処理
  double totalAmount = order.calculateTotal();
  print('注文合計金額: $totalAmount');
}

void main() {
  final items = [
    Item(100, 2),
    Item(50, 3),
  ];
  final myOrder = Order(items);
  processOrder(myOrder);
}

どのように変わったのかと言うと、、、

/// 修正前
void processAndSendOrder(Order order) {
  // 抽象レベル1: 高レベルの処理(注文処理の流れ)
  // この関数に注文オブジェクトを渡し、最終的な処理を実行する
  _sendOrderToWarehouse(order);

  // 抽象レベル2: 低レベルの処理(具体的な計算)
  // ここで直接、注文オブジェクトのメソッドを呼び出し、合計金額を計算する
  double totalAmount = order.calculateTotal();

  print('注文合計金額: $totalAmount');
}

これが、

/// 修正後
void processOrder(Order order) {
  // 高レベルの処理に特化
  // 注文処理という、より大きな流れに焦点を当てる
  _sendOrderToWarehouse(order);
  _displayOrderSummary(order);
}

こんな風になったのです。

前者の抽象度の低いコードをメソッドとして抽出して、それをcallするようにしているんですね。

こうすることでメソッド自体の処理がシンプルになり、行数も少なくなったことは勿論、

抽象度が揃っている(どちらも高いレベルになっている)ことにより斜め読みが非常にしやすい、

言わば『一言で言うと何だっけ?』がしやすい状態になっており、

それを反映してメソッド名の冗長性が解消されています。

実際のproductコードはこれとは比にならないくらいに長大なため、

抽象度を意識してコードを眺めてみることで、

コードがより良くなるかも知れませんね🤔

参考

https://book.mynavi.jp/ec/products/detail/id=147499

Discussion