はじめに
あいさつ
開発部のShosukeです。普段はPython、PHPを利用して主にバックエンドの開発をしています。
また、INAPに新卒で入社してから1年近くになり、この1年でレビュー時に注意されたことや私が意識していることについて書こうと思います。
なぜ書くのか
このブログはこれから入ってくる後輩たちに向けて書きます。(新しく開発業務に関わるようになった人にも見ていただけたら嬉しいです。)
理由は私がこの1年で注意されたり学んだ事を共有することで、後輩たちのレベルアップを促進してINAPの技術レベルを上げていきたいからです。
内容としては、一般的な技術書やブログと大きく重複しますが、その中でも特に私が重要だと思ったものを集めています。初心者でも実践できるプログラミングのエッセンスが詰まったものだと考えて読んでいただければと思います。
この記事を読んで少しでも学びがあれば幸いです。
心構え
テクニックについてこれから説明しますが、ベースとなる哲学はこれです。正直に言うと、ここさえ読めば後は別のブログ記事や技術書を読んでもOKです。
とにかく読みやすいコードにする!
エンジニアは想像よりもコードを書く時間が少なく、他人が書いたコードを読んでいる時間のほうが多いです。(業種やレベルによって個人差あり)
例えば、チームメンバーのコードを読んで、バグがあるか、これから実装する機能で再利用できるクラスがあるかチェックしたり、使用サービスの使い方を調べるために公式のドキュメントを読んでいます。それは他のエンジニアにも当てはまると思います。
そのため、読む時間をいかに少なくするか、自分が書いたコードでチームメンバーの時間をいかに取らせないかが大切になり、コードの読みやすさがとても重要になります。
また、コードが読みやすいとバグ修正の時に原因を探しやすくなります。(とても大事)
自分の能力に落ち込まずに、常により良い方法を考える!
1年目でも、一生懸命書いていると他人に自慢したくなるほど良い設計や処理を書けることがあります。そんな時に自分はこの言語を完全に理解したんだ!と思わずに先輩にレビューしてもらったり、github上で優秀なエンジニアのコードを見てください。おそらく自分よりステップ数が少なくて処理が早いコードがたくさん見つかり、とても落ち込みます。(毎週経験あり)
こういうときこそ成長のチャンスだと思います。人は物を覚える時は感情(今回なら落ち込みや驚き)と結びつけたほうが記憶に定着しやすいそうですし、自分と優秀な人の技術能力の差分を客観的に見ることができる貴重な機会です。気分が落ち込んでも決して腐らずに成長の糧にできるようになるといいと思います!
すぐに手を動かさない!
頭の中でアルゴリズムが思い浮かぶとすぐに手を動かしていませんか?
これはミスがあった場合にコードを1から書き直す可能性のある非常に悪い習慣です。(経験豊富な優秀なエンジニアは除く)
自分で他の方法はないか考えたり、選択肢に迷った場合は先輩に相談したほうが良いです。
特に最初の数ヶ月は先輩に毎回、方針を相談することが良いと思います。
主に以下が理由です。
最初に思い浮かんだ方法が最適とは限らない
まだまだ半人前なので、自分にとっての最良が一般的な最良とは限りません。もし他人の意見を受ける前に自分の考えで機能を作成すると、バグや処理速度の問題から1から作り直す可能性があります。
そもそも、タスクの依頼されたモノと異なるものを作成しようとしている可能性がある
最悪のパターンです。エンジニアは要求されたモノを作成することが仕事です。これをやってしまうとお金をもらっているのに、その時間を無駄にするというお客様にも会社にも自分のメンタルにも大きなダメージを食らうことになります。
タスクで不明点があったらプロジェクトリーダーや先輩に自分の解釈があっているか質問しましょう!
変数
変数はプログラミングをする上で最も基本的なものですが、使い方次第では読み手はコメントなしでも処理内容を理解することができます。
反対にここがダメダメだといくらコメントをつけようが読みにくくなるのでとても重要です。
注意・工夫点リスト
- 意図が伝わる変数名になっているか
- 単数形と複数形を使い分けているか
- 再代入をしていないか
- スコープはなるべく狭く
意図が伝わる変数名になっているか
変数には必ず、変数の内容や使用目的が分かるような名前をつけましょう。(例外あり)
短い関数などでは問題はありませんが、その関数もシステムが運用されていくうちに機能が追加されたりする可能性があります。またデバッグの時に頭の中で変数名から変数の用途を思い浮かべないといけないので頭が疲れます。そのためレビューの精度が落ちたり、時間を取られます。
ダメな例
# 変数名に数字や無機質な名前を使わない int1 = 3776 int2 = -7.1 str2 = “shizuoka” |
良い例
# それぞれの数字と文字列の意味がはっきりした mt_fuji_elevation = 3776 mt_fuji_avg_temperature = -7.1 mt_fuji_location = “shizuoka” # ループ中で使う「i」「j」などはインデックスとして利用することが習慣となっており、他のエンジニアが見ても意味が理解できるので問題ない for i in range(3): print(mt_fuji_location) |
単数形と複数形の使い分けは出来ているか
一般的に複数形は配列などの繰り返されるもの、単数形は値が1つだけ入っていると考えられています。
そのため読み手は複数形が来たら繰り返しのできるものが入っているのだなと想定してコードを読み進めます。
その時に文字列が1つだけ入っているとバグではないかと考え調査してしまい時間が無駄になるので注意が必要です。
# ダメな例 mountains = “mt_fuji” # 良い例 mountains = [“mt_fuji”, “mt_hiei”] |
再代入をしていないか
バグのもとになりやすいのが変数に想定しない値が入ることです。それは再代入を行う時に発生しやすいです。
そのため基本的には変数は使い回さずに、使用時に新たに作成するほうが安全です。
スコープはなるべく狭く
再代入と同じ理由になります。グローバル変数など広いスコープで扱える変数には注意が必要です。
例えばグローバル変数はかなり広範囲で扱うことができるため、あるメソッドで値を更新すると他の利用しているメソッドでも更新後の値で処理されます。
そのためメソッド作成時に想定した結果を返さない(つまりバグ)ことになります。
これを防ぐには次の手段が考えられます。
- グローバル変数など広いスコープで扱える変数はFinalなどを利用して変更不可能にする。
- グローバル変数などを利用するときは、メソッド内でそのまま使わずに別の変数に値をコピーしてそれを使い回す。
読みやすいアルゴリズム
変数を工夫することでかなり読みやすくなりますが、その他にも工夫すべき点があります。
ループや条件分岐(for, ifなど)はとても便利ですが、簡単にダメダメなコードを生み出してしまうので注意が必要です。以下の工夫をすることで、ダメになりにくくなります。
注意・工夫点リスト
- 早期return, continue, break
- 否定の多用
- ロジックの切り出し
- 書くべきコメント
早期return, continue, break
メソッドの始まりからすぐに、return, continue, breakを行うとネストを浅くすることが出来ます。
以下はゲームでレベルアップした時のメッセージを取得する処理です。
1つ目の例だとネストが深くて読むのが面倒ですが、早期returnをすることでネストが浅くなり読みやすさが上がりました!
処理概要
プレイヤーのレベルが10以上であれば、レベルと職業に合わせてメッセージを返す。
レベルが10未満であれば、「もっと強くなろう」を返す。
# 早期returnしていない例 def get_level_up_message(player_level: int, player_job: str): if player_level >= 10: if player_level < 20: if player_job == “knight”: return “ナイトは強くなった!” if player_job == “witch”: return “魔女は強くなった!” elif player_level < 30: if player_job == “knight”: return “ナイトはとっても強くなった!!!!” if player_job == “witch”: return “魔女はとっても強くなった!!!!” else: return “レベルアップした” else: return “もっと強くなろう” # 早期returnしている例 def get_level_up_message(player_level: int, player_job: str): # 最初にレベル10未満を早期returnした。ネストが1段低くなった。 if player_level < 10: return “もっと強くなろう” if player_level < 20: if player_job == “knight”: return “ナイトは強くなった!” if player_job == “witch”: return “魔女は強くなった!” elif player_level < 30: if player_job == “knight”: return “ナイトはとっても強くなった!!!!” if player_job == “witch”: return “魔女はとっても強くなった!!!!” else: return “レベルアップした” |
否定の多用に注意
否定は便利ですが、多用することはおすすめできないです。
複数の条件をつないだ時に否定があると条件が複雑になりやすくなり頭が疲れます。そのためコードを書いている人もテストに時間がかかったり、レビュアーもレビューの際にも時間がかかったり、テストケースを見過ごしてしまう可能性があります。
対策としては、早期returnやドモルガンの法則などを活用してなるべく条件が複雑にならないようにすることが挙げられます。
active_flag = True anonymous_flag = True valid_flag = True # 条件が複雑でわかりにくい if not active_flag and anonymous_flag or not valid_flag: return “有効なユーザでない” # 早期returnなどを活用して条件を分割する if not valid_flag: return “有効なユーザでない” if not active_flag and anonymous_flag: return “有効なユーザでない” |
ロジックの切り出し
処理が複雑になったり、1つのメソッドの行数が長くなった場合は別のメソッドに切り出すことが重要です。
メソッドに切り出す場合には以下の点に注意してください。
- メソッドの名前以上の機能を与えないでください。
- メソッドの名前から機能が直感的に分かるようにする
- メソッドの行数は長くても、1画面に収まる範囲にする
下の例では上記のコードをメソッドでまとめています。
def is_invalid_user(active_flag, anonymous_flag, valid_flag): if not valid_flag: return True if not active_flag and anonymous_flag: return True return False # 条件判定をメソッドでまとめたので、条件式がシンプルになった if is_invalid_user(active_flag, anonymous_flag, valid_flag): return “有効なユーザでない” |
コメントで書くべきもの
これは人によって考えが異なります。以下は私の考えです。
コメントでは、コードの説明ではなくそのコードを書いた根拠を書くべきです。(複雑な処理の場合は、処理の説明を加えることは重要)
また、コードだけでなく、コメントも保守の対象です。コメントが多いほど仕様変更が合った時に修正範囲が増えるので、いたずらに書くべきではないと考えています。
例えば、下の例だとコメントに処理の説明を書いていますが、このコメントは下の処理を見ればすぐに分かることなので必要性は薄いです。
areas = [“shizuoka”, “nagano”, “gifu”, “toyama”] result = “” # areasでループを回し、静岡ならループを中断しでresultにareaに代入する for area in areas: if area == “shizuoka”: print(“Mt.Fuji is there.”) result = area break |
より早く、安全に
ガード
アプリの実行中に関数に予期せぬ値が入ってくることがあります。予期せぬ値のままメソッドを実行してしまうとエラーが発生する可能性が非常に高いです。それを防ぐのがガードです。
ガードはメソッドの最初の方の行で引数をチェックし、チェックした結果によって処理を実行します。
以下では、引数にNone(Nullのようなもの)が来た時に、引数を空配列で初期化してエラーが起きないようにしています。
def get_sum_array(numbers: list[int]): # 引数numbersにNoneが来た場合に、ループでエラーが起きるので空配列で初期化する if numbers is None: numbers = list() sum = 0 for num in numbers: sum += num return sum |
クエリをなるべく少なくする
SQLの実行は、システムを実行する上で比較的時間がかかります。そのためSQLの実行回数を減らすほどシステムの処理速度は不要なSQLのために遅くならないです。
対策方法としては、なるべく1回のSQLに情報をつめることです。基本的には各フレームワーク毎にそれを行う機能があるのでそちらを実行してください。
ループがあった場合はSQLが無駄に実行されていないかチェックしてみてください。
下の例では、実行されるコードとクエリを表しています。
for customer in customers: # ループの度にSQLが実行される get_customer_record(id=cutomer.id) “”” 合計3回のSQLが実行される SELECT * FROM customers WHERE ‘id’=1; SELECT * FROM customers WHERE ‘id’=2; SELECT * FROM customers WHERE ‘id’=3; “”” # 取得したいレコードを1回で実行する get_cutomer_records(target_list=cutomers) “”” 合計1回のSQLが実行される SELECT * FROM customers WHERE ‘id’ IN (1,2,3); “”” |
安全なクラス設計
クラスはオブジェクト指向をする上で切っても切り離せませんが理解することは少し難しいです。今回はその中でも私が大事にしていることを紹介します。
- コンストラクタで正常値をセットする
- 値オブジェクトを利用する
- 継承とポリモーフィズムを活用する
コンストラクタで正常値をセットする
システムを実行させると予期しない値が渡ってくることが往々にしてあります。その際にシステムとしての誤動作しないように、クラスには自分でメンバー変数を守ってもらいます。
そのためにコンストラクタで期待した型のデータが期待したサイズで入ってきているかなどをチェックします。
これによりクラス内のメソッドが予期せぬ動作をすることを防ぐことができます。
class TestResult: def __init__(self, math_score: int, english_score: int): self.math_score: int = math_score self.english_score: int = english_score # テストの点数が0未満または、100より大きいときにエラーを出す if math_score < 0 or 100 < math_score: raise ValueError(“math score is invalid”) if english_score < 0 or 100 < english_score: raise ValueError(“english score is invalid”) |
値オブジェクトを利用する
値オブジェクトはメンバー変数ごとに固有のクラスを用意して、そのクラスのオブジェクトとして扱うことです。
コンストラクタで値をセットできても、クラス内のメソッドに誤ったメンバー変数を渡す可能性があります。
例えば上の例だと、数学の点数を渡すべきところに英語の点数を渡しても型が同じなためエラーが発生せずにそのまま処理が続きます。
そこで下のように値オブジェクトを用意すると、誤った値が渡ってきた時に型が異なるので、エラーが発生します。
class MathScore: def __init__(self, score): self.score = score if score < 0 or 100 < score: raise ValueError(“math score is invalid”) class EnglishScore: def __init__(self, score): self.score = score if score < 0 or 100 < score: raise ValueError(“math english is invalid”) class TestResult: # 引数に値オブジェクトを利用することで、誤った引数が渡されないようにする def __init__(self, math_score: MathScore, english_score: EnglishScore): self.math_score: MathScore = math_score self.english_score: EnglishScore = english_score # 数学の点数が赤点を超えているか # MathScoreを受け取るため、英語の点数で赤点をチェックすることがない @classmethod def beyond_math_akaten(cls, math_score: MathScore): if math_score.score > 40: return True else: return False # 英語の点数が赤点を超えているか # EnglishScoreを受け取るため、数学の点数で赤点をチェックすることがない @classmethod def beyond_english_akaten(cls, english_score: EnglishScore): if english_score.score > 30: return True else: return False |
継承とポリモーフィズムを活用する
継承とポリモーフィズムを活用することで似通った処理を簡単に実装できたり、処理の行数を減らすことが出来ます。
継承を行うことで複数クラスで使いたい処理を、親クラスに定義することで複数のクラスで利用することが出来ます。
ポリモーフィズムは主にインターフェースを使用して実現し、インターフェースを継承したクラスでは同名のメソッドを定義することを強制されるため、利用者は複数のクラスを同じ呼び方で実行出来るようになります。
(Pythonではインターフェースが存在しないため、エラーを活用して擬似的にインターフェースを再現します。)
class ParentClass: def interface_method(self): raise Exception(“このメソッドをオーバーライドしないとExceptionが発生します。”) def common_method(self): print(“このメソッドは、BaseClassを継承したクラスで利用できる共通メソッドです。”) class ChildClass(ParentClass): # このメソッドは継承しないとエラーが出る def interface_method(self): print(“ChildClassがインターフェースメソッドをオーバーライドしました。”) class ChildClass2(ParentClass): # このメソッドは継承しないとエラーが出る def interface_method(self): print(“ChildClass2がインターフェースメソッドをオーバーライドしました。”) def run_interface_method(object: ParentClass): # このメソッドでは、引数にChildCalssとChildCalss2のオブジェクトを受け取るが、どちらが来ても同じように呼び出すことができる!!! object.interface_method() if __name__ == ‘__main__’: child = ChildClass() child2 = ChildClass2() print(“—– ChildClass —–“) child.common_method() child.interface_method() print(“—– ChildClass2 —–“) child2.common_method() child2.interface_method() print(“—– run_interface_method —–“) run_interface_method(child) run_interface_method(child2) |
実行結果
—– ChildClass —– このメソッドは、BaseClassを継承したクラスで利用できる共通メソッドです。 ChildClassがインターフェースメソッドをオーバーライドしました。 —– ChildClass2 —– このメソッドは、BaseClassを継承したクラスで利用できる共通メソッドです。 ChildClass2がインターフェースメソッドをオーバーライドしました。 —– run_interface_method —– ChildClassがインターフェースメソッドをオーバーライドしました。 ChildClass2がインターフェースメソッドをオーバーライドしました。 |
インターフェースメソッドは継承先でオーバーライドしないと次のようにエラーが出ます。
/Users/aoshimasys/PycharmProjects/printTest/venv/bin/python /Users/aoshimasys/PycharmProjects/printTest/main.py —– ChildClass —– このメソッドは、BaseClassを継承したクラスで利用できる共通メソッドです。 Traceback (most recent call last): File “/Users/aoshimasys/PycharmProjects/printTest/main.py”, line 195, in <module> child.interface_method() File “/Users/aoshimasys/PycharmProjects/printTest/main.py”, line 167, in interface_method raise Exception(“このメソッドをオーバーライドしないとExceptionが発生します。”) Exception: このメソッドをオーバーライドしないとExceptionが発生します。 |
継承をすることでChildCalssとChildCalss2で共通メソッドを利用できました。
そのためインターフェースは継承先の振る舞いを制限することができます。その制限を利用することで、複数のクラスのメソッドを同じように呼び出すことが出来ます。
おすすめの書籍、サイト
私がこの1年で参考にした書籍とサイトを紹介します。ぜひ読んでみてください!!!
良いコード/悪いコードで学ぶ設計入門 ―保守しやすい 成長し続けるコードの書き方
リーダブルコード
コードを読みやすい、読みにくいと感じる要因を体系立てて説明してくれる超有名名著。自分のコードが読みやすいのか疑問に思ったら読むべきです。
良いコードの書き方
今回参考にさせていただいたサイトです。ご自身の経験を元に、テクニックを幅広く紹介されています。文字数は多いですが、各テクニックに重要度が書かれているので、つまみ読みでも大変勉強になります。
読みやすいコードを書くために
今回参考にさせていただいたサイトです。もともと社内向けの資料を外部へ公開したものです。そのため、要点を得ており短い文章ながらとても勉強になります。
終わりに
今回は非常に長い文章になりましたが、ここまで読んでいただきありがとうございました。
この記事では、色々なサイトや書籍を読みつつ特に重要だな、自分でも実践できそうだというものを集めています。そのため、あまり経験のない人でも十分に使いこなせるテクニックが集まっていると思います。
これらのテクニックを経て今の私の技術力があるので、後輩達にはここで紹介したテクニックをササッと身につけていただき、すぐに同じレベルで切磋琢磨出来るようになれたら嬉しいです。
この記事が参考になったら幸いです。頑張ってください。