モジュール結合度について復習した
プリンシプルオブプログラミング読んでなんとなく思い立ってモジュール結合度について考え直した
昔、基本情報技術者試験受けたときに勉強したときはただ暗記しただけだったけど改めて結合度について学んでみると「なるほどな…」って思うことが多かったので実際にクラスとか書いてまとめてみる
モジュール結合度とは
モジュール同士の関係の密接さを表す尺度……
と書いてあるがちょっと意味が難しい。クラス同士の影響度と言ったほうがわかりやすい。クラスの処理や値を変更すると他のクラスに影響を及ぼすものが結合度が高いということだろう。
モジュール結合度には以下の6種類があり、レベルが高いほど結合度が低い。
- レベル6データ結合
- レベル5 スタンプ結合
- レベル4 制御結合
- レベル3 外部結合
- レベル2 共通結合
- レベル1 内容結合
レベル6データ結合
スカラー型のデータを引数として渡す結合。
<?php class ModuleA { public function add($a,$b){ return $a + $b; } } class ModuleB{ public function doService(){ $moduleA= new ModuleA(); $result = $moduleA->add(1,2); //データ結合 // doSomething } }
ModuleAもModuleBもお互いに内部の実装に干渉しない(ブラックボックス化されている)ので、安心してプログラムの中を変更できる。
結合度が低いと言える。
ModuleAはModuleBとデータ結合してる状態。
レベル5 スタンプ結合
共通域にないデータ構造を受け渡しするような結合形態。
スカラー型ではなく、オブジェクトを引数として渡すと解釈して良さそう。
<?php class User{ private $id; private $name; public function __construct($id, $name) { $this->id = $id; $this->name = $name; } public function getId() { return $this->id; } public function setId($id) { $this->id = $id; } public function getName() { return $this->name; } public function setName($name) { $this->name = $name; } } class ModuleA { /** * @param $user User * @return string */ public function doService($user){ return " name : ".$user->getName(); } } class ModuleB{ public function doService(){ $user = new User(1,"masahide"); $moduleA = new ModuleA(); $result = $moduleA->doService($user); // doSomething } }
ModuleAもModuleBもお互いに内部の実装に干渉しない。
スタンプ結合の場合受け渡すデータの一部を使用しないことがある。
Userのnameは使うけどidは使わないというような状態。
レベル4 制御結合
呼び出し側のモジュールが、呼び出し先のモジュールの制御を指示するデータをパラメーターとして渡す方式。
よくある引数にflgとかtypeとかstatusを渡して呼び出し先の処理を制御するパターンと解釈してよさそう。
<?php class ModuleA { public function doHoge($type){ if($type === 1){ //doSomething }elseif ($type === 2){ //doSomething }elseif ($type === 3){ //doSomething }else{ //doSomething } } } class ModuleB{ public function doService(){ $moduleA = new ModuleA(); $moduleA->doHoge(2); // doSomething } }
ModuleBがModuleAを使用するときに、typeを渡すことによって内部の処理を制御してる。
データ結合や、スタンプ結合と違いモジュールの使用側が使用するモジュールの内部の実装を知っている必要があるので結合度が高くなると解釈した。
レベル3 外部結合
外部宣言したデータを共有したモジュール間の結合形式。
書籍や記事によってはグローバル変数のことであったり、publicな変数であったりと少し表現が違っている。
基本情報技術者試験に則って、グローバル変数の値を参照するパターンを外部結合としてみる。
<?php $GLOBALS['hoge'] = "hoge"; class ModuleA { public function doService(){ echo "moduleA : ".$GLOBALS['hoge']; } } class ModuleB{ public function doService(){ echo "moduleB : ".$GLOBALS['hoge']; } }
こんな感じかな… グローバル変数なのでどこで書き換えれるかわからず、書き換えられるとModuleA,Bともに影響を受けるので結合度は高い。
レベル2 共通結合
これもグローバル変数をモジュール間で共有するパターンだが、外部結合との違いは、データ結合とスタンプ結合の違いと同じように
データ構造を共有して不必要な値までも共有しているということ
<?php $GLOBALS['user'] = new User(1,"masahide"); class User{ private $id; private $name; public function __construct($id, $name) { $this->id = $id; $this->name = $name; } public function getId() { return $this->id; } public function setId($id) { $this->id = $id; } public function getName() { return $this->name; } public function setName($name) { $this->name = $name; } } class ModuleA { public function doService(){ echo "moduleA : ".$GLOBALS['user']->getName(); } } class ModuleB{ public function doService(){ echo "moduleB : ".$GLOBALS['user']->getName(); } }
これもグローバル変数を共有しているのでどこで書き換えれるかわからず、さらに本来不必要なデータまで共有している分外部結合よりさらに高くなる
レベル1 内容結合
モジュール同士の命令の一部を共有したり、外部宣言していないデータを直接参照したりしている状態
カプセル化できてないクラスや、リフレクション使って無理やり命令の一部を使っている状態と言ってよさそう
queryパラメーターをPHPで暗号化して渡してJavaで復元したい
とあるPHPで書かれたWebサービスAから、とあるJavaで書かれたWebサービスBにパラメーターを暗号化してqueryパラメーターで渡したいときがあったのでその時のメモ
PHP側の暗号・復号化を行うクラス
<?php class MyCipher { const SECRET_KEY = "1234567890123456"; //Javaと共通の秘密鍵。ランダウな文字列16桁 const IV_PARAMETER = "1234567890123456"; //Javaと共通のIV。ランダムな文字列16桁 const METHOD = "AES-128-CBC"; //256のほうが安全だが、Java側がデフォルトで256使えないので128にしてる /** * 受け取った文字列を暗号化しBase64+URLエンコードして返す * @param $originalStr * @return string */ public static function encrypt($originalStr) { return urlencode(openssl_encrypt($originalStr, self::METHOD, self::SECRET_KEY, false, self::IV_PARAMETER)); } /** * Base64+URLEncodeされている暗号化文字列を元の文字列に復号する * @param $encryptStr * @return string */ public static function decrypt($encryptStr){ return openssl_decrypt(urldecode($encryptStr), self::METHOD, self::SECRET_KEY, false, self::IV_PARAMETER); } } $encrypt = MyCipher::encrypt("abcdef"); //JxdRLkrOq5SDY9YMcUSUhg%3D%3D $decrypt = MyCipher::decrypt($encrypt); //abcdef echo $decrypt === "abcdef"; //復号される
Javaの暗号・復号化を行うクラス
public final class MyCipher { private static final String SECRET_KEY = "1234567890123456";// 秘密鍵は16文字で。PHP側と同じものを指定する private static final byte[] IV_Parameter = "1234567890123456".getBytes();//IVの値。PHP側と同じものを指定する private static final String ALGORITHM = "AES/CBC/PKCS5Padding";// 暗号化方式 /** * 文字列を16文字の秘密鍵でAES暗号化してBase64+URLEncodeした文字列で返す */ public static String encrypt(String originalSource) throws Exception { Cipher cipher = initCipher(Cipher.ENCRYPT_MODE); //文字列をバイト列に変換 byte[] originalBytes = originalSource.getBytes(); //暗号化後のバイト列取得 byte[] encryptBytes = cipher.doFinal(originalBytes); //バイト列を文字列にして返す return URLEncoder.encode(Base64.getEncoder().encodeToString(encryptBytes),"UTF-8"); } /** * Base64+URLEncodeされたAES暗号化文字列を元の文字列に復元する */ public static String decrypt(String encryptStr) throws Exception { //暗号化されたmd文字列からバイト列を取得 //JavaのUrlDecoderは「+」を半角スペースに変えてしまうので、「%2B」に置換する byte[] encryptBytes = Base64.getDecoder().decode(URLDecoder.decode(encryptStr.replace("+", "%2B"),"UTF-8")); Cipher cipher = initCipher(Cipher.DECRYPT_MODE); //復号化後にバイト列取得 byte[] originalBytes = cipher.doFinal(encryptBytes); //文字列にして返却 return new String(originalBytes); } private static Cipher initCipher(int method) throws InvalidAlgorithmParameterException, InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException { Cipher cipher = Cipher.getInstance(ALGORITHM); cipher.init(method,new SecretKeySpec(SECRET_KEY.getBytes(),"AES"),new IvParameterSpec(IV_Parameter)); return cipher; } }
public class Sample { public static void main(String[] args) throws Exception { String encrypt = MyCipher.encrypt("abcdef"); System.out.println(encrypt); //JxdRLkrOq5SDY9YMcUSUhg%3D%3D System.out.println(MyCipher.decrypt(encrypt)); //abcdef //復号化されてる } }
これでPHPとJavaで相互に値を受け渡しできるようになりました。
秘密鍵と初期ベクトルが外部に漏れない限りは復号できないと思われるので、まぁ十分に安全かと思います。
ただ不安はつきものなのであまり重要なデータは別の方法で受け渡ししたほうが良いかもしれない。
参考URL : stackoverflow.com
kotlinでワンソースのDIコンテナ試しに書いた
やっと仕事落ち着いてきたので軽く何かkotlin書きたいなあと思ってモヤモヤして、そういえば以前からDagger2も取り入れてAndroidアプリ開発したいなぁと思っていたが
なんかDagger2の使い方がややこしくて、逆に複雑度が増す感じがして個人で小規模な開発には正直メリットを見いだせなかった。純粋に勉強不足というものもあるがもっとゆるーいDIコンテナ欲しいなと考えたり他のDIコンテナってどんな感じなんだろうと思って色々調べてるうちに、PHPの軽量なDIコンテナのPimpleに出会った
なるほど……オブジェクトを生成する関数をDIコンテナに配列で保存しておいて必要になったらそのコンテナに保存しておいた関数でオブジェクトを生成するという感じか
これぐらいゆるい感じで軽量なら同じようなことがkotlinでもできそうだなと思い立ってとりあえず書いた
GitHub - masahide318/Kontainer
koRegister(String,()->Any)でオブジェクトの生成方法を保存しておき、koInject(String)でオブジェクトを生成して返す koInjectSingleton(String)で取得すれば、singletonとしてオブジェクトを取得できる 使い方はReadmeにも書いてある通り以下の様な感じ
class SampleObj(val id: Int, val name: String) { } koRegister("sample",{SampleObj(1,"hoge")}) //inject with new Object val sampleObj1:SampleObj = koInject("sample") val sampleObj2:SampleObj = koInject("sample") //別のインタンスとして取得できる println(sampleObj1 === sampleObj2) //false //inject with singleton val singleton1:SampleObj = koInjectSingleton("sample") val singleton2:SampleObj = koInjectSingleton("sample") //シングルトンとして同一オブジェクトが帰る println(singleton1 === singleton2) // true
Androidの開発で果たして使えるのか、恐らくApplicationクラスのonCreateでサービスクラス系をこいつにぶち込む感じになるんだろうが
まぁ試しに使って見ながら余裕があれば色々改良してみよう
DialogFragmentのshowとdismiss方法はコレに落ち着いた
ちょっと仕事でアプリのクラッシュログがすごい量出てきて調べてほしいと言われてとりあえずFabricのスタックトレースをみてみたら
「java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState」で落ちまくっている。なんとなく予想はしていたが
Fragmentの貼り付けているWebViewのonPageStartedでDialogFragmentをshow()して、onPageFinished()でdismiss()している箇所があり
ここの部分が半端無く落ちてる。まぁそうだろうな…WebViewのロードが終わる前に画面回転とかしたらこのExceptionが発生してそのまま即死コース一直線だ
kotlinでAndroidアプリを試しに作った
よくお世話になるクローディアのアプリのAndroid版をkotlinで書き直し中 最近あまりネイティブアプリを書くことがなくなってきたのでリハビリを兼ねて作ってみました。
とにかく最近のトレンドを追いかけるために
- retrofit
- databinding
- realm
- rxJava
この辺りは入れてみようと思いましたが、realmだけまだ入れてない。。。
Dagger2もまだ使ったことないので使ってみたかったが後回し
設計もいまトレンドのMVPでActivityを極力Viewの操作のみに専念し
PresenterがRepository層のクラスを使ってAPI通信を行ったり、PreferenceFileに書き込みを行ったりということをしています。少々雑ですが…
更に細かくやると、UseCase層も作ってそこにRepositoryとのやりとりをまとめたほうが抽象度は上がるのでしょうが、個人でやるにはそこまではやりすぎ感があったので
Presenterが色んなRepositoryとやりとりして結果をViewに知らせる形にしました。
テストコードもビジネスロジックはPresenter側に押し込めるのでPresenterを抑えておけばとりあえず大丈夫なはず。
View側のテストはInstrumentテストで頑張ればいいかなぁという感じ。
RepositoryとViewはInterfaceで実装を抽出してPresenterに渡しているのでmockを差し込むのもやりやすくて良かったです。
Androidのアレ!iOSでいうと
メモ書き程度のもの そのうち追加したり、もう少し詳しく書く
画面遷移(startActivity)
Android
Intent intent = new Intent(this,NextActivity.class); startActivity(intent);
iOS
stroyboardでViewControllerを「Control + ドラッグ」でつなげて SegueのIdentifierを定義する「toNextViewController」
self.performSegueWithIdentifier("toNextViewController",sender:nil)
画面遷移with値(intent.putExtra)
Android
Intent intent = new Intent(this,NextActivity.class); intent.putExtra("param","hoge"); startActivity(intent); //NextActivityで getIntent().getExtras().getString("param") //hoge
iOS
prepareForSegueを使います
class NextViewController:UIViewController{ var value1:Int = 0 var value2:String = "" }
func hoge(){ self.performSegueWithIdentifier("toNextViewController",sender:nil) } override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { let nextViewController = segue.destinationViewController as! NextViewController nextViewController.value1 = 123 nextViewController.value2 = "hogehoge" }
画面から戻ってきたとき(onActivityResult)
Android
public class FugaActivity extends Activity { private static final int REQUEST_CODE = 1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Intent intent = new Intent(this,NextActivity.class); startActivityForResult(intent, REQUEST_CODE); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if(resultCode == RESULT_OK && requestCode == REQUEST_CODE){ //doSomething } } }
iOS
StoryBorad Unwind segue でググる
pull refresh
Android
AndroidではSwipeRefreshLayoutの中にListViewを入れるのが一般的です
http://developer.android.com/intl/ja/samples/SwipeRefreshListFragment/index.html より
<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/swiperefresh" android:layout_width="match_parent" android:layout_height="match_parent"> <ListView android:id="@android:id/list" android:layout_width="match_parent" android:layout_height="match_parent" /> </android.support.v4.widget.SwipeRefreshLayout>
mSwipeRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.swiperefresh); mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { //必要に応じてtrue,falseを設定してあげる mSwipeRefreshLayout.setRefreshing(true); //何かしらの処理。多くの場合はAPI叩く } });
iOS
tableViewControllerを使わない場合
@IBOutlet weak var tableView: UITableView! let refreshControl:UIRefreshControl = UIRefreshControl() override func viewDidLoad() { super.viewDidLoad() //pull to refreshで実行するメソッド名を指定します refreshControl.addTarget(self, action: Selector("refresh"), forControlEvents: UIControlEvents.ValueChanged) tableView.addSubview(refreshControl) } func refresh(){ //doSomething self.refreshControl.endRefreshing() }
tableViewControllerを使っている場合 storyboardからTableViewのRefreshを「enable」にして
override func viewDidLoad() { super.viewDidLoad() self.refreshControl?.addTarget(self, action: "refresh:", forControlEvents: UIControlEvents.ValueChanged) } func refresh(){ //doSomething self.refreshControl.endRefreshing() }
これでいける模様
バックグラウンドから復帰したとき
Android
onResumeやonRestartなど、その時の用途にあったライフサイクルを選ぶ
iOS
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIApplication_Class/
override func viewDidLoad() { let notificationCenter = NSNotificationCenter.defaultCenter() //アプリがアクティブになったとき notificationCenter.addObserver( self, selector: "doHoge", name:UIApplicationDidBecomeActiveNotification, object: nil) } func doHoge(){ //doSomething }
そのほかにバックグラウンドに移行したときなど、notificaitonのtypeがいろいろあります
リストビューの最下部までスクロールしたときをハンドリング
Android
listView.setOnScrollListener(new AbsListView.OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { if (totalItemCount == firstVisibleItem + visibleItemCount) { //doSomething } } });
iOS
func scrollViewDidScroll(scrollView: UIScrollView) { //一番下までスクロールしたかどうか if(tableView.contentOffset.y >= (tableView.contentSize.height - tableView.bounds.size.height) && !nowLoading) { //doSomething } }