CODE COMPLETE 上 第5章 コンストラクションにおける設計 5.3.7疎結合の維持
5.3.7疎結合の維持
結合度
クラスやルーチーンが他のクラスやルーチーンとどれだか結びついているかを表す。
良いモジュール間結合とは、あるモージュールが他のモジュールから簡単に使用できるくらい弱い(疎)結合であること。
他のモジュールに依存しないモジュールを作成しよう!
恋人同士ではなく、職場の冷めた関係くらいがちょうどいい。
結合の基準
モジュール間結合を評価する際の基準について紹介
サイズ
モジュールのインターフェイスが小さいほど良い。
使用するモジュールの引数は1つの方が6つより良い。
使用するクラスのパブリックルーチーンは4つの方が40個より良い。
可視性
2つのモジュール間結合が目に見えるということ。
グローバルデータを使うのはやめておきなさい。
柔軟性
モジュール間の結合はシンプルな方がいい。半田ごてと銅線を使うより、USBで接続できるぐらいがいい。
社員に与える休暇日数を雇用期間と職種情報から計算するLookupVacationBenefit()があるとする。
それとは別のモジュールが雇用日数と職種情報を持つemployeeオブジェクトを持ってて、LookupVacationBenefit()にemployeeオブジェクトを渡す設計だったとする。
もしemployeeオブジェクトを持ってないモジュールがLookupVacationBenefit()を使いたくなったらどうする?
無理やりemployeeオブジェクトを作る?
それより、LookupVacationBenefit()に雇用日数と職種情報の二つの引数を渡す様な設計の方が柔軟だ。
結合の種類
単純パラメータ結合 ←OK
ルーチーンの引数が全て基本的なデータ型のみ。
単純オブジェクト結合 ←OK
モジュール内でオブジェクトをインスタンス化すする場合。
オブジェクトパラメータ結合 ←あかんやつ
オブジェクト1がオブジェクト2を受け取る。
オブジェクト2の内部について知らなければならい。
セマンティック結合 ←あかんやつ
最も油断ならない結合。あるモジュールが別のモジュールのセマンティクス(一部の機能)を使用する場合に生じる。
例えば、モジュール1が制御フラグをモジュールに2に渡して、モジュール2ないの挙動を指示する。これはモジュール2の内部構造を熟知していなければならない。
使用される側のコードを変更したときに、思わぬ影響が起きる。
CODE COMPLETE 上 第7章 高品質なルーチーン 7.3良いルーチーン名
7.3良いルーチーン名
ルーチーンが行うことをすべて説明する
ルーチーン名には処理の内容すべてが分かる様な名前をつけなければならい。
例えば、環境変数の数をカウントして昇順に並べて出力する関数だとしたら、
CountEnvTotal()では不十分でCountEnvTotalAndSortAscending()の方が関数の処理を把握できる。しかし、冗長で稚拙に感じる。
そもそも、関数の中に二つの処理を混ぜると分かりにくい関数名になりがち。
関数一つで一つの処理を行う様にしよう。
意味のない動詞、曖昧な動詞、どっちつかずの動詞を使わない
例えば、 ProcessInput()、OutputUser()という名前からは、なんらかの入力を処理するんだろうなとかユーザの情報を出力するのかなぐらいの情報しかない。
こういう薄い名前を使っているときは、関数を細分化しすべてのルーチーンが目的化される様に構造を見直すべき。
ルーチーン名を数字だけで区別しない
OutputUser1(),OutputUser2()とかは絶対したらだめ。もってのほか。
必要な長さのルーチーン名にする
平均して最適なルーチーン名の長さは9文字〜15文字らしい。
ルーチーン名はオブジェクトと関連することが多いので、オブジェクト名を考慮しながら明確な名前を設定するべき。
関数名には戻り値の説明を反映させる
cos(),Printer.IsReady(), pen.CurrentColor()とかは良い例。
プロシージャ名には効果的な動詞とオブジェクトを使用する
※プロシージャとは関数みたいなもの。関数との違いについては7.6で説明がある。
オブジェクト自体が呼び出しに含まれるため、プロシージャ名の中にオブジェクトに関する情報が残ってると冗長になる。例えば、document.PrintDocument()とかは冗長。
check.Print()とかは小切手の印刷を連想させるのでグッド。
正確な反意語を使用する
add/remove, begin/end, create/destroy, firs/lastとか正確な反意語を意識する。
一般的な処理の規約をまとめる
各オブジェクトに一意な識別子を割り当てたが、識別子(id)を返すルーチーンを統一していなかった。例えば、
employee.id.Get()
candidate.id()
dependent.GetId()
各オブジェクトで識別子の取得方法がバラバラになってしまっている。
同じ様な関数は同じ様な形式に統一するべき。
CODE COMPLETE 上 第7章 高品質なルーチーン 7.2ルーチーンレベルでの設計
凝集度
cohesion: 強度とも言う
クラス設計においては凝集度はあまり関係ないが、ルーチーンの観点からは非常に重要。
凝集度とはルーチーン内の処理がどれだけ強く関連しているかを表す。
例えば、Cosine()という関数はルーチーンが一つの処理に徹しているので凝集度が高い。
CosineAndTan()は複数の処理が混ざっており、凝集度が弱い。一つのルーリーンは一つのことだけ処理できるようにするべき。
凝集度による効果
強い凝集度によって得られる効果は信頼性の向上。
凝集度の高いルーチーンのエラー発生率は50%だったのに対し、
凝集度の低いルーチーンのエラー発生率は82%だったという論文がある。
また、凝集度が低く密結合のルーチンのエラー発生率は凝集度が高く疎結合のルーチーンの7倍、修正コストは20倍になると報告されている。
段階別凝集度
- 機能的凝集度 良い 理想
- 情報的凝集度 ↑ 可能な限り小さく保つ
- 連絡的凝集度 ↑ 可能な限り小さく保つ
- 時間的凝集度 ↑ 可能な限り小さく保つ
- 手順的凝集度 ↑ 可能な限り小さく保つ
- 論理的凝集度 ↑ 可能な限り避けるべき
- 暗号的凝集度 悪い やっちゃだめ
上に行くほど良いとされています。
下に該当する関数はリファクタリングして、少しでも上の凝集度に寄せたほうがベター
機能的凝集度
ルーチーンの処理が一つだけ実行する場合。
sin(), GetCustomerName(), EraseFile()とかは一つのことしか実行しないと想像できる。
情報的凝集度
ルーチーンが決まった順番で実行しなければならない処理で構成され、それぞれの段階でデータを共有し、同時に実行したのでは機能を完全に達成できない場合。
例えば、APIでユーザ情報を取得し、描画する関数とか。
連絡的凝集度
ルーチーン内の処理で同じデータを使用するが、それ以外に関連性のない場合。
関数の引数として業務概要データを渡され、関数内で印刷する処理と、概要データを初期化する処理があったとする。
ここでは印刷する処理と、初期化する処理との間の共通点は同じデータを利用するということだけ。
時間的凝集度
タイミング的に近い処理を一つの関数にまとめたもの。
ログを初期化、DB初期化、ページ情報を取得、UIに表示みたいな一連の流れをすべて一つの関数にまとめたもの。
手順的凝集度
ルーチーンの処理が特定の順序で実行される場合。
ファイルのアクセス権を確認してから、書き込むとか。
論理的凝集度
複数の処理が一つのルーチーンに詰め込まれ、渡された制御フラグによっていずれかの処理が選択される場合。
暗号的凝集度
ルーチーン内の処理同士にそれとわかるような関係がない場合。
論理的凝集度、暗号的凝集度の使用は避ける。
低レベルの凝集度の関数を上レベルの凝集度に引き上げることを意識しながら関数を作るのが大事。
気になったので exportとenvの違い
exportとenvの違い
envコマンド
➜ env env | sort
Apple_PubSub_Socket_Render=/private/tmp/com.apple.launchd.fVQpnTWccq/Render
COLORFGBG=7;0
COLORTERM=truecolor
COMMAND_MODE=unix2003
C_INCLUDE_PATH=/Users/tosakai/.brew/include:
DISPLAY=/private/tmp/com.apple.launchd.dXXH6s45qm/org.xquartz:0
HOME=/Users/tosakai
ITERM_PROFILE=Default
ITERM_SESSION_ID=w0t0p0:CF6A5DCB-3D5D-4DCA-A5D3-903C94DDA6E0
LC_CTYPE=UTF-8
LC_TERMINAL=iTerm2
LC_TERMINAL_VERSION=3.3.6
LESS=-R
LIBRARY_PATH=/Users/tosakai/.brew/lib:
LOGNAME=tosakai
LSCOLORS=Gxfxcxdxbxegedabagacad
LS_COLORS=di=1;36:ln=35:so=32:pi=33:ex=31:bd=34;46:cd=34;43:su=30;41:sg=30;46:tw=30;42:ow=30;43
OLDPWD=/Users/tosakai/Documents/42cursus/minishell/training
PAGER=less
PATH=/Users/tosakai/.brew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/go/bin:/usr/local/munki:/opt/X11/bin
PWD=/Users/tosakai/Documents/42cursus/minishell/training/env
SECURITYSESSIONID=186bb
SHELL=/bin/zsh
SHLVL=1
SQLITE_EXEMPT_PATH_FROM_VNODE_GUARDS=/Users/tosakai/Library/WebKit/Databases
SSH_AUTH_SOCK=/private/tmp/com.apple.launchd.OMUMYeJc4D/Listeners
TERM=xterm-256color
TERM_PROGRAM=iTerm.app
TERM_PROGRAM_VERSION=3.3.6
TERM_SESSION_ID=w0t0p0:CF6A5DCB-3D5D-4DCA-A5D3-903C94DDA6E0
TMOUT=86400
TMPDIR=/var/folders/zz/zyxvpxvq6csfxvn_n000127c0008jv/T/
USER=tosakai
XPC_FLAGS=0x0
XPC_SERVICE_NAME=0
ZSH=/Users/tosakai/.oh-my-zsh
_=/usr/bin/env
__CF_USER_TEXT_ENCODING=0x223B:0x1:0xE
exportコマンド
➜ export export | sort
Apple_PubSub_Socket_Render=/private/tmp/com.apple.launchd.fVQpnTWccq/Render
COLORFGBG='7;0'
COLORTERM=truecolor
COMMAND_MODE=unix2003
C_INCLUDE_PATH=/Users/tosakai/.brew/include:
DISPLAY=/private/tmp/com.apple.launchd.dXXH6s45qm/org.xquartz:0
HOME=/Users/tosakai
ITERM_PROFILE=Default
ITERM_SESSION_ID=w0t0p1:B17045F0-5535-40A0-A000-1E7BA1780863
LC_CTYPE=UTF-8
LC_TERMINAL=iTerm2
LC_TERMINAL_VERSION=3.3.6
LESS=-R
LIBRARY_PATH=/Users/tosakai/.brew/lib:
LOGNAME=tosakai
LSCOLORS=Gxfxcxdxbxegedabagacad
LS_COLORS='di=1;36:ln=35:so=32:pi=33:ex=31:bd=34;46:cd=34;43:su=30;41:sg=30;46:tw=30;42:ow=30;43'
OLDPWD=/Users/tosakai/Documents/42cursus/minishell/training
PAGER=less
PATH=/Users/tosakai/.brew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/go/bin:/usr/local/munki:/opt/X11/bin
PWD=/Users/tosakai/Documents/42cursus/minishell/training/export
SECURITYSESSIONID=186bb
SHELL=/bin/zsh
SHLVL=1
SQLITE_EXEMPT_PATH_FROM_VNODE_GUARDS=/Users/tosakai/Library/WebKit/Databases
SSH_AUTH_SOCK=/private/tmp/com.apple.launchd.OMUMYeJc4D/Listeners
TERM=xterm-256color
TERM_PROGRAM=iTerm.app
TERM_PROGRAM_VERSION=3.3.6
TERM_SESSION_ID=w0t0p1:B17045F0-5535-40A0-A000-1E7BA1780863
TMOUT=86400
TMPDIR=/var/folders/zz/zyxvpxvq6csfxvn_n000127c0008jv/T/
USER=tosakai
XPC_FLAGS=0x0
XPC_SERVICE_NAME=0
ZSH=/Users/tosakai/.oh-my-zsh
__CF_USER_TEXT_ENCODING=0x223B:0x1:0xE
ITERM_SESSION_IDは異なるが、基本的に同じものが出力。
ちなみに、printenv | sortも同じ。
シェル変数を確認したいときはsetコマンドを実行。
シェル変数と環境変数の違い
シェル変数で設定した場合
➜ HOGE=FUGA
➜ echo $HOGE
FUGA
➜ cat test.sh
echo $HOGE
➜ sh test.sh
シェル内では認識されるが、子プロセスには引き継がれていない。
環境変数として設定した場合
➜ export HOGE=FUGA
➜ echo $HOGE
FUGA
➜ cat test.sh
echo $HOGE
➜ sh test.sh
FUGA
シェル内で認識されるし、子プロセスにも引き継がれている。
CODE COMPLETE 上 第7章 高品質なルーチーン 7.1ルーチーンを作成する理由
7.1ルーチーンを作成する理由
※ルーチーンとは1つの目的で実行される個々のメソッド、プロシージャのこと。
- 複雑さを低減する
- 中間部分をわかりやすく抽象化する
- コードの重複を避ける
- サブクラスを作成しやすくする
- 処理順序を隠蔽する
- ポインタ処理を隠蔽する
- 移植性を向上させる
- 複雑な論理評価を単純にする
- パフォーマンスの向上
- 全てのルーチーンが小さいことを保証する?
1 複雑さを低減する
ルーチーンを作成して内部を隠蔽してしまえば、詳細について考える必要は無くなる。
内部ルーチーンや条件の深いネストはルーチーンすべき兆候。
2 中間部分をわかりやすく抽象化する
コードの一部を分かりやすい名前のルーチーンにまとめると、その目的がうまく伝わる様になる。
if (node <> NULL) then
while (node.next <> NULL) do
node = node.next
leafName = node.name
end while
else
leafName = ""
end if
↓
leafName = GetLeafName(node)
こうすれば、関数名を見るだけで何をしているのかすぐ分かる。
3 コードの重複を避ける
ルーチーンの最も一般的な理由は、コードの重複を避けること。
重複を避ければ、その分スペースも圧縮できるし、コードの修正も1箇所だけすれば終わる。
4 サブクラスを作成しやすくなる
ルーチーンが十分に分解されていて短く保てていれば、十分に分解されていない長いコードをオーバーライドするより新たに書かなければならないコードは短くなる。
5 処理順序を隠蔽する
例えば、ユーザデータを取得するのと、ファイルからデータを取得することの順番を毎回明示する必要は無い。関数の中に隠蔽してしまう方がスッキリする。
6 ポインタ処理を隠蔽する
ポインタの処理は理解が難しくエラーの原因になりやすい。
ルーチーンにしてしまえば、ポインタへの扱いをルーチーンに任せてしまうことができる。
7 移植性を向上させる
移植性のない機能をまとめておくことで、いざ移植する際に取り組むべき範囲が明確になる。
8 複雑な論理評価を単純にする。
プログラムの流れを理解するのに複雑な論理評価を理解する必要は無い。関数として独立させると以下のメリットがある。
- 評価の詳細を関数内に移動できる。
- 評価の関数名で何をしてるのか明示できる。
9 パフォーマンスを向上させる
コードを複数の場所ではなく1箇所で最適化できる。効率の悪い部分を見つけるためのプロファイリングが容易になる。
10 全てのルーチーンが短いことを保証する?
そうとは言えない。これまでの理由があるのだから、それを理由にする必要は無い。
7.1.1 ルーチーンにするには単純すぎる?
2、3行のコードをルーチーンにするのは気が重いことがある。
でもルーチーンにしてしまえば、関数名で何をしてるのか一発で分かる。
また、もしコードの修正をする必要が出てきたら、一つのルーチーンの修正するだけで完了する。
はじめて読む486 3.3 メモリ管理 その2
メモリ管理の話の中の、今回はメモリ保護について。
- メモリ割り当て
- メモリ保護
- 仮想記憶
メモリ保護
メモリ保護とはタスク内で実行されるアプリケーションがアクセスできるメモリ領域を制限すること。
エンジニアのミスにより他のアプリケーションに割り当てられているメモリやオペレーティングシステムのメモリ領域にアクセスしたりすると予期せぬ結果やシステムクラッシュの原因となる。
アドレス変換機能
メモリ保護機能を実現するための前提知識として、アドレス変換機能について説明。
タスクAとタスクBの2つのタスクが存在すると仮定。
1,2,3,,,,,と順番に並んでいる値は、各タスクから見たデータが保存されているメモリ番号。その下にある7,13,5,,,,とあるのは本当のメモリ番号。タスクが切り替わる際に、Xさん(OS)がアドレス変換を行っている。
アドレス変換をOS側に任せることでプログラマーは複雑なメモリ管理を気にする必要は無いし、アクセスしてはいけないメモリ領域にアクセスすることができなくなる。
MMUの動き
あとで追記。。
はじめて読む486 3.3 メモリ管理 その1
オペレーティングシステムの主要な役割
- プロセス管理
- メモリ管理
- ファイルシステム
- 入出力管理
オペレーティングシステムの主要な役割のプロセス管理は以前書いたので、今回はメモリ管理の話。
ボリュームが多いので、いくつかに分けようと思います。
メモリ管理の話を3つに分けて書きたいけど、今回はメモリ割り当てについて。
- メモリ割り当て
- メモリ保護
- 仮想記憶
メモリ割り当て
マシンに搭載しているメモリのうち、オペレーシングシステムが使う領域をシステム領域と言い、アプリケーションが使う領域をユーザ領域と言う。
システム領域では、セグメントアドレスとかオフセット値とか属性情報を保存したり、ディスクリプタテーブルの保存に使ったりするんだろう。
昔のMS-DOSとかの場合はシングルタスクなので、一つのアプリケーションに全てのメモリが割り当てられるからメモリ管理の必要は無かった。今はマルチタスクが当たり前なので、メモリ管理機能が必要。
タスクが起動するときにはプログラムをロードするのに必要なメモリが割り当てられる。
追加でメモリが必要になったときはオペレーティングシステムに追加のメモリを依頼する。これがmallocなんだろう。
アプリケーションが割り当てられたメモリを開放したいときにオペレーティングシステムに依頼をすると開放される。これがfreeなのだろう。開放されたメモリの管理がオペレーティングシステムに戻る。
タスクが終了した際も、使っていたメモリはすべてオペレーティングシステムに管理が戻される。なので、タスク中にmallocで追加したメモリは都度オペレーティングシステムに戻したほうがベターなのだろうけど、容量を圧迫するほどじゃなければfreeしなくても、そこまで問題にはならないと思った。