若手エンジニアを不幸にしないための開発の「べからず」集が
長くなりすぎたので、コーディングに関する部分を別項目として独立させました。
「若手エンジニアを不幸にしないための開発の「べからず」集」を書くことを思いつくきっかけとなった記事
若手エンジニアを不幸にしないためのC++コーディングべからず集
も参考にしていただければ幸いです。
##コーディングの「べからず」##
###何を実装すべきなのかについての詳細を確認しない。
アジャイルな開発をしている場合には、すぐさま開発することがあるので、時として何を実装すべきなのかの詳細を確認しないままになってしまうことがあります。
- 実装のコーディングの前に、個別の関数への仕様が十分に明らかになっているかを確認しない。
- ヘッダファイルに記述されたドキュメンテーションコメントをも誰でもが曖昧さなしに解釈できる内容になっているかをチェックしない。
- 実装すべき内容は、どのようにテストしたらよいのかをコーディング前に考えない。
- テスト駆動開発(TDD)ならば、テストのコードを先に書くことになりますね。
###アイディア検証を軽視する。
- アイディア検証には、信頼性の高い別ライブラリ・別言語を使って楽をするなどという可能性を考えない。
- アイディア検証の時点では、グラフ作成や高次の機能でのassertが楽にできる言語・ライブラリを使うと格段に楽になります。制限の強いライブラリで見ていると、問題に気づきにくくなります。
- 実装したい機能を実現する方法を1つ考えたら、それ以上の可能性を考えようともしない。
- アイディア検証の時点で、テストをしておくことでバグの可能性を減らせることを考えない。
- 画像処理や機械学習の場合には、「このアルゴリズムでうまくいく」と言えるアルゴリズムを構築して、想定する入力データに対して動作を検証することが一番むずかしい部分です。アルゴリズムすなわち内部仕様が既に明らかになっているのであれば、コーディング自体の比率はとても少なくなっています。(特に、Pythonから使う場合には、OpenCVのライブラリ、scikit-learn, scikit-image, matplotlibなどのライブラリを使うことで、利用できるライブラリはとても充実しています。これらのライブラリは広く使われているので、バグが残っている比率は極めて少ないものになっています。ラベリング関数もOpenCV3に含まれているので、自作でラベリング関数を書いてバグを埋め込んでしまうなどをすることはありません。)
####ビルドに潜む厄介さに対して対策を打たない
-
if(ブール定数){}
で十分なことを#if
#endif
で条件付きコンパイルしてしまう。 -
#if
#endif
で条件つきコンパイルを増やしてしまい、いつの間にかビルドさえできないソースコードを作り出してしまう。 - 条件付きコンパイルのための指定をソースコード中に
#define
で記述してしまうので、コードの利用者が条件付きコンパイルのための指定がばらついているときに、予期しない#define
の組み合わせでコードをビルドしてしまう。
const
で十分なところを#define
を用いてしまう。 - マクロ定数が多すぎる。
- CMakeを使わない。
- コミットするのは、ビルドができてからというルールを徹底させない。
###隠蔽をしすぎる。
- プログラムで生じる例外を区別せずに、全部まとめて例外を捕捉する。
- バグに気づきうるかもしれない項目も含めて、全てを隠蔽してしまう。
- 何が入力になって処理のとあとに何が出力になるのかというデータの依存性を見えなくするまでに、メソッドの入力と戻り値を隠してしまう。
- 関数のインタフェースを共通化しようとするあまり、全ての関数に共通に引数を与えてしまう。そうすると、本当に引数を必要にするものと、必要ととしないものの区別がつきにくくしてしまう。
- 単体テストをするならば必要になるインタフェースを関数として抽出していない。
###単体テストを軽視する
- 変換と逆変換が存在するのに、その動作の単体テストを実施しない。
- 再現性のある単体テストが不可能なcamera captureのインタフェースでしか用意しない。
- 単一責務の原理を無視しまくっているので、テスト用のコードと本来の実装のコードがいつのまにか乖離しまくっているので、テスト用のコードが意味をなさなくなっている。単一責務の原理(Single Responsibility Principle)を守るとバグに苦しむことが減る
- こうすればきれいな実装になるはずと、単体テストを用意せずに設計変更して泥沼に導いてしまうこと。
- デバッグ目的の機能が、本来の実装の重荷になるような設計をしてしまう。
- プログラムのライブラリに対して単体テスト(例 CppUnit, googletestなど)を行うようにすれば、開発目的のソースコード自体には余分なデバッグ用・テスト用のコードを書かなくて済む。一方、そのような単体テストを使わないのと、本来の開発のソースコードの中に、山ほどassert()を書いたりすることになるので、保守性を低下させてしまう。
- for文とif文の組み合わせで条件にヒットしなかったときの可能性を考えていない。
###コーディングをやっかいにしないための方策を取り入れようとしない。
- 意味を誤解するような関数名・変数名を使い続けること。
- 1割増しの困難をもたらすコード上のやっかいさを何十個と含めてしまう。
- イテレータを使うことで簡潔に書ける部分でイテレータを使わない。
- プログラムの動作状況や原因追跡のログを書くコードの中で
fflush()
を実施しない。 - 型の指定を間違えてときにトラブルを生じやすい
printf()
,scanf()
を使うこと。 - 副作用のある引数を多用してしまう。関数名や引数名からすると
const
であることが期待されるのに副作用を持っていると、利用者は値が書き換えられていることに気づかず、原因不明のバグに苦しむ。 -
std::max()
,std::min()
を使わず、if文や3項演算子を使う。 -
std::cout
を使わず、printf()
を使う。得てして、printf
の型指定を間違えてしまう。 -
__FUNCTION__
マクロ定数を使わずに、表示の文字列の中でべた書きする。
###古すぎるライブラリ
- 他に山ほど処理時間のかかる部分があるのに、
std::string
はchar*
型よりオーバーヘッドが大きいと気にすること。 - Boostなどの有用なライブラリをもちいれば、OS依存性の問題を避けられるのに、windows.hなどに含まれている機能を使うこと。
- UnixのC言語のライブラリは、ネットワークや文字コードが進展する前の大昔のコードを多く含んでいるのに、それが有名なテキストに含まれている関数だからといって使い続ける設計を行う。
- 利用できるライブラリに対して無関心でありつづけること。
- 対象のコンパイラではサポートしている機能があるのに、無意味に古いコンパイラで動作させようとすることにこだわる。
- 微々たる「高速化」のために、enum型やdefineによるマクロ定数を乱発して、エラーメッセージがわかりにくくなる設計をする。
###最終ターゲットと開発環境
- 最終ターゲットと違いすぎる環境で長期間開発しすぎること。
- とりわけソフトウェア環境として違いすぎること。OSやC++などの利用するコンパイラや利用するライブラリが違いすぎることが開発の手間を増やしてしまう。コンパイラの種類によらず適切なC++のコードを書くのは理想かもしれないが、何もGCCとVisual StudioのC++の最新規格への対応状況を自分の仕事のプロジェクトの中に持ち込みたくはない。GCCで十分であるのならば、GCCだけを使って、PC-LinuxとARMの組み込みLinuxに対応できる限り対応するほうがいい。
- 最終ターゲットが貧弱な環境であるのにもかかわらず、最初からその貧弱な実環境での開発にこだわりすぎること。
- アイディアの検証をする必要がある時点で、最初から過度に最終ターゲットの実環境と限定されたライブラリだけを使ってアイディアの検証をしようとする。
- コーディング時点でのテストの軽視
- コンピュータ以外の部分含まれている開発で、周辺機器のIOを含む部分での単体テストのコードを書かない。
- 繰り返し試験を行わなければならないライブラリのテストを十分な自動化をしないために不十分なテストになってしまうままのコーディング・テストマシンの運用状況を作ってしまう。
- テストマシンとしては、RaspberryPiなどのマシンが安価に調達しやすいということに気づかない。
- RaspberryPI の産業用途
- RaspberryPi でPythonでモータ制御するための情報
- CNN(Convolutional Neural Network) on Raspberry pi
- Pythonでのモータ制御に関心をもっている理由
- Cython on Raspberry Pi
- Omega2 というWi-Fi内蔵・Linux搭載のコンピューターが気になりはじめました
###既存のコードをリファクタリングしないままに、機能を追加する。
その結果、1つのクラスもしくはモジュールの役割が不明確になりがちです。大概の場合は、追加した機能によって、単一責務の原理から外れてしまいがちです。いつのまにか、コードの明快さがなくなってしまいがちです。そうなることを防ぐために、まずリファクタリングすることです。そして、その後で機能を追加します。追加する機能が、既存のクラスもしくはモジュールに、ちょっと異質の要素がある場合には、別のクラスもしくは別のモジュールにして、その差分が明確になるようにして実装したほうがいいと感じています。既存のコードをリファクタリングしないままに機能を追加すると、コードを書いている本人にとっても理解不能なコードになっていく危険なアプローチです。
トレーサビリティを軽視する。
品質を確保するには、どのソースコード、どのデータで、どの装置でどういう結果を出したのかを知る必要がある。つじつまの合わない組み合わせで動作させたり、バグが残っていたバージョン古いコードを使って動作させていないことを確認する必要があります。そのため、トレーサビリティが確保できる仕組みを開発中のソースコードに埋め込んでいく必要があります。
### 再現性を軽視する
装置などの外部状況が無視できないソフトウェアを開発する場合には、再現性が確保できるようにするための作業が必要になる。それらの作業を軽視すれば、えたいの知れない挙動に苦しむことになる。
再現性が確保できるように、手順を標準化するための作業をソフトウェアエンジニアだけではなく、装置の設計・製作・操作などの担当者の協力をあおぐ必要がある。
### バージョン管理システムで、主旨の異なる修正を同時にコミットしてしまう。
ソースコードを修正する際に、主旨の異なる修正を同時にコミットしてしまう。そうすると、何か不具合が発生したときに、どの部分で不具合を生じさせてしまっているのかをたどることが手間が増えてしまう。だから主旨の異なる修正は、同時にコミットすることをしてはならない。
###コーディングについてありがちな発言の例
###これ経験があるぞ
試行錯誤を繰り返す
デモを何度もつくるために色んなバージョンがある,
ドキュメント化はされない.
詳しくはリンク先を見てください。【ソフト開発 アンチパターン】Lava Flow
###「さあ?なんでなんでしょうか?私が受け持った時には既にそうなっていたので…」
このせりふは次のリンク先の印象的な言葉を引用させてもらっている。
消えたプログラマの残したものは
そう、大多数のソフトウェア開発者は、自分が書いたわけじゃないコードを引継いで、メンテナンスや追加開発をしている。
途中から引継いだコードは、奇妙なコードになっていることがある。
そう、そのコードのデータ構造の使い勝手の悪さは、あなたが選択したものではない。でも、その使い勝手の悪さが、開発プロジェクトの開発速度の低下をまねいているんだとしたら、いつまで、その使い勝手の悪い仕組みを使い続けるのですか。
### 自分の権限が及ばない部分のコードをどうやって改善してもらうか?
自分の権限でリファクタリングし、修正できる場合はいい。自分の権限が及ばない範囲のコードに、問題のあるコードはそのままになっていることは、あなたの職場ではないでしょうか。テストのしやすさを意識していないコード。ヘッダファイルのincludeが際限なく増えているソースコード。constを使えば十分なところをマクロ定数を使っているコード。どうやってわかってもらうか悩ましいところです。
以下のリンクは、そのような状況で参考にしてほしいと思う記事です。
引き継いだソースコードを改変する前に
引き継いだソースコードをメンテナンスしやすくする。
引き継いだソースコードを大幅改変する
リファクタリングで何を優先すべきか(うれしかったかのは何か)