Post

良いコードを書くための原則

良いコードを書くことの必要性と、一般的に良いコードを書くための主要な原則について学びます。

良いコードを書くことの必要性

目の前の実装のために急いでコードを書くことだけに集中すると、技術的負債が管理不能なレベルまで膨らみ、後の保守に問題が生じる可能性があります。したがって、開発プロジェクトを進める際には、可能な限り最初から読みやすく保守が容易な良いコードを書くことが重要であることは言うまでもありません。

アルゴリズム問題解決(PS、Problem Solving)やプログラミングコンテスト(CP、Competitive Programming)の場合、通常、問題解決に使用したコードはその問題解決やコンテストが終わった後に再利用することはなく、特にCPの場合は時間制限があるため、良いコードを書くことよりも速い実装の方が重要ではないかという意見もあります。この質問に答えるためには、自分が何のためにPS/CPを行い、どのような方向性を追求しているのかを考える必要があります。

個人的に考えると、PS/CPを通じて学べる点は以下のようなものです:

  • 与えられた実行時間制限とメモリ制限などの条件内で問題を解決する過程で、様々なアルゴリズムとデータ構造を使用して習得することができ、これを通じて実際のプロジェクトを進める際にも特定の状況でどのようなアルゴリズムとデータ構造を使用すれば良いかの感覚を養うことができます
  • コードを書いて提出すると、即座に正解/不正解の判定と実行時間、メモリ使用量に関する客観的なフィードバックを受けることができるため、見落としなく正確なコードを速く熟練して書く練習ができます
  • 他の上級者が書いたコードを見て、自分が書いたコードと比較し、改善点を見つけることができます
  • 実際の開発プロジェクトに比べれば小規模の、似たような機能を持つコードを繰り返し書くため、(特に一人でPSを練習する場合)締め切りなどに縛られずに細部に気を配りながら簡潔で良いコードを書く練習ができます

PS/CPを単に趣味として楽しむ場合ももちろんありますが、私のようにPS/CPを間接的にプログラミングスキルを向上させるために行う場合、最後の「良いコードを書く練習」も前の3つに劣らず大きな利点です。良いコードを書くことも最初から自然にできるわけではなく、繰り返しの練習を通じて着実に習得する必要があるからです。また、複雑で読みにくいコードはデバッグが難しく、自分も一度で正確に書くのが容易ではないため、非効率的なデバッグに時間を取られて結局そんなに速く実装できない場合も多いです。PS/CPはもちろん現場と大きな違いがありますが、だからといって良いコードを書くことを全く気にせず目の前の実装に急ぐのは上記の理由で本末転倒であるため、個人的にはPS/CPでも簡潔で効率的なコードを書くよう努力しています。

良いコードを書くための原則

コンテストで書くコードであれ実務で書くコードであれ、良いコードと呼べる条件は大きく変わりません。この記事では、一般的に良いコードを書くための主要な原則を扱います。ただし、PS/CPでは速い実装のために実務に比べれば相対的に妥協する部分があり得ますが、そのような場合は記事内で別途言及します。

簡潔なコードの作成

“KISS(Keep It Simple, Stupid)”

  • コードが短く簡潔であるほど、当然ながらタイプミスや単純なバグが発生する恐れが減り、デバッグも容易になります
  • できるだけ別途のコメントなしでも簡単に解釈できるように書き、本当に必要な場合にのみコメントを付けて詳細説明を追加します。コメントに依存するよりもコード構造自体を簡潔に保つことが望ましいです。
  • コメントを書く場合は、明確かつ簡潔に書きます
  • 一つの関数に渡す引数は3つ以下にし、それ以上の引数を一緒に渡す必要がある場合は一つのオブジェクトにまとめて渡します
  • 条件文の深さ(depth)が二重、三重に深くなると可読性を低下させるため、条件文の深さを増やすことはできるだけ避けるべきです。 例)上のコードよりガード節イディオム(Guard Clause)を活用した下のコードの方が可読性の面で有利です

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    async def verify_token(email: str, token: str, purpose: str):
        user = await user_service.get_user_by_email(email)
      
        if user:
            token = await user_service.get_token(user)
      
            if token :
                if token.purpose == 'reset':
                    return True
        return False
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    async def verify_token(email: str, token: str, purpose: str):
        user = await user_service.get_user_by_email(email)
      
        if not user:
            return False
        
        token = await user_service.get_token(user)
      
        if not token or token.purpose != 'reset':
            return False
        
      return True
    
  • ただし、PS/CPではさらに進んでコードの長さを縮めて速く書くために、時々C/C++のマクロを活用する裏技を使う場合があります。時間が限られたコンテストに限って時々使用すると有用ですが、PS/CPに限って通用する方法であり、一般的にC++でのマクロ使用は避けるべきです。 例)

    1
    
    #define FOR(i,n) for(int i=0; i<n; i++)
    

