はじめに
良いコードとは何か?
と言う点において、世の中には良いコードを書くための技術や内容がまとまった書籍など色々あります。今回の記事では、私がこれまでAndroid開発でKotlinを触ってきた中で、どういったものが良いコードなのか?と言うことと、何をすると良いコードになるかといったところについてまとめてみたいと思います。
背景
良いコードをなぜ書くのか?と言う事の背景には、技術的負債を抱えたプロダクトの存在があります。
- この変数がどこでどう使われているのかが分からない
- 何をするための処理なのかがコードを読んでも理解できない
といった事象が発生し、バージョンアップ開発における生産性を下げてしまっている現状があります。そのため、
「技術的負債を返済する時が来た」
これをスローガンにして
- 今後のバージョンアップ開発で技術的負債を溜め込まないようにする
- これまで溜め込んできた技術的負債を少しずつ返済していく
ということを目標に掲げて日々のバージョンアップ開発において継続的に意識していきたいことを書いていきたいと思います。
結論
良いコード = 美しいコード
他人が読んだ時に理解がし易いコードは美しいです。
読むのに時間がかからないコードは美しいコードです。
読むべき範囲が局所化されたコードは美しいコードです。
つまり、パッと見ただけで美しいと思えるコードこそ良いコードです。
そんな良いコード=美しいコードを書く時に気をつける10のことを解説したいと思います。
良いコードを書く時に気をつける10のこと
これから紹介する内容のコードはKotlinをベースにして書きますが、プログラミング言語全般的な内容として読んで頂けると思います。
1.クラスコメント・メソッドコメントを書く
コメントがあるだけで、そのクラス・メソッドが何をするものなのか分かります。
なので、逐一コードを読まなくても何をするクラス・メソッドなのか分かるのがメリットになります。
Before
class LoginViewModel(private val repository: PasswordMemoRepository, private val loginDataManager: LoginDataManager) : ViewModel() {
fun passwordLogin(context: Context) {
val masterPassword = loginDataManager.masterPassword
if (editMasterPassword.value == "") {
_naviMessage.value = context.getString(R.string.err_empty)
}
// etc...
}
}
After
/**
* ログイン画面ビューモデル
*
* @property repository データアクセスリポジトリ
* @property loginDataManager ログインデータ管理
*/
class LoginViewModel(private val repository: PasswordMemoRepository, private val loginDataManager: LoginDataManager) : ViewModel() {
/**
* パスワードログイン判定メッセージ取得処理
* * パスワードの生成が必要か照合したかの判定処理をしてメッセージを返却する
*
* @param context コンテキスト
* @return 判定メッセージ
*/
fun passwordLogin(context: Context) {
val masterPassword = loginDataManager.masterPassword
if (editMasterPassword.value == "") {
_naviMessage.value = context.getString(R.string.err_empty)
}
// etc...
}
}
2.変数にも必ずコメントを書く
コメントがあるだけで、その変数が何者なのかが分かります。
なので、逐一コードを読まなくても変数のそれぞれが何を意味するのか分かるのがメリットとなります。
Before
class LoginViewModel @Inject constructor(private val repository: PasswordMemoRepository, private val loginDataManager: LoginDataManager) : ViewModel() {
private val _naviMessage = MutableStateFlow("")
val naviMessage: StateFlow<String> = _naviMessage.asStateFlow()
private val _keyIconRotate = MutableStateFlow(false)
val keyIconRotate: StateFlow<Boolean> = _keyIconRotate.asStateFlow()
val editMasterPassword = MutableStateFlow("")
var firstTime = false
var firstPassword: String? = null
var incorrectPwCount = 0
After
class LoginViewModel @Inject constructor(private val repository: PasswordMemoRepository, private val loginDataManager: LoginDataManager) : ViewModel() {
/** ログイン時の案内メッセージ */
private val _naviMessage = MutableStateFlow("")
val naviMessage: StateFlow<String> = _naviMessage.asStateFlow()
/** ログイン時の案内メッセージ */
private val _keyIconRotate = MutableStateFlow(false)
val keyIconRotate: StateFlow<Boolean> = _keyIconRotate.asStateFlow()
/** 入力パスワード */
val editMasterPassword = MutableStateFlow("")
/** 初回ログインかどうか */
var firstTime = false
/** マスターパスワード作成時の1回目の入力値 */
var firstPassword: String? = null
/** パスワード誤り回数 */
var incorrectPwCount = 0
3.処理のまとまり単位でコメントを書く
処理のブロック単位で何をしているのかが分かります。
なので、逐一コードを読まなくても処理ブロックでやりたい事が分かるので、メソッドの大まかな処理の流れが分かります。
Before
fun rearrangePasswordList(fromPos: Int, toPos: Int): List<PasswordEntity> {
val origPasswordIds = ArrayList<Long>()
val rearrangePasswordList = ArrayList<PasswordEntity>()
origPasswordList?.let {
for (entity in it) {
origPasswordIds.add(entity.id)
rearrangePasswordList.add(entity)
}
}
val fromPassword = rearrangePasswordList[fromPos]
rearrangePasswordList.removeAt(fromPos)
rearrangePasswordList.add(toPos, fromPassword)
val itr = origPasswordIds.listIterator()
for (comic in rearrangePasswordList) {
comic.id = itr.next()
}
return rearrangePasswordList
}
After
fun rearrangePasswordList(fromPos: Int, toPos: Int): List<PasswordEntity> {
// 元のIDのリストと並べ替え用のリストを定義
val origPasswordIds = ArrayList<Long>()
val rearrangePasswordList = ArrayList<PasswordEntity>()
// 元のIDの並びを保持と並べ替えができるリストに入れ替える
origPasswordList?.let {
for (entity in it) {
origPasswordIds.add(entity.id)
rearrangePasswordList.add(entity)
}
}
// 引数で渡された位置で並べ替え
val fromPassword = rearrangePasswordList[fromPos]
rearrangePasswordList.removeAt(fromPos)
rearrangePasswordList.add(toPos, fromPassword)
// 再度IDを振り直す
val itr = origPasswordIds.listIterator()
for (comic in rearrangePasswordList) {
comic.id = itr.next()
}
// 並べ替えたリストを返却
return rearrangePasswordList
}
4.KDocに沿ったクラスコメントを書く
KDocの規約に沿ったクラスコメントを書くだけで、自動でドキュメントの生成ができます。
なので、詳細設計ドキュメントの代わりになります。
/**
* リサイクラービューのアイテムに対する分割線の描画クラス
*
* @constructor
* コンストラクタ
*
* @param context コンテキスト
* @param orientation リスト方向
*/
class DividerItemDecoration(context: Context, orientation: Int) : ItemDecoration() {
↓
5.KDocに沿ったメソッドコメントを書く
KDocの規約に沿ったメソッドコメントを書くだけで、自動でドキュメントの生成ができます。
なので、詳細設計ドキュメントの代わりになります。
/**
* 対象ビューの操作中の表示イベント
* スワイプすると表示されるボタンの描画イベントとして処理する
*
* @param c 描画用のキャンバス
* @param recyclerView ビュー全体
* @param viewHolder 操作対象のビューホルダー
* @param dX 横方向移動量
* @param dY 縦方向移動量
* @param actionState 操作イベント種別
* @param isCurrentlyActive アクティブな操作かどうか
*/
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
↓
6.命名規則を守る
クラス名とメソッド名の命名規則を守ることで、クラスの定義なのかメソッドの定義なのかがパッと見で分かるので可読性が向上します。
クラス名:パスカルケース
すべての単語の先頭を大文字にする形式で書きます。
/**
* パスワード一覧画面フラグメント
*
*/
class PasswordListFragment : Fragment() {
メソッド名:キャメルケース
最初の単語を小文字にし、それ以降の単語の先頭を大文字にする形式で書きます。
/**
* 検索文字列に応じたパスワード一覧のフィルタ処理
*
*/
fun setSearchWordFilter() {
7.命名規則を守った呼び出しにする
命名規則が守られていれば、他のクラスからの呼び出し時にも、処理を見た時にクラスなのかメソッドなのかがパッと見で見分けがつくので可読性が向上します。
良い例
val dialog = BackgroundColorUtil().createBackgroundColorDialog(this)
- BackgroundColorUtilクラスを生成して
- createBackgroundColorDialogというダイアログを生成するメソッドを呼び出している
といった感じで、ぱっと見で分かりやすいです。
悪い例
val dialog = backgroundColorUtil().createbackgroundcolordialog(this)
- backgroundColorUtilはメソッドなのか?
- createbackgroundcolordialogはどんな単語のメソッドなんだ?
といった感じで、メソッドをネストして読んでいるのか?どんな意味のメソッドなのか?が非常に分かりづらくなります。
8.マジックナンバーを使わない
数値や文字列が直値で変数に代入されていると、その値が何を意味しているのか分からないので、定数を定義してしっかりと値に意味を持たせる事でコードを見ただけで判別がつくようになります。
Before
val uri = resultData.data
when (requestCode) {
1 -> {
val restoreDbFile = RestoreDbFile(requireActivity(), this)
restoreDbFile.restoreSelectFolder(uri)
}
2 -> {
val backupDbFile = BackupDbFile(requireContext())
backupDbFile.backupSelectFolder(uri)
}
3 -> {
val inputExternalFile = InputExternalFile(requireActivity(), settingViewModel)
inputExternalFile.inputSelectFolder(uri)
}
4 -> {
val outputExternalFile = OutputExternalFile(requireContext(), settingViewModel)
lifecycleScope.launch {
outputExternalFile.outputSelectFolder(uri)
}
}
}
After
val uri = resultData.data
when (requestCode) {
RESTORE_DB -> {
val restoreDbFile = RestoreDbFile(requireActivity(), this)
restoreDbFile.restoreSelectFolder(uri)
}
BACKUP_DB -> {
val backupDbFile = BackupDbFile(requireContext())
backupDbFile.backupSelectFolder(uri)
}
INPUT_CSV -> {
val inputExternalFile = InputExternalFile(requireActivity(), settingViewModel)
inputExternalFile.inputSelectFolder(uri)
}
OUTPUT_CSV -> {
val outputExternalFile = OutputExternalFile(requireContext(), settingViewModel)
lifecycleScope.launch {
outputExternalFile.outputSelectFolder(uri)
}
}
}
companion object {
/** DB復元操作 */
private const val RESTORE_DB = 1
/** DBバックアップ操作 */
private const val BACKUP_DB = 2
/** CSV入力操作 */
private const val INPUT_CSV = 3
/** CSV復元操作 */
private const val OUTPUT_CSV = 4
}
9.変数の定義はイミュータブル変数から始める
ミュータブル変数ではなく、イミュータブル変数で定義する事で、コンストラクタ等の初期化時に代入された値である事が保証され、後から変更される値ではないと分かる事がメリットです。
変数定義について
- ミュータブル変数:後から値を変更できる変数
- イミュータブル変数:一度設定した値を変更できない変数
/**
* パスワード編集データ
*
* @property edit 編集モード
* @property id パスワードデータID
* @property title タイトル
* @property account アカウント
* @property password パスワード
* @property url サイトURL
* @property groupId グループID
* @property memo メモ
* @property inputDate 更新日付
*/
@Parcelize
data class PasswordEditData(
var edit: Boolean = false,
val id: Long,
val title: String,
val account: String,
val password: String,
val url: String,
val groupId: Long,
val memo: String,
val inputDate: String,
) : Parcelable
- varはミュータブル変数としての定義なので、途中の処理で値が変更される変数である事が定義を見ただけで分かる
- valはイミュータブル変数としての定義なので、コンストラクタで渡された値から不変である事が保証される
となるので、val変数については、調査するべきスコープが狭まるため影響範囲などの調査時間の削減に繋がります。
10.変数のスコープはprivateから始める
参照・書込されるのが、外部からなのか、クラス内だけなのかがスコープが絞れます。
なので、影響範囲の調査をする際にprivateならクラス内だけを調査すれば良くなるため、調査時間の削減に繋がります。
class GeneratePasswordDialogFragment : DialogFragment() {
/** パスワード自動生成種別 */
private var passwordKind = 0
/** パスワード自動生成文字数 */
private var passwordCount = 0
/** パスワード自動生が成小文字のみかどうか */
private var isLowerCaseOnly = false
/** 自動生成パスワード */
var generatePassword: String? = null
さいごに
ここまで読んでいただきありがとうございました。
コメントの記載やそのルールを守ること、命名規則や定数・変数の定義のルールを守ること、これらを気を付けるだけでもだいぶ読みやすいコードになったかと思います。
こういったルールが守られたコードは全体を俯瞰して眺めた時に、美しいコードに映ると思います。そして、
- 処理内容の理解のし易さ
- 可読性の向上
- 調査時間の短縮
といった効果を発揮するため生産性の向上に繋がっていくと思います。