29
40

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ケント・ベックに学ぶ良いコードの書き方🗒️

Last updated at Posted at 2024-09-13

こんにちは、しが あきとし(@akitosihga)です。

先日あるMeetUpで良いコードの書き方について考える機会がありました。
『良いコード』の定義は幅広く様々な解釈があると思います。
その中でも、自分が敬愛するプログラマーのケント・ベックから学んだ事に焦点を当てて良いコードの書き方についてまとめました。

ケント・ベックとは

  • テスト駆動開発(TDD)で有名なプログラマー
  • アジャイル開発におけるエクストリームプログラミング(XP)の考案者としても有名
  • アジャイル開発関連の書籍に度々登場するCRCを発明したのも彼だったりする
  • 代表的な著書は「テスト駆動開発」「エクストリームプログラミング」

TDDのイメージが強い彼ですが、実はコーディングに対して並々ならぬ情熱を持っているのです。

彼の著書「実装パターン」では以下のように語っています。

『 70年の人生は、20億秒を少し超えるに過ぎない。誇りの持てない仕事で無駄にする時間はない。良いコードを書くこと自体が喜びであり、そのコードを他の人が理解し、評価し、使用し、拡張してくれればさらに喜びは増す。 』

職業人生をコーディングにかけている情熱的な人で自分は彼のそんな所が好きだったりします。

ケント・ベックの考える『良いコード』

ケントベックは『良いコード』の持つ価値を以下の3つと定めています。

  1. コミュニケーション
  2. シンプル
  3. 柔軟性

コミュニケーション

ケント・ベックは日々の開発をコードとプログラマーのコミュニケーションと捉えました。
コミュニケーションが取れるコードとは、読み手が理解できるコード、実装意図を正しく伝えらえるコードのことです。
コードは書いた時間より読まれる時間の方が長いです。
そのことから、人間とコミュニケーションが取れるコードは保守のコストを大きく減らすことができると言う点で大きな価値があります。

シンプル

シンプルとは『余分な複雑性』のないコードのことを指します。
『余分な複雑性』を持つコードとは、以下のようなものです。

  • 動作に影響がないが冗長なコード
  • 一生懸命動かそうとした痕跡のある整然としていないコード

例えば、機能が少しづつ育ってリファクタ可能だが着手できないコードは余分な複雑性がある状態のコードです。
ただし、複雑なコード全てが悪というわけではありません。
解決すべき問題が複雑な場合はその複雑さが反映されます。
これは『本質的な複雑性』を持つコードであり、プログラミングにおいてはその複雑性が『余分』なのか『本質的』なのかを判断する必要が生じます。

柔軟性

柔軟性のあるコードとは変更を容易に行えるコードのことです。
明日必要と思われた柔軟性も状況が変わって不要になってしまうことは皆さんも経験があるかもしれません。
こういったことから、凝った設計で備えるよりはシンプルと包括的なテストから得られる柔軟性の方が効果的です。
「YAGNI」・「KISS」の原則を尊重することでこの柔軟性に近づく事ができると自分は考えています。

この3つの価値に対してケント・ベックは『パターン』を用いてアプローチしました。

パターン

パターンとはソフトウェア設計上の課題とその有効な解決策を再利用可能なように一般化したものです。
GoFのデザインパターンは広く知られていると思います。

ケント・ベックはこのパターンの先駆けで、彼はこのパターンを用いて『良いコード』を書くことを推進しました。

ケント・ベックの用いるパターンの特徴は以下です。

  • いずれもコミュニケーション・シンプル・柔軟性のいずれかの価値を反映している
  • 粒度が細かく幅広い
    • 一般的に想像されるパターンに加えて命名規則にもこのパターンの考えが用いられている

これらの特徴に加えて、どのパターンも後述の6つの原則に従っています。
この原則を守ることでコミュニケーション・シンプル・柔軟性の価値を生むことができるというのがケント・ベックの主張です。

次にこの6つの原則について紹介します。

パターンが従う6つの原則

原則1 結果の局所化

  • コードの変更の結果が一箇所に留まるようにコードを構成すること
  • コードの変更が想定外の場所に影響するとその変更コストは劇的に上昇する
  • 結果の局所化ができているコードであれば、コード全体を把握しなくても段階的に理解すれば良くなる

原則2 繰り返しの最小化

  • 同じ目的の下に同じ処理を行うコードは一箇所にまとめて共通化すること
  • (当たり前だけど)コードが重複していると一つを変更すると他のすべても変更しなければいけなくなる
  • 重複したコードの分だけ変更コストは増大する
  • ただし、必ずしも重複を排除すれば良いのではなく、目的が異なれば同じ処理であってもコードは共通化しない方が良い

原則3 対称性

  • 対称性を意識するとコードは劇的に読みやすくなる
    • コードの粒度や行える操作を対称的にする(addメソッドがあれば、removeメソッドも実装する)
    • クラス内のプロパティの生存期間を同一にする
    • あるメソッドのグループがあれば全て同じ引数を取るようにする
    • 長い処理のメソッドを均一の粒度に分割する

原則4 ロジックとデータの一体化

  • ロジックとそのロジックが操作するデータは近くに置くこと
  • 結果の局所化を遵守すると必然的にロジックとデータは一体化する
  • 関連するロジックとデータは同じタイミングで変更することが多くなる
  • これらが同じ場所にあれば変更の結果も局所化が保たれる
    • ただし、ロジックとデータをどこに置くべきかは最初から明確ではない場合がある
    • コードに対する継続的な責務の見直しは大事だなというのが個人的な所感

原則5 宣言型の表現

  • 実装の詳細を書くよりこの処理が何をするかという意図を宣言する
    • JavaScriptを例にすると、for文よりforEachメソッド
    • Javaを例にすると、for文よりStream
  • 命令型のプログラミングは制御とデータのフローを頭の中でイメージしながら読む必要がある
  • 宣言型で表現されていれば読み手の認知負荷を大幅に抑えられる

原則6 変更頻度

  • 変更されるタイミングが異なるロジックやデータは分けておく
  • 変更されるタイミングが同じロジックやデータは同じ場所に置いておく
  • クラス内のプロパティはなるべく一緒のタイミングで変更されるべき
  • あるメソッドの実行中にだけ変更されるプロパティは、プロパティにせずローカル変数にすべき

税額計算のソフトウェアを例にすると...
一般的な計算ロジックのコードと、年ごとに固有なコードは一緒にせず分けておく

最後にパターンの例をいくつか紹介します。

パターンの例

サンプルコードはRubyで書いています。
馴染のない方向けに一般的なイディオムとは離れた書き方をしている箇所があります。

パターン1 Composed Method

# Before
# 商品の金額を計算する
class PriceCalculator
  def discounted_total_price(order)
    total = 0

    order.items.each do |item|
      total += item.price * item.quantity
    end

    if total > 100
      total -= (total * 0.1)
    end

    if order.customer.premium_member?
      total -= (total * 0.1)
    end

    return total.round()
  end
end

# After
class PriceCalculator
  def discounted_total_price(order)
    total = total_price(order) 
    total = basic_discounted_price(total) 
    total = member_discounted_price(total)
    
    return total.round()
  end

  private

  def total_price(order)
    return order.items.reduce(0) do |sum, item|
      sum + item.price * item.quantity
    end
  end

  def basic_discounted_price(total)
    if total > 100
      return total - (total * 0.1)
    else
      return total
    end
  end

  def member_discounted_price(total, customer)
    if order.customer.premium_member?
      return total - (total * 0.1)
    else
      return total
    end
  end
end

プログラムを一つの事のみにするメソッドに分割しています。
これによりメソッド内部のメッセージは同じ抽象度に揃えられ以下の効果があります。

  • メソッド内部の対称性が保たれる
  • 宣言型で意図が表現されるため読みやすく、意図が伝わりやすくなる

Beforeではメソッドの処理を追わなければなりませんが、Afterではメソッドの呼び出しを見るだけで何をやっているか大まかな意図がわかるようになっています。

パターン2 Double Dispatch

# Before
class Member
  attr_reader :rank
  
  def initialize(rank)
    @rank = rank
  end
  
  def rent_video(type)
    if @rank == :premium
      if type == :new_release
        puts "Premium member renting a new release."
      elsif type == :regular
        puts "Premium member renting a regular video."
      end
    else
      if type == :new_release
        puts "Standard member cannot rent new releases."
      elsif type == :regular
        puts "Standard member renting a regular video."
      end
    end
  end
end

member = Member.new(:premium)
member.rent_video(:new_release)  # "Premium member renting a new release."

member2 = Member.new(:standard)
member2.rent_video(:new_release)  # "Standard member cannot rent new releases."

# After
class Member
  def initialize(rank)
    @rank = rank
  end
  
  def rent_video(video_type)
    rank_handler = case @rank
                   when :premium then PremiumRank.new
                   when :standard then StandardRank.new
                   else raise "Unknown member rank"
                   end
                    
    video_type_handler = case video_type
                         when :new_release then NewRelease.new
                         when :regular then RegularVideo.new
                         else raise "Unknown video type"
                         end
                          
    rank_handler.rent(video_type_handler)
  end
end

class Rank
  def rent(video_type)
    raise NotImplementedError, "This method should be overridden by subclasses"
  end
end

class VideoType
  def rent_by_premium
    raise NotImplementedError, "This method should be overridden by subclasses"
  end
  
  def rent_by_standard
    raise NotImplementedError, "This method should be overridden by subclasses"
  end
end

# After
class PremiumRank < Rank
  def rent(video_type)
    video_type.rent_by_premium
  end
end

class StandardRank < Rank
  def rent(video_type)
    video_type.rent_by_standard
  end
end

class NewRelease < VideoType
  def rent_by_premium
    puts "Premium member renting a new release."
  end
  
  def rent_by_standard
    puts "Standard member cannot rent new releases."
  end
end

class RegularVideo < VideoType
  def rent_by_premium
    puts "Premium member renting a regular video."
  end
  
  def rent_by_standard
    puts "Standard member renting a regular video."
  end
end

class Member
  attr_reader :rank
  
  def initialize(rank)
    @rank = rank
  end
  
  def rent_video(type)
    @rank.rent(type)
  end
end

PremiumRank.new.rent(NewRelease.new) #  "Premium member renting a new release."
StandardRank.new.rent(NewRelease.new) # "Standard member cannot rent new releases."
PremiumRank.new.rent(RegularVideo.new) # "Premium member renting a regular video."
StandardRank.new.rent(RegularVideo.new) # "Standard member renting a regular video."

多態性を用いて異なる2つの関心事の変更頻度を分割することで、冗長な『余分な複雑性』の排除を行っています。

パターン3 Method Object

# Before
class Order
  def calculate_total_price(customer, items, discount,
    tax_rate, shipping_cost, coupon, loyalty_points)

    # ベースの合計金額を計算
    total = items.sum(&:price)

    # アイテムが10個以上なら追加割引
    total *= 0.95 if items.size >= 10

    # その他すごく複雑な処理が続く...

    total
  end

  def other_method
    # 他の処理
  end

  # その他メソッドが続く...
end

order = Order.new
puts order.calculate_total_price(customer, items, 10, 0.08, 5, coupon, loyalty_points)


# After
class Calculator
  attr_reader :customer, :items, :discount, :tax_rate,
              :shipping_cost, :coupon, :loyalty_points

  def initialize(customer, items, discount, tax_rate,
                 shipping_cost, coupon, loyalty_points)
    @customer = customer
    @items = items
    @discount = discount
    @tax_rate = tax_rate
    @shipping_cost = shipping_cost
    @coupon = coupon
    @loyalty_points = loyalty_points
  end

  def calculate_total_price
    # ベースの合計金額を計算
    total = items.sum(&:price)

    # アイテムが10個以上なら追加割引
    total *= 0.95 if items.size >= 10

    # その他すごく複雑な処理が続く...

    total
  end
end

class Order
  attr_reader :calculator

  def calculate_total_price
    calculator.calculate_total_price
  end

  def other_method
    # 他の処理
  end

  # その他メソッドが続く...
end

order = Order.new
Order.calculator = Calculator.new(customer, items, 10, 0.08, 5, coupon, loyalty_points)
puts order.calculate_total_price

Composed Methodでも単純化できないほど大きいメソッドは、そのメソッド自体をオブジェクトにします。
関連するデータをプロパティとして持つことでロジックとデータを一体化しています。
多くの引数を持つ場合、メソッドから引数がなくなることで可読性も向上します。
Composed Methodと組み合わせて使うことでより明確でシンプルなコードになります。

まとめ

  • ケント・ベックの考える『良いコード』はコミュニケーション・シンプル・柔軟性から成り立っている

  • パターンを積極的に再利用することで効率的で的確なコーディングを行っている

  • ケント・ベックのパターンは6つの原則に従っている

    1. 結果の局所化
    2. 繰り返しの最小化
    3. 対称性
    4. ロジックとデータの一体化
    5. 宣言型の表現
    6. 変更頻度
  • 本日紹介したパターンやその他のパターンを学んだり、普段コーディングを行う中でパターンを発見することは良いコードを書くことに繋がっていく

  • 普段コードを書く中でパターンの6つの原則を意識するのも効果的

29
40
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
40

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?