コードのモジュール化

“DRY(Don’t Repeat Yourself)”

  • 同じコードを繰り返し使用する場合、該当部分を関数やクラスに分離して再利用します
  • モジュール化を通じてコードを積極的に再利用すると可読性が向上し、後でコードを修正する必要が生じた場合、該当関数やクラスを一度だけ修正すれば良いので保守が容易になります
  • 原則的には、一つの関数が二つ以上の仕事をせず、一つの機能のみを実行することが理想的です。ただし、PS/CPで書くコードは大抵単純な機能を実行する小規模のプログラムであるため再利用に限界があり、時間が制限されているため実務のように厳格に原則に従うのは難しい場合があります。

標準ライブラリの活用

“Don’t reinvent the wheel”

  • アルゴリズムやデータ構造を勉強する段階では、キューやスタックのようなデータ構造、ソートアルゴリズムなどを直接実装してみながら原理を理解するのが有用ですが、そうでない場合は標準ライブラリを積極的に活用するのが良いです
  • 標準ライブラリはすでに数え切れないほど多く使用され検証されており、最適化もよくされているため、直接再実装するよりも効率的です
  • すでにあるライブラリを持ってきて使用すれば良いので、不必要に同じ機能をするコードを直接実装するのに時間を無駄にする必要がなく、協業時に書いたコードを他のチームメンバーが理解しやすくなります

一貫性のある明確な命名法の使用

“Follow standard conventions”

  • 曖昧でない変数名と関数名を使用します
  • 通常使用するプログラミング言語ごとにそれに合った命名規約(naming convention)があるので、使用する言語の標準ライブラリで使用されている命名規約を学び、クラス、関数、変数などを宣言する際に一貫して適用します
  • それぞれの変数と関数、クラスがどのような機能をするのか、そしてブール(boolean)型の場合はどのような条件で真(True)を返すのかが明確に表れるように命名します

すべてのデータを正規化して保存

  • すべてのデータは一つの一貫した形式に正規化して処理します
  • 同じデータが二つ以上の形式を持つと、文字列表現が少しずつ異なったり、ハッシュ値が異なったりするなど、捉えにくい微妙なバグが発生する可能性があります
  • タイムゾーン、文字列などのデータを保存して処理する際は、入力を受け取ったり計算したらすぐにUTC、UTF-8エンコーディングなど一つの標準形式に変換する必要があります。該当データを表現するクラスのコンストラクタで最初から正規化を行うか、データを入力として受け取る関数ですぐに正規化を行うのが良いです。

コードのロジックとデータの分離

  • コードのロジックと関係のないデータは条件文の中に直接入れずに、別のテーブルに分離します 例)上のコードより下のコードのように書くのが望ましいです。

    1
    2
    3
    4
    5
    6
    
    string getMonthName(int month){
      if(month == 1) return "January";
      if(month == 2) return "February";
      ...
      if(month == 12) return "December";
    }
    
    1
    2
    3
    4
    5
    
    const string monthName[] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"};
    
    string getMonthName(int month){
      return monthName[month-1];
    }
    
This post is licensed under CC BY-NC 4.0 by the author.

Comments powered by Disqus.