- 投稿日:2019-03-18T22:57:28+09:00
【Gang of Four】デザインパターン学習 - Interpreter
通訳
目次
このパターンは構造を理解するのに時間がかかりました…構文木を生成するのが目的のパターンでしょうか。
本パターンは開始タグから終了タグまでのひとまとまりを、一つのオブジェクトで表現します。
Compositeパターンを用いて解析し、まとまりごとに処理を行う感じですね。目的
言語に対して、文法表現と、それを使用して文を解釈するインタプリタを一緒に定義する。
構成要素
・AbstractExpression すべてのノードの抽象クラス
・TerminalExpression 終端を表現するクラス
・NonterminalExpression 終端以外を表現するクラス
・Context 文脈・状況
・Client 利用者実装
階層構造になっている四則演算用データを処理するサンプルプログラムを実装します。
<+> ~ </+> 足し算
<-> ~ </-> 引き算
<*> ~ </*> 掛け算
</> ~ <//> 割り算対象サンプルデータ.xml<+> 1 <-> 2 3 4 5 6 <*> 7 8 </*> 9 </-> 10 11 </> 12 4 <//> </+>上記サンプルデータを見慣れた形へ整形すると
1 + (2 - 3 - 4 - 5 - 6 - (7 * 8) - 9) + 10 + 11 + (12 / 4)
となり、解-56
が期待値です。AbstractExpression 命令・抽象的な表現
Expression.ktpackage interpreter interface Expression { enum class Operator(val startTag: String, val endTag: String) { Plus("<+>", "</+>"), Minus("<->", "</->"), Multiplication("<*>", "</*>"), Divide("</>", "<//>"), } fun interpret(context: Context): Int companion object { fun getExpression(token: String?): Expression? { return when (token) { Operator.Plus.startTag -> { PlusNonterminalExpression() } Operator.Minus.startTag -> { MinusNonterminalExpression() } Operator.Multiplication.startTag -> { MultiplicationExpression() } Operator.Divide.startTag -> { DivideExpression() } Operator.Plus.endTag, Operator.Minus.endTag, Operator.Multiplication.endTag, Operator.Divide.endTag -> { null } else -> { TerminalExpression() } } } } }TerminalExpression 終端となる表現
木の末端となる要素です。本サンプルデータでは数字がそれにあたります。TerminalExpression.ktpackage interpreter class TerminalExpression: Expression { private var saveToken: String? = null override fun interpret(context: Context): Int { saveToken = context.token context.nextToken() return Integer.parseInt(saveToken ?: "0") } override fun toString(): String { return saveToken ?: "0" } }NonterminalExpression 終端以外の表現
木が分岐する要素です。本サンプルデータでは<+>や<->がそれにあたります。演算クラス
CalcExpression.ktpackage interpreter import java.util.ArrayList abstract class CalcExpression: Expression { protected val numList = ArrayList<Int>() protected val list = ArrayList<Expression>() override fun interpret(context: Context): Int { context.nextToken() loop@ while (!context.isEnd) { val childExpressions = Expression.getExpression(context.token) if (childExpressions == null) { context.nextToken() break@loop } else { numList.add(childExpressions.interpret(context)) list.add(childExpressions) } } return calc() } abstract fun calc(): Int abstract override fun toString(): String }足し算クラス
PlusNonterminalExpression.ktpackage interpreter /** * 足し算(<+> ~ </+>) */ class PlusNonterminalExpression: CalcExpression() { override fun calc(): Int { var result = numList.first().toInt() for (i in 1 until numList.size) { result += numList[i] } return result } override fun toString(): String { return "+$list" } }引き算クラス
MinusNonterminalExpression.ktpackage interpreter /** * 引き算(<-> ~ </->) */ class MinusNonterminalExpression: CalcExpression() { override fun calc(): Int { var result = numList.first().toInt() for (i in 1 until numList.size) { result -= numList[i] } return result } override fun toString(): String { return "-$list" } }掛け算クラス
MultiplicationExpression.ktpackage interpreter /** * 掛け算(<*> ~ </"*>) */ class MultiplicationExpression: CalcExpression() { override fun calc(): Int { var result = numList.first().toInt() for (i in 1 until numList.size) { result *= numList[i] } return result } override fun toString(): String { return "*$list" } }割り算クラス
DivideExpression.ktpackage interpreter class DivideExpression: CalcExpression() { override fun calc(): Int { var result = numList.first().toInt() for (i in 1 until numList.size) { result /= numList[i] } return result } override fun toString(): String { return "/$list" } }Context 文脈・状況
Context.ktpackage interpreter import java.util.* class Context(source: String) { private val tokens: StringTokenizer = StringTokenizer(source) var token: String? = null private set val isEnd: Boolean get() = !tokens.hasMoreElements() init { nextToken() } fun nextToken() { var token: String? = null if (!isEnd) { token = tokens.nextToken() // 標準の .nextToken() を呼び出す } this.token = token } }Client 利用者
Main.ktpackage interpreter import interpreter.Expression.Companion.getExpression fun main(args: Array<String>) { val source = "<+> 1 <-> 2 3 4 5 6 <*> 7 8 </*> 9 </-> 10 11 </> 12 4 <//> </+>" val context = Context(source) val expression = getExpression(context.token) println(expression?.interpret(context)) println(expression.toString()) }[out-put] -56 +[1, -[2, 3, 4, 5, 6, *[7, 8], 9], 10, 11, /[12, 4]]期待通りの式、解となりました。
- 投稿日:2019-03-18T22:51:55+09:00
【Gang of Four】デザインパターン学習 - Command
コマンド
目次
本パターンの主旨は、要求をオブジェクト化することで動的に要求を編集できることと、一連の処理を一つのオブジェクトに纏め共通のインターフェースを用意することで保守管理を簡単にすることを実現する、でしょうか。よくこんなメソッドを見かけます。
Receiver.ktfun run(mode: ModeType) { when(mode) { ModeType.Begin -> { // 準備処理 loadProperty() startDriver() } ModeType.Running -> { // メイン処理 running() } ModeType.After -> { // 終了処理 saveState() stopDriver() } } } private fun loadProperty(){} private fun startDriver(){} private fun running(){} private fun saveState(){} private fun stopDriver(){}中断処理が必要になったため、
saveState()
のみ呼びだすモードがほしくなったのでモードと実装を追加します。Receiver.ktModeType.Interruption -> { // 中断処理 saveState() }また更に別のモードを…となってくると
Receiver
クラスを延々と修正しなければならず、Mode
もどんどん増えていきます。これを本パターンを適用することでReceiver
クラスを修正することなく、柔軟な振る舞いをさせることができるようになります。目的
要求をオブジェクトとしてカプセル化することによって、異なる要求や、要求からなるキューやログにより、クライアントをパラメータ化する。また、取り消し可能なオペレーションをサポートする。
構成要素
・Command Receiverクラスによって実行されるメソッドを定義する抽象クラス
・ConcreteCommand Commandクラスの具象クラス
・Client 使う人
・Invoker
・Receiver 命令の出し方を知っているクラス実装
Receiver 命令の出し方を知っているクラス
インターフェースとReceiver.ktpackage command interface Receiver { fun getName(): String }具象クラス
Car.ktpackage command class Car(private val name: String): Receiver { override fun getName(): String { return "レシーバは${name}オブジェクトです" } fun openDoor() { println("ドアを開ける") } fun engineStart() { println("エンジンスタート") } fun engineStop() { println("エンジンストップ") } fun lock() { println("ロックする") } fun unlock() { println("ロックを解除する") } fun pushAxelPedal() { println("アクセルを踏む") } fun pushBreakePedal() { println("ブレーキペダルを踏む") } }Command Receiverクラスによって実行されるメソッドを定義する抽象クラス
Command.ktpackage command interface Command { fun execute() }ConcreteCommand Commandクラスの具象クラス
SimpleCommand.ktpackage command class SimpleCommand(private val receiver: Receiver, private val method: String): Command { override fun execute() { receiver.javaClass.getDeclaredMethod(method).invoke(receiver) } }MacroCommand.ktpackage command import kotlin.collections.ArrayList class MacroCommand: Command { private val commandList = ArrayList<Command>() override fun execute() { commandList.forEach { it.execute() } } fun addCommand(command: Command) { commandList.add(command) } fun removeCommand(command: Command) { commandList.remove(command) } }Client
使う人
車のエンジンをかけるコマンドを作成します。Client.ktpackage command class Client { init { val receiver = Car("プリウス") createStartCarCommand(receiver).execute() } private fun createStartCarCommand(receiver: Car): Command { val startCarCommand = MacroCommand() startCarCommand.addCommand(SimpleCommand(receiver, "unlock")) startCarCommand.addCommand(SimpleCommand(receiver, "openDoor")) startCarCommand.addCommand(SimpleCommand(receiver, "engineStart")) return startCarCommand } }実行結果
[out-put] ロックを解除する ドアを開ける エンジンスタート車のエンジンをとめるコマンドが欲しくなりました。
Client.ktpackage command class Client { init { val receiver = Car("プリウス") createStopCarCommand(receiver).execute() } private fun createStopCarCommand(receiver: Car): Command { val stopCarCommand = MacroCommand() stopCarCommand.addCommand(SimpleCommand(receiver, "engineStop")) stopCarCommand.addCommand(SimpleCommand(receiver, "openDoor")) stopCarCommand.addCommand(SimpleCommand(receiver, "lock")) return stopCarCommand } }実行結果
[out-put] エンジンストップ ドアを開ける ロックする車を走らせるコマンドを作成したくなりました。
Client.ktpackage command class Client { init { val receiver = Car("プリウス") createCarRunCommand(receiver).execute() } private fun createCarRunCommand(receiver: Car): Command { return SimpleCommand(receiver, "pushAxelPedal") } }実行結果
[out-put] アクセルを踏む車のエンジンをかけ、しばらく走ったあとエンジンをとめるコマンドが必要になりました。
Client.ktpackage command class Client { init { val receiver = Car("プリウス") createStartAndRunAndStopCarCommand(receiver).execute() } private fun createStartAndRunAndStopCarCommand(receiver: Car): Command { val startAndRunAndStopCarCommand = MacroCommand() startAndRunAndStopCarCommand.addCommand(createStartCarCommand(receiver)) val runCommand = createCarRunCommand(receiver) startAndRunAndStopCarCommand.addCommand(runCommand) startAndRunAndStopCarCommand.addCommand(runCommand) startAndRunAndStopCarCommand.addCommand(runCommand) startAndRunAndStopCarCommand.addCommand(runCommand) startAndRunAndStopCarCommand.addCommand(createStopCarCommand(receiver)) return startAndRunAndStopCarCommand } private fun createStartCarCommand(receiver: Car): Command { val startCarCommand = MacroCommand() startCarCommand.addCommand(SimpleCommand(receiver, "unlock")) startCarCommand.addCommand(SimpleCommand(receiver, "openDoor")) startCarCommand.addCommand(SimpleCommand(receiver, "engineStart")) return startCarCommand } private fun createCarRunCommand(receiver: Car): Command { return SimpleCommand(receiver, "pushAxelPedal") } private fun createStopCarCommand(receiver: Car): Command { val stopCarCommand = MacroCommand() stopCarCommand.addCommand(SimpleCommand(receiver, "engineStop")) stopCarCommand.addCommand(SimpleCommand(receiver, "openDoor")) stopCarCommand.addCommand(SimpleCommand(receiver, "lock")) return stopCarCommand } }実行結果
[out-put] ロックを解除する ドアを開ける エンジンスタート アクセルを踏む アクセルを踏む アクセルを踏む アクセルを踏む エンジンストップ ドアを開ける ロックするGoF本の適用可能性を読むと、このパターンの肝は要求をオブジェクト化することによって任意の要求を取り消しできること、所謂
MacroCommand
クラスのremoveCommand
メソッドを呼び出すことらしいのですが、イマイチ必要になるシチュエーションが思い浮かびません。
- 投稿日:2019-03-18T22:27:39+09:00
【Gang of Four】デザインパターン学習 - Chain of Responsibility
責任の連鎖
目次
親子関係のあるオブジェクトにおいて、処理を行うオブジェクトを親子間で柔軟に決定することができるようにするのが本パターンか?目的
1つ以上のオブジェクトに要求を処理する機会を与えることにより、要求を送信するオブジェクトと受信するオブジェクトの結合を避ける。受信する複数のオブジェクトをチェーン状につなぎ、あるオブジェクトがその要求を処理するまで、そのチェーンに沿って要求を渡していく。
構成要素
・Handler 親子クラスの抽象クラス
・ConcreteHandler Handlerの具象クラス
・Client 使用者実装
画面を構成する部品で例えると下記のような親子関係があると思います。
ウィンドウ ----ウインドウの中に表示するダイアログ --------ダイアログの中にあるボタン ----ウインドウの中にあるボタンどれかのオブジェクトを操作して不具合が発生した場合に、対応処理を行うオブジェクトを自由に決められるサンプルコードを実装します。
Handler 親子クラスの抽象クラス
共通クラス
全部のオブジェクトのスーパークラスです。
Viewをインスタンス化した際にmessageTypeへNormal以外を設定したものは、当該オブジェクト内で不具合に対する処理を実行します。
Normalを設定したViewは親Viewへ処理を任せます。そのまた親Viewが...繰り返しView.ktpackage chainofresponsibility abstract class View(private val parent: View?, private val messageType: MessageType) { enum class MessageType { Normal, Warning, Danger } protected fun handleHelp() { if (hasMessage()) { helpLogic() } else { parent?.handleHelp() } } abstract fun helpLogic() private fun hasMessage(): Boolean { val ret = MessageType.Normal != messageType if (ret) { createDialog() } return ret } private fun createDialog() { when (messageType) { MessageType.Warning -> { print("警告ダイアログ:") } MessageType.Danger -> { print("エラーダイアログ:") } } } }ConcreteHandler Handlerの具象クラス
ウインドウ
Window.ktpackage chainofresponsibility class Window(parent: View?, messageType: View.MessageType): View(parent, messageType) { override fun helpLogic() { println("ウインドウに起因する不具合!") } }ダイアログ
Dialog.ktpackage chainofresponsibility class Dialog(parent: View?, messageType: View.MessageType): View(parent, messageType) { override fun helpLogic() { println("ダイアログに起因する不具合!") } }ボタン
actionメソッドに0以外を渡すと不具合が発生します。Button.ktpackage chainofresponsibility class Button(parent: View?, messageType: View.MessageType): View(parent, messageType) { fun action(arg: Int) { if (arg == 0) { println("正常終了") } else { handleHelp() } } override fun helpLogic() { println("ボタンに起因する不具合!") } }Client 使用者
action(1)で不具合が発生します。下記のサンプルコードではbutton1で発生した不具合はwindowがキャッチし、button2で発生した不具合はdialogがキャッチするようになっています。
dialogのメッセージタイプをNormalへ変更すればwindowがキャッチするようになります。Client.ktpackage chainofresponsibility class Client { init { // ウインドウ val window = Window(null, View.MessageType.Danger) // ウインドウ直下に配置されたボタン val button1 = Button(window, View.MessageType.Normal) // ウインドウ直下に配置されたダイアログ val dialog = Dialog(window, View.MessageType.Warning) // ダイアログに配置されたボタン val button2 = Button(dialog, View.MessageType.Normal) button1.action(0) button1.action(1) button1.action(0) button2.action(0) button2.action(1) button2.action(0) } }[out-put] 正常終了 エラーダイアログ:ウインドウに起因する不具合! 正常終了 正常終了 警告ダイアログ:ダイアログに起因する不具合! 正常終了このパターンはどうやろう…
パッと見ではそのうちどの親オブジェクトがハンドリングするのか管理しきれなくなって結局バグを生み出しそうですが、そもそもの主旨がメッセージを受信するオブジェクトを知る必要がないようにするパターンなので、それで良いんでしょうか。
うーん。
- 投稿日:2019-03-18T22:16:46+09:00
【Gang of Four】デザインパターン学習 - Proxy
代理
目次
動機の一つとして、生成と初期化にコストのかかるオブジェクトを使用する時(GUIアプリでいう画面に表示する時)に初めてインスタンス化する、といった場合に本パターンを適用します。ある画面内には1000枚画像を表示する必要がありますが、有効描画領域には10枚までしか表示することができません。にも関わらず初期表示時にGraphicsクラスのオブジェクトを1000個分インスタンス化し、画像を1000枚ロードしていては、初期表示にあまり関係のない処理にリソースを大きく割かなければならなくなります。
そういった課題を解消するために本パターンを用いるようです。本来Graphicsオブジェクトを割り当てておく領域にProxyを割り当てて置き、表示するタイミングになって初めて画像をロードします。
目的
あるオブジェクトへのアクセスを制御するために、そのオブジェクトの代理、または入れ物を提供する。
構成要素
・Proxy 影武者クラス
・Subject ProxyクラスとRealSubjectクラスの抽象クラス
・RealSubject 本物クラス実装
ではサンプルコードとして画像描画プログラムを実装します。実際に1000個分インスタンス化するのもめんどくさいので初期表示時の描画数を3とします。
Subject ProxyクラスとRealSubjectクラスの抽象クラス
Subject.ktpackage proxy interface Subject { fun draw() }RealSubject 本物クラス
イメージクラス
インスタンス化した瞬間画像を読み込むクラスImage.ktclass Image(private val filePath: String): Subject { init { loadImage(filePath) } override fun draw() { println("${filePath}を描画") } private fun loadImage(filePath: String) { println("${filePath}を読み込み") } }ではこのImageクラスを使用して画面を初期表示してみます。
クライアントクラス
Client.ktpackage proxy class Client { private val initDrawNum = 3 init { val imageList = getNonProxyImageList() for (i in 0 until initDrawNum) { imageList[i].draw() } } private fun getNonProxyImageList(): ArrayList<Subject> { val imageList = ArrayList<Subject>() imageList.add(Image("./image/りんご.png")) imageList.add(Image("./image/みかん.png")) imageList.add(Image("./image/もも.png")) imageList.add(Image("./image/ばなな.png")) imageList.add(Image("./image/ぱいなっぷる.png")) imageList.add(Image("./image/いちご.png")) return imageList } }[out-put] ./image/りんご.pngを読み込み ./image/みかん.pngを読み込み ./image/もも.pngを読み込み ./image/ばなな.pngを読み込み ./image/ぱいなっぷる.pngを読み込み ./image/いちご.pngを読み込み ./image/りんご.pngを描画 ./image/みかん.pngを描画 ./image/もも.pngを描画3つ表示するだけで良いものの全ての画像を読み込んでしまっています。Proxyクラスを利用してみます。
Proxy 影武者クラス
イメージProxyクラス
一度読み込んだものは永久に保持するようになっていますが、有効描画領域外になった場合、ロードした画像を解放するメソッドを実装したほうがより良いですね。しかし、めんどくさいので今回は省略します。
ImageProxy.ktpackage proxy class ImageProxy(private val filePath: String): Subject { var image: Image? = null override fun draw() { image?.let { unwrapImage -> unwrapImage.draw() } ?: run { val tmpImage = Image(filePath) tmpImage.draw() image = tmpImage } } }再びクライアントクラス。今回はProxyクラスを利用します。
Client.ktpackage proxy class Client { private val initDrawNum = 3 init { val imageList = getProxyImageList() for (i in 0 until initDrawNum) { imageList[i].draw() } } private fun getProxyImageList(): ArrayList<Subject> { val proxyImageList = ArrayList<Subject>() proxyImageList.add(ImageProxy("./image/りんご.png")) proxyImageList.add(ImageProxy("./image/みかん.png")) proxyImageList.add(ImageProxy("./image/もも.png")) proxyImageList.add(ImageProxy("./image/ばなな.png")) proxyImageList.add(ImageProxy("./image/ぱいなっぷる.png")) proxyImageList.add(ImageProxy("./image/いちご.png")) return proxyImageList } private fun getNonProxyImageList(): ArrayList<Subject> { val imageList = ArrayList<Subject>() imageList.add(Image("./image/りんご.png")) imageList.add(Image("./image/みかん.png")) imageList.add(Image("./image/もも.png")) imageList.add(Image("./image/ばなな.png")) imageList.add(Image("./image/ぱいなっぷる.png")) imageList.add(Image("./image/いちご.png")) return imageList } }[out-put] ./image/りんご.pngを読み込み ./image/りんご.pngを描画 ./image/みかん.pngを読み込み ./image/みかん.pngを描画 ./image/もも.pngを読み込み ./image/もも.pngを描画今度は表示する分だけロードできるようになりました。
- 投稿日:2019-03-18T22:06:55+09:00
【Gang of Four】デザインパターン学習 - Fly Weight
軽い物
目次
書中では何やら難しいことが色々と書いてありますが、要するに同じオブジェクトを複数回作成していると色々な面で高コストになるので、Factoryクラスを作成しインスタンスを生成するのは一度きりとすることでそれを回避しましょうね。というのが本パターンの主旨です。目的
多数の細かいオブジェクトを効率よくサポートするために共有を利用する。
構成要素
・Flyweight 同一インスタンスを大量に生成する必要があるオブジェクトの抽象クラス
・ConcreteFlyweight Flyweightの具象クラス
・UnsharedConcreteFlyweight Flyweightクラスを管理するクラス 行、列など子としてFlyweightクラスを保持するクラス
・FlyweightFactory Flyweightクラスを生成するクラス
・Client 使用者実装
※UnsharedConcreteFlyweightに相当するクラスはありません。
文字オブジェクト管理するサンプルコードを書きます。書中のサンプルコードも文字オブジェクトが例になっているので同じようなものになってしまいますが。Flyweight 同一インスタンスを大量に生成する必要があるオブジェクトの抽象クラス
ConcreteFlyweight Flyweightの具象クラス
文字クラス
文字を1文字管理するクラスCharacter.ktpackage flyweight class Character(private val character: Char) { fun print() { print(character) } }FlyweightFactory Flyweightクラスを生成するクラス
文字製造クラス
文字クラスのインスタンスを生成、プールするファクトリークラスを作成します。
当該クラスにて一度生成したオブジェクトをプールすることにより、以前生成したことのあるオブジェクトを再度生成しない。CharacterFactory.ktpackage flyweight // シングルトンオブジェクトにする object CharacterFactory { val characterPool = HashMap<AllCharacter, Character>() enum class AllCharacter(val value: Char) { A('A'), B('B'), C('C'), D('D'), E('E'), F('F'), G('G'), H('H'), I('I'), J('J'), K('K'), L('L'), M('M'), N('N'), O('O'), P('P'), Q('Q'), R('R'), S('S'), T('T'), U('U'), V('V'), W('W'), X('X'), Y('Y'), Z('Z') } fun getCharacterObject(characterType: AllCharacter): Character { // 未生成であれば、当該インスタンスをプールする。 characterPool[characterType]?.let { return it } ?: run { val char = Character(characterType.value) characterPool[characterType] = char return char } } }Client 使用者
文字を使う人
BANANAを生成します。Client.ktpackage flyweight class Client { init { val characters = ArrayList<Character>() // "BANANA"を生成します characters.add(CharacterFactory.getCharacterObject(CharacterFactory.AllCharacter.B)) characters.add(CharacterFactory.getCharacterObject(CharacterFactory.AllCharacter.A)) characters.add(CharacterFactory.getCharacterObject(CharacterFactory.AllCharacter.N)) characters.add(CharacterFactory.getCharacterObject(CharacterFactory.AllCharacter.A)) characters.add(CharacterFactory.getCharacterObject(CharacterFactory.AllCharacter.N)) characters.add(CharacterFactory.getCharacterObject(CharacterFactory.AllCharacter.A)) characters.forEach { it.print() } } }[output] BANANA
- 投稿日:2019-03-18T21:54:30+09:00
【Gang of Four】デザインパターン学習 - Facade
外見
目次
クライアントがあるサブシステムを利用する場合、そのままでは複雑なインタフェースを最低限必要なインターフェースのみにまとめるようなパターンです。
当該サブシステムをカスタマイズするようなことがない場合、Facadeパターンを適用したインターフェースのみ注視することによりサブシステムを利用しやすくさせます。要はクライアントとサブシステムの結合度を弱めるためですね。サブシステムを変更する際も結合度が弱いためし易くなります。
目的
サブシステム内に存在する複数のインタフェースに1つの統一インタフェースを与える。Facade パターンはサブシステムの利用を容易にするための高レベルインタフェースを定義する。
構成要素
・Facade クライアントがサブシステムを利用するにあたって必要最低限のインターフェースを定義するクラス
・サブシステム内のクラス 利用するサブシステム構成要素のうちサブシステム内のクラスだけ何故ちゃんとした名前がついていないんでしょう…
実装
窓口を新たに作成するだけですがサンプルコードを書きます。
サブシステム内のクラス 利用するサブシステム
プリンタークラス インターフェース色々Printer.ktpackage facade class Printer { fun startUp() { print("起動") } fun shutDown() { print("終了") } fun setToner(color: String) { print("トナーを${color}色に設定します。") } fun addPaper(num: Int) { print("紙を${num}枚追加します。") } fun login(name: String) { print("ユーザ${name}がログインしました。") } fun printOut(num: Int) { print("${num}枚印刷します。") } }クライアントがプリンターを操作する上で必要なのは印刷することだけなので、それのみの窓口を用意します。
Facade クライアントがサブシステムを利用するにあたって必要最低限のインターフェースを定義するクラス
インターフェースを限定FacadePrinter.ktpackage facade class FacadePrinter { private val printer = Printer() fun printOut(num: Int) { printer.printOut(num) } }クライアント
Client.ktpackage facade class Client { init { val printer = FacadePrinter() printer.printOut(15) // 他のメソッドは使用できない // printer.startUp() // pritner.shutDown() } }結果といっても大したものはないですが
[output] 15枚印刷します。なんかこれは自分の認識が間違っちゅう気がする。コメントにてご指摘ください。
- 投稿日:2019-03-18T21:51:47+09:00
【Gang of Four】デザインパターン学習 - Decorator
装飾者
目次
いきなり話が脇道に逸れますが、デザインパターンやオブジェクト指向に関する書籍を読んでいるとよくオブジェクトに対する責務や責任といった言葉がでてきます。
これは当該オブジェクトにて出来なければならないことを指しています。
また単一責務の原則というのがありますが、これはクラスを変更する理由は1つであるべき、というものです。例えば以下のようなクラスは修正する理由が2つあります。
- 画面遷移処理を追加したい→画面に関する変更理由
- 引き算処理を追加した→計算に関する変更理由画面計算.ktinterface 画面計算 { fun 描画(): Boolean fun 足し算(num1: Int, num2: Int): Int }単一責務の原則に則ると以下のように分離すべきです。
画面.ktinterface 画面 { fun 描画(): Boolean fun 画面遷移(): Boolean }計算.ktinterface 計算 { fun 足し算(num1: Int, num2: Int): Int fun 引き算(num2: Int, num2: Int): Int }この単一責務の原則というのも、やはり変更に対して柔軟に対応するための考えだと理解しています。脇道話は以上です。
では本題、目的に責任を動的に追加するとあるので、継承を用いずにオブジェクト(インスタンス)に任意のタイミングで機能を追加していくようなパターンですね。
目的
オブジェクトに責任を動的に追加する。Decorator パターンは、サブクラス化よりも柔軟な機能拡張方法を提供する。
構成要素
・Component 責任を動的に追加できるインターフェースを定義した抽象クラス
・ConcreteComponent Componentクラスの具象クラス
・Decorator Componentクラスに責任を追加するインターフェースを定義した抽象クラス
・ConcreteDecorator Decoratorクラスの具象クラス実装
テキストビューインスタンスを生成するタイミングで、いろいろな機能を追加できるプログラムを実装します。
Component 責任を動的に追加できるインターフェースを定義した抽象クラス
Decorator Componentクラスに責任を追加するインターフェースを定義した抽象クラス
ViewコンポーネントインターフェースViewComponent.ktpackage decorator interface ViewComponent { fun draw() }ConcreteComponent Componentクラスの具象クラス
テキストビューTextView.ktpackage decorator class TextView: ViewComponent { override fun draw() { print("【テキストビュー】") } }ConcreteDecorator Decoratorクラスの具象クラス
影をつけるデコレータShadowDecorator.ktpackage decorator class ShadowDecorator(viewComponent: ViewComponent): ViewComponent { val viewComponent = viewComponent override fun draw() { viewComponent.draw() addShadow() } private fun addShadow() { print(":影付き") } }ConcreteDecorator Decoratorクラスの具象クラス
スクロールさせるデコレータScrollDecorator.ktpackage decorator class ScrollDecorator(viewComponent: ViewComponent): ViewComponent { val viewComponent = viewComponent override fun draw() { viewComponent.draw() addScroll() } private fun addScroll() { print(":スクロール可能") } }部品が全部できました。使っていきましょう。
使う人
ComponentManager.ktpackage decorator class ComponentManager { init { // スクロール可能 影付き テキストビュー val scrollShadowTextView = ScrollDecorator(ShadowDecorator(TextView())) // スクロール可能 テキストビュー val scrollTextView = ScrollDecorator(TextView()) // 描画 scrollShadowTextView.draw() print("\n") scrollTextView.draw() } }実行すると以下のようになります。
[output] 【テキストビュー】:影付き:スクロール可能 【テキストビュー】:スクロール可能
- 投稿日:2019-03-18T21:48:52+09:00
【Gang of Four】デザインパターン学習 - Composite
複合
目次
クラスを木構造で組み立てる。とあるように、compositeとleafと呼ばれるオブジェクトで構成するパターンのようです。目的
部分―全体階層を表現するために、オブジェクトを木構造に組み立てる。Composite パターンにより、クライアントは、個々のオブジェクトとオブジェクトを合成したものを一様に扱うことができるようになる。
構成要素
・Component 根、節、葉の抽象クラス
・Leaf 葉
・Composite 根または節
・Client 使用者実装
木構造と聞くとディレクトリツリーが思い浮かびますね。
ということでディレクトリツリーを管理、出力できるプログラムを実装します。directoryがcompositeでfileがleafですね。
Component 根、節、葉の抽象クラス
directoryとfileのインターフェースElement.ktpackage composite interface Element { enum class ElementType(val type: String) { DIRECTORY("Direcotry"), FILE("File") } fun getType(): ElementType fun getName(): String }Composite 根または節
directory抽象クラスelementListを持たせ、自分の配下にいるElementを管理できるようにします。
AbstractDirectory.ktpackage composite abstract class AbstractDirectory: Element { var elementList: MutableList<Element> = mutableListOf() abstract fun addElement(element: Element) }directory具象クラス
Directory.ktpackage composite class Directory(private val name: String): AbstractDirectory() { override fun getType(): Element.ElementType { return Element.ElementType.DIRECTORY } override fun addElement(element: Element) { elementList.add(element) } override fun getName(): String { return name } }Leaf 葉
file具象クラスFile.ktpackage composite class File(private val name: String): Element { override fun getType(): Element.ElementType { return Element.ElementType.FILE } override fun getName(): String { return name } }Client 使用者
ディレクトリツリーを管理する人DirectoryManager.ktpackage composite class DirectoryManager { init { // ルートフォルダ val rootDirectory = Directory("/") rootDirectory.addElement(File("sample.config")) rootDirectory.addElement(File("gof.env")) // iOSアプリ用フォルダ val iosDirectory = Directory("iOS") val iosAppDirectory = Directory("GofiOSApp") iosAppDirectory.addElement(File("xcode.project")) val iosAppSourceDirectory = Directory("Source") iosAppSourceDirectory.addElement(File("hoge.swift")) iosAppSourceDirectory.addElement(File("fuga.swift")) iosDirectory.addElement(iosAppDirectory) iosAppDirectory.addElement(iosAppSourceDirectory) rootDirectory.addElement(iosDirectory) // Androidアプリ用フォルダ val androidDirectory = Directory("AndroidOS") val androidAppDirectory = Directory("GofAndroidApp") androidAppDirectory.addElement(File("AndroidManifest.xml")) val androidAppSourceDirectory = Directory("Source") androidAppSourceDirectory.addElement(File("moge.kt")) androidAppSourceDirectory.addElement(File("unbaba.kt")) androidDirectory.addElement(androidAppDirectory) androidAppDirectory.addElement(androidAppSourceDirectory) rootDirectory.addElement(androidDirectory) show(rootDirectory, 0) } private fun show(element: Element, indent: Int) { if (element is Directory) { println("${"----".repeat(indent)}【${element.getType().type}】${element.getName()}") element.elementList.forEach { show(it, indent + 1) } } else { println("${"----".repeat(indent)}【${element.getType().type}】${element.getName()}") } } }以上でcompositeパターンを用いたディレクトリツリーの完成です。
[output] 【Direcotry】/ ----【File】sample.config ----【File】gof.env ----【Direcotry】iOS --------【Direcotry】GofiOSApp ------------【File】xcode.project ------------【Direcotry】Source ----------------【File】hoge.swift ----------------【File】fuga.swift ----【Direcotry】AndroidOS --------【Direcotry】GofAndroidApp ------------【File】AndroidManifest.xml ------------【Direcotry】Source ----------------【File】moge.kt ----------------【File】unbaba.kt/iOS/GofIosApp/Sourceフォルダを同じ階層へ複製したくなった場合、
iosAppDirectory.addElement(iosAppSourceDirectory)
を追加します。[output] 【Direcotry】/ ----【File】sample.config ----【File】gof.env ----【Direcotry】iOS --------【Direcotry】GofiOSApp ------------【File】xcode.project ------------【Direcotry】Source ----------------【File】hoge.swift ----------------【File】fuga.swift ------------【Direcotry】Source ----------------【File】hoge.swift ----------------【File】fuga.swift 略ディレクトリを自由自在に操れてます。
kotlinは文字列操作なんかも簡潔に書けていいですね。
- 投稿日:2019-03-18T21:45:51+09:00
【Gang of Four】デザインパターン学習 - Bridge
橋
目次
通常、抽出できる部分を抽象クラスとし、それ以外の部分を抽象クラスを継承した具象クラスで実装します。抽象クラスと具象クラスを分離し抽象クラスの実装を実行時に決定できるようにするのが本パターンです。
アイコンに関する情報を管理するアイコンクラスを例とします。
最初にモノクロアイコンクラス(抽象クラス)とモノクロアイコンを様々な大きさで表示させるための大モノクロアイコンクラス(具象クラス)、中モノクロアイコンクラス(具象クラス)、小モノクロアイコンクラス(具象クラス)を実装します。
しかしクライアントからの予定外の仕様変更により、途中でRGBを自由に設定できるカラーアイコンクラス(抽象クラス)を実装しなければならなくなりました。もちろんアイコンはモノクロアイコンと同様大、中、小の3種類を表示する仕様も満たしていなければいけません。抽象クラスと具象クラスが継承により永続的に結合されているとカラーアイコンクラスを継承した大カラーアイコンクラス、中カラーアイコンクラスを…略
といったようにカラーアイコンクラスを継承した全ての大~小アイコンを作成しなければならなくなります。さらにクリアアイコンを追加するときはまた同様に…略
DBで多対多のリレーショナルがあるテーブルを、中間テーブルを作成して正規化するのに近い考え方かな?
目的
抽出されたクラスと実装を分離して、それらを独立に変更できるようにする。
構成要素
・Abstraction 抽象クラス
・RefinedAbstraction Abstractionクラスを拡張したクラス
・Implementor 具象クラス
・ConcreteImplementor Implementorクラスを拡張したクラス実装
まず抽象クラスのモノクロアイコンクラスとカラーアイコンクラスを実装します。
Abstraction 抽象クラス
抽象アイコンクラス
このクラスが所謂本パターンの肝、抽象クラスと具象クラスの橋になります。AbstIcon.ktpackage bridge abstract class AbstIcon(iconType: IconType) { enum class IconType(val value: String) { BlackAndWhite("モノクロアイコン"), Color("カラーアイコン") } private var bigIcon: ImpIcon = BigIcon(iconType) private var middleIcon: ImpIcon = MiddleIcon(iconType) private var smallIcon: ImpIcon = SmallIcon(iconType) abstract fun getType(): String fun getBigIcon(): ImpIcon { return bigIcon } fun getMiddleIcon(): ImpIcon { return middleIcon } fun getSmallIcon(): ImpIcon { return smallIcon } }RefinedAbstraction Abstractionクラスを拡張したクラス
モノクロアイコン抽象クラスAbstBlackAndWhiteIcon.ktpackage bridge class AbstBlackAndWhiteIcon: AbstIcon(IconType.BlackAndWhite) { override fun getType(): String { return AbstIcon.IconType.BlackAndWhite.value } // モノクロアイコン独自処理色々 }カラーアイコン抽象クラス
AbstColorIcon.ktpackage bridge class AbstColorIcon: AbstIcon(IconType.Color) { override fun getType(): String { return AbstIcon.IconType.Color.value } // カラーアイコン独自処理色々 }続いて具象クラスである大アイコン、中アイコン、小アイコンの作成
Implementor 具象クラス
具象アイコンインターフェースImpIcon.ktpackage bridge interface ImpIcon { enum class IconSize(val value: String) { Big("大アイコン"), Middle("中アイコン"), Small("小アイコン") } fun getIcon(): String }ConcreteImplementor Implementorクラスを拡張したクラス
大、中、小アイコンクラスBigIcon.ktpackage bridge class BigIcon(iconType: AbstIcon.IconType): ImpIcon { private val iconType = iconType override fun getIcon(): String { return "【タイプ】:" + iconType.value + "【サイズ】:" + ImpIcon.IconSize.Big.value } }MiddleIcon.ktpackage bridge class MiddleIcon(iconType: AbstIcon.IconType): ImpIcon { private val iconType = iconType override fun getIcon(): String { return "【タイプ】:" + iconType.value + "【サイズ】:" + ImpIcon.IconSize.Middle.value } }SmallIcon.ktpackage bridge class SmallIcon(iconType: AbstIcon.IconType): ImpIcon { private val iconType = iconType override fun getIcon(): String { return "【タイプ】:" + iconType.value + "【サイズ】:" + ImpIcon.IconSize.Small.value } }各アイコンを使う人
Client.ktpackage bridge class Client { init { val colorIcon = AbstColorIcon() println(colorIcon.getType()) println(colorIcon.getBigIcon().getIcon()) println(colorIcon.getMiddleIcon().getIcon()) println(colorIcon.getSmallIcon().getIcon()) val blackAndWhiteIcon = AbstBlackAndWhiteIcon() println(blackAndWhiteIcon.getType()) println(blackAndWhiteIcon.getBigIcon().getIcon()) println(blackAndWhiteIcon.getMiddleIcon().getIcon()) println(blackAndWhiteIcon.getSmallIcon().getIcon()) } }以上でアイコンを用いたサンプルコードの実装が完了です。
[output] カラーアイコン 【タイプ】:カラーアイコン【サイズ】:大アイコン 【タイプ】:カラーアイコン【サイズ】:中アイコン 【タイプ】:カラーアイコン【サイズ】:小アイコン モノクロアイコン 【タイプ】:モノクロアイコン【サイズ】:大アイコン 【タイプ】:モノクロアイコン【サイズ】:中アイコン 【タイプ】:モノクロアイコン【サイズ】:小アイコンここで背景が透明なクリアアイコンを追加してみます。従来の方法では大クリアアイ…略ですが、本パターンでは
AbstIconクラス
を継承したAbstClearIconクラス
を実装するだけで実現できるようになっています。AbstClearIcon.ktpackage bridge class AbstClearIcon: AbstIcon(IconType.Clear) { override fun getType(): String { return AbstIcon.IconType.Clear.value } // クリアアイコン独自処理色々 }Clientクラスに下記コードを追加したあと再び実行してみます。
Client.ktval clearIcon = AbstClearIcon() println(clearIcon.getType()) println(clearIcon.getBigIcon().getIcon()) println(clearIcon.getMiddleIcon().getIcon()) println(clearIcon.getSmallIcon().getIcon())[output] カラーアイコン 【タイプ】:カラーアイコン【サイズ】:大アイコン 【タイプ】:カラーアイコン【サイズ】:中アイコン 【タイプ】:カラーアイコン【サイズ】:小アイコン モノクロアイコン 【タイプ】:モノクロアイコン【サイズ】:大アイコン 【タイプ】:モノクロアイコン【サイズ】:中アイコン 【タイプ】:モノクロアイコン【サイズ】:小アイコン クリアアイコン 【タイプ】:クリアアイコン【サイズ】:大アイコン 【タイプ】:クリアアイコン【サイズ】:中アイコン 【タイプ】:クリアアイコン【サイズ】:小アイコン簡単に拡張することができました。
これで抽象クラス側がいくら増えても、具象クラス側がいくら増えても用意に実装することが可能になりました。
- 投稿日:2019-03-18T21:42:52+09:00
【Gang of Four】デザインパターン学習 - Adapter
適合させる人[物]
目的
あるクラスのインタフェースをクライアントが求める他のインタフェースへ変換する。Adapterパターンはインタフェースに互換性のないクラス同士を組み合わせることができるようにする。
構成要素
・Target 組み合わせたあとのクラスのインターフェースを定義する。
・Client 利用者クラス
・Adaptee 組み合わせたいクラス
・Adapter Adapteeクラスを組み合わせて作成したクラス実装
目的を読んで「そんなことできるがやろか」と思っていたらあるクラスに多重継承させて色々なクラスをまとめるような感じでした。GoF本のサンプルコードは基本的にC++で記述されているため、kotlin(もといjava)では実現できないようです。構造に関するパターンの一発目なのに。
色々調べてみるとjavaでadapterパターンを実現している(しようとしている?)ページが幾つかありました。
is-a関係
またはhas-a関係
の二つで実現することができる。とどのページでも書かれていましたがそれは本当にadapterパターンなのか…?と思います。例えば
TextViewクラス
とRectクラス
とColorBackgroundクラス
というのが標準ツールキットに含まれているけれど、今から作るシステムには角を丸くできて背景色を変更できるテキストビューが必要なんだ!!!というようなときに本パターンを用いるもんだと私は認識しています。もしjavaで実現できるならば、下記のようなクラスができます。
※kotlinだとextendsとimplementsの見分けがつきにくいのでjavaでコーディングします。ColorRectTextView.javapublic class ColorRectTextView extends TextView, Rect, ColorBackground { // 角丸にしたり、背景色変更したりするメソッド }がしかし、javaは多重継承を許しません。これを
is-a関係
やhas-a関係
で実現するとなるとIsAColorRectTextView.javapublic class IsAColorRectTextView extends TextView implements RectInterface, ColorBackgroundInterface { // RectInterfaceとColorBackgroundInterfaceで定義されたメソッドを実装する }のような形や
HasAColorRectTextView.javapublic class HasAColorRectTextView extends TextView { private Rect rect; private ColorBackground colorBackGround; public HasAColorRectTextView(Rect rect, ColorBackground colorBackGround) { this.rect = rect; this.colorBackGround = colorBackGround; } public void rectMethod() { rect.rectMethod() } public void colorBackGroundMethod() { colorBackGround.colorBackGroundMethod() } }のような形になります。果たしてこれをデザインパターンと呼べるのか…?
- 投稿日:2019-03-18T21:40:26+09:00
【Gang of Four】デザインパターン学習 - Singleton
1枚札
目次
生成に関するパターンの最後はシングルトンです。これは誰にとっても馴染みのあるパターンかと思います。自分も一番最初に知ったデザインパターンはシングルトンだったような気がする。DB接続を提供するクラスやアセットを取得するようなクラスは大体このパターンで実装されていますね。プログラム中で当該クラスのインスタンスの唯一性を保証します。
目的
あるクラスに対してインスタンスが1つしか存在しないことを保証し、それにアクセスするためのグローバルな方法を提供する。
構成要素
・Singleton インスタンスが1つしか生成されないクラス
実装
本パターンはkotlinでコーディングすると下記のように鬼シンプルな形になってしまいよくわからないため、javaでコーディングします。
SingletonObject.ktpackage singleton object SingletonObject { var str = "" fun print() { println("出力結果:$str") } }複数の画面からシングルトンオブジェクトを介して画像や音楽ファイルを作成します。
Singleton インスタンスが1つしか生成されないクラス
シングルトンオブジェクトJavaSingletonObject.javapackage singleton; public class JavaSingletonObject { /** * staticで定義し自クラスのインスタンスを唯一とする */ public static JavaSingletonObject instance = new JavaSingletonObject(); private String address = "/assets/temp"; enum Assets { IMAGE1("image_1"), IMAGE2("image_2"), AUDIO1("audio_1"), AUDIO2("audio_2"); private final String value; Assets(final String value) { this.value = value; } } /** * コンストラクタをprivateで定義し、新たなインスタンスを生成できないようにする。 */ private JavaSingletonObject(){ System.out.println("接続:" + address); } public String getAssets(Assets target) { System.out.println("シングルトンオブジェクトで資材取得:" + target.value); return target.value; } }画面1クラス
DisplayOne.ktpackage singleton class DisplayOne { fun draw() { println("User1で${JavaSingletonObject.instance.getAssets(JavaSingletonObject.Assets.IMAGE1)}を描画!!") } }画面2クラス
DisplayTwo.ktpackage singleton class DisplayTwo { fun start() { println("User2で${JavaSingletonObject.instance.getAssets(JavaSingletonObject.Assets.AUDIO1)}を再生!!") } }使用者
画面管理クラスManager.ktpackage singleton class Manager { init { DisplayOne().draw() DisplayTwo().start() } }以上でシングルトンパターンの実装が完了しました。
JavaSingletonObject
クラスのコンストラクタでassetsへの接続を確立しているため、本処理は1回のみ実行されることになっています。またコンストラクタをprivateで定義しているため当該クラスのインスタンスを新たに生成することができなくなり唯一性が保証されます。
[output] 接続:/assets/temp シングルトンオブジェクトで資材取得:image_1 User1でimage_1を描画!! シングルトンオブジェクトで資材取得:audio_1 User2でaudio_1を再生!!
- 投稿日:2019-03-18T21:36:49+09:00
【Gang of Four】デザインパターン学習 - Prototype
試作品
目次
部品をプールし、クローンすることでオブジェクトを生成するパターンです。目的
生成すべきオブジェクトの種類を原型となるインスタンスを使って明確にし、それをコピーすることで新たなオブジェクトの生成を行う。
構成要素
・Prototype 複製を行う抽象クラス
・ConcretePrototype 複製を行う具象クラス
・Client 複製を依頼するクラス実装
車を量産するために必要な試作品工場を実装します。
まずは試作品工場が作る部品から実装します。
エンジンクラス
Engine.ktpackage prototype class Engine(displacement: Int): Cloneable { var displacement = displacement fun show() { print("【エンジン】"+ displacement +"cc ") } public override fun clone(): Any { return super.clone() } }タイヤクラス
Tire.ktpackage prototype class Tire(num: Int): Cloneable { var num = num fun show() { print("【タイヤ】" + num + "個 ") } public override fun clone(): Any { return super.clone() } }以上が部品です。続いて製品の車を実装します。
車クラス
Car.ktpackage prototype class Car(engine: Engine, tire: Tire) { val engine = engine val tire = tire fun show() { print("【製品】車 ") engine.show() tire.show() print("\n") } }今まで実装したEngineとTireを次々量産できる工場を実装します。
Prototype 複製を行う抽象クラス
試作品インターフェースProtoType.ktpackage prototype /** * 試作品 * Prototype */ interface ProtoType { fun createEngine(): Engine fun createTire(): Tire }ConcretePrototype 複製を行う具象クラス
試作品工場クラスProtoTypeFactory.ktpackage prototype class ProtoTypeFactory: ProtoType { var engine = Engine(0) var tire = Tire(4) override fun createEngine(): Engine { return engine.clone() as Engine } override fun createTire(): Tire { return tire.clone() as Tire } }最後に試作品工場から部品を受け取り製品を量産する工場を実装します。
Client 複製を依頼するクラス
車製造工場クラスFactory.ktpackage prototype class Factory { var carList:MutableList<Car> = mutableListOf() var prototypeFactory = ProtoTypeFactory() init { // 排気量を1000ccに設定 prototypeFactory.engine.displacement = 1000 // 排気量1000ccの車を3台量産する massProduction(3) // 排気量を2000ccに設定 prototypeFactory.engine.displacement = 2000 // 排気量2000ccの車を2台量産する massProduction(2) // 排気量を12000ccに設定 prototypeFactory.engine.displacement = 12000 // タイヤを8本に設定 prototypeFactory.tire.num = 8 // 排気量12000cc タイヤ8本の車を5台量産する バス? massProduction(5) for (car in carList) { car.show() } } private fun massProduction(num: Int) { for (i in 0..num) { carList.add(Car(prototypeFactory.createEngine(), prototypeFactory.createTire())) } } }以上で試作品を次々量産できる工場が実装できました。Abstract Factoryパターンとは異なり、いろいろな車種を量産したくなっても
ProtoTypeFactory
クラスが持つ部品のメンバを変更すれば新たな工場を実装せずに済みます。[output] 【製品】車 【エンジン】1000cc 【タイヤ】4個 【製品】車 【エンジン】1000cc 【タイヤ】4個 【製品】車 【エンジン】1000cc 【タイヤ】4個 【製品】車 【エンジン】1000cc 【タイヤ】4個 【製品】車 【エンジン】2000cc 【タイヤ】4個 【製品】車 【エンジン】2000cc 【タイヤ】4個 【製品】車 【エンジン】2000cc 【タイヤ】4個 【製品】車 【エンジン】12000cc 【タイヤ】8個 【製品】車 【エンジン】12000cc 【タイヤ】8個 【製品】車 【エンジン】12000cc 【タイヤ】8個 【製品】車 【エンジン】12000cc 【タイヤ】8個 【製品】車 【エンジン】12000cc 【タイヤ】8個 【製品】車 【エンジン】12000cc 【タイヤ】8個
- 投稿日:2019-03-18T21:32:59+09:00
【Gang of Four】デザインパターン学習 - Factory Method
工場の方法
目次
Abstract Factoryパターンの具象工場クラスの部分のみが本パターンに該当すると理解しています。目的
オブジェクトを生成するときのインタフェースだけを規定して、実際にどのクラスをインスタンス化するかはサブクラスが決めるようにする。Factory Methodパターンは、インスタンス化をサブクラスに任せる。
構成要素
・Product 生成される部品の抽象クラス
・ConcreteProduct 生成される部品
・Creator 部品製造工場の抽象クラス
・ConcreteCreator 部品製造工場実装
プロスポーツ選手に主催者が宣誓してもらうようなプログラムを実装します。宣誓を行う代表選手はそれぞれのスポーツ連盟により派遣されます。
Product 生成される部品の抽象クラス
プロスポーツ選手抽象クラスProfessionalAthlete.ktpackage factorymethod abstract class ProfessionalAthlete(type: Type) { enum class Type(val value: String) { Soccer("サッカー"), BaseBall("野球") } val myType = type abstract fun athletesOath() }ConcreteProduct 生成される部品
プロサッカー選手具象クラスProfessionalSoccerAthlete.ktpackage factorymethod class ProfessionalSoccerAthlete(name: String): ProfessionalAthlete(Type.Soccer) { private val name = name override fun athletesOath() { println("宣誓!私は" + name + "です。プロ【" + myType.value + "】選手としてサッカーボールを使い正々堂々プレーします!") } }プロ野球選手具象クラス
ProfessionalBaseBallAthlete.ktpackage factorymethod class ProfessionalBaseBallAthlete(name: String): ProfessionalAthlete(Type.BaseBall) { private val name = name override fun athletesOath() { println("宣誓!私は" + name + "です。プロ【" + myType.value + "】選手としてバットと野球ボールを使い正々堂々プレーします!") } }以上で選手(product)が実装できました。次は選手(product)を派遣(create)するスポーツ連盟(factory)を実装していきます。
Creator 部品製造工場の抽象クラス
スポーツ連盟抽象クラスSportsFederation.ktpackage factorymethod abstract class SportsFederation { fun getAthlete(): ProfessionalAthlete { return dispatchRepresentativeAthlete() } protected abstract fun dispatchRepresentativeAthlete(): ProfessionalAthlete }ConcreteCreator 部品製造工場
サッカー連盟具象クラスSoccerFederation.ktpackage factorymethod class SoccerFederation: SportsFederation() { override fun dispatchRepresentativeAthlete(): ProfessionalAthlete { return ProfessionalSoccerAthlete("サカ田 代表太郎") } }野球連盟具象クラス
BaseBallFederation.ktpackage factorymethod class BaseBallFederation: SportsFederation() { override fun dispatchRepresentativeAthlete(): ProfessionalAthlete { return ProfessionalBaseBallAthlete("野球本 キャプテン男") } }最後に各スポーツ連盟(factory)に代表選手(product)を要求し、宣誓させる主催者を実装します。
使用者
主催者クラスOrganizer.ktpackage factorymethod class Organizer { init { var athleteList: MutableList<ProfessionalAthlete> = mutableListOf() // それぞれのスポーツ連盟から代表選手を派遣してもらう // サッカー連盟から代表選手取得 athleteList.add(SoccerFederation().getAthlete()) // 野球連盟から代表選手取得 athleteList.add(BaseBallFederation().getAthlete()) // 宣誓してもらう for (athlete in athleteList) { athlete.athletesOath() } } }以上で生成しなければならないオブジェクトのクラスを事前に知ることができない場合に用いることができるパターンができました。ような気がします。
[output] 宣誓!私はサカ田 代表太郎です。プロ【サッカー】選手としてサッカーボールを使い正々堂々プレーします! 宣誓!私は野球本 キャプテン男です。プロ【野球】選手としてバットと野球ボールを使い正々堂々プレーします!
- 投稿日:2019-03-18T21:29:20+09:00
【Gang of Four】デザインパターン学習 - Builder
建築業者
目次
Abstract Factoryがその名の通り工場であり各部品を製造する機能を提供するならば、本パターンは部品を組み合わせた製品(複合オブジェクト)を量産するようなイメージながかな?目的
複合オブジェクトについて、その作成過程を表現形式に依存しないものにすることにより、同じ作成過程で異なる表現形式のオブジェクトを生成できるようにする。
構成要素
・Builder Productクラスを生成するための抽象クラス
・ConcreteBuilder 具象Builderクラス。Productクラスを生成する
・Director Builderクラスのインタフェースを使ってオブジェクトを生成する
・Product 多くの構成要素からなる複合オブジェクト実装
下記のようなフローでクリエイターが街づくりを行います。
専門の建築業者へ建てたい建築物を依頼 -> 建物のフロアの数、部屋の数を決める -> 希望する数だけ建ててもらう
Builder Productクラスを生成するための抽象クラス
建築業者インターフェースBuilder.ktpackage builder interface Builder { enum class ProductType(val value: String) { ArtMuseum("美術館"), Museum("博物館"), MovieTheater("映画館") } fun getProduct(): Product fun addFloor(floorNum: Int) fun addRoom(targetFloor: Int, roomNo: Int) }ConcreteBuilder 具象Builderクラス。Productクラスを生成する
博物館専門建築業者具象クラスMuseumBuilder.ktpackage builder class MuseumBuilder(productName: String): Builder { private var product = Product(Builder.ProductType.Museum, productName) override fun getProduct(): Product { product.countUpProductNumber() return product.clone() as Product } override fun addFloor(floorNum: Int) { if (product.floorList.asSequence().filter { floor -> floor.floorNum == floorNum }.count() == 0) { product.floorList.add(Floor(floorNum)) } } override fun addRoom(targetFloor: Int, roomNo: Int) { val floor= product.floorList.filter { floor -> floor.floorNum== targetFloor } if (floor.count() > 0) { floor[0].addRoom(roomNo) } } }美術館専門建築業者具象クラス
ArtMuseumBuilder.ktpackage builder class ArtMuseumBuilder(productName: String) : Builder { private var product = Product(Builder.ProductType.ArtMuseum, productName) override fun getProduct(): Product { product.countUpProductNumber() return product.clone() as Product } override fun addFloor(floorNum: Int) { if (product.floorList.asSequence().filter { floor -> floor.floorNum == floorNum }.count() == 0) { product.floorList.add(Floor(floorNum)) } } override fun addRoom(targetFloor: Int, roomNo: Int) { val floor= product.floorList.filter { floor -> floor.floorNum== targetFloor } if (floor.count() > 0) { floor[0].addRoom(roomNo) } } }映画館専門建築業者具象クラス
MovieTheaterBuilder.ktpackage builder class MovieTheaterBuilder(productName: String): Builder { private var product = Product(Builder.ProductType.MovieTheater, productName) override fun getProduct(): Product { product.countUpProductNumber() return product.clone() as Product } override fun addFloor(floorNum: Int) { if (product.floorList.asSequence().filter { floor -> floor.floorNum == floorNum }.count() == 0) { product.floorList.add(Floor(floorNum)) } } override fun addRoom(targetFloor: Int, roomNo: Int) { val floor= product.floorList.filter { floor -> floor.floorNum== targetFloor } if (floor.count() > 0) { floor[0].addRoom(roomNo) } } }建物~部屋までは読み飛ばしてもらって結構です。
Product 多くの構成要素からなる複合オブジェクト
建物クラスProduct.ktpackage builder class Product(productType: Builder.ProductType, productName: String): Cloneable { var productType = productType val floorList: MutableList<Floor> = mutableListOf() var productName = productName var productNumber = 0 fun countUpProductNumber() { productNumber++ } fun show(): String { var ret = "【建物】${productType.value}:$productName$productNumber 棟目" for (floor in floorList) { ret += floor.show() } return ret } public override fun clone(): Any { return super.clone() } }階層クラス 1階 2階...
Floor.ktpackage builder class Floor(floorNum: Int) { var floorNum = floorNum private val roomList:MutableList<Room> = mutableListOf() fun addRoom(roomNo: Int) { if (roomList.asSequence().filter { room -> room.roomNo == roomNo }.count() == 0) { roomList.add(Room(roomNo)) } } fun show(): String { var ret: String = "【階層】$floorNum 階 " for (room in roomList) { ret += room.show() } return ret } }部屋クラス
Room.ktpackage builder class Room(roomNo: Int) { val roomNo = roomNo fun show(): String { return "【部屋】 $roomNo 号室" } }Director Builderクラスのインタフェースを使ってオブジェクトを生成する
街を作る人クラスCreator.ktpackage builder class Creator { init { var productList:MutableList<Product> = mutableListOf() // 西洋美術館の建築を美術館専門建築業者に依頼する var artMuseumBuilder1 = ArtMuseumBuilder("西洋美術館").apply { addFloor(1) addFloor(2) addFloor(3) addRoom(1, 101) addRoom(1, 102) addRoom(2, 201) addRoom(3, 301) } // 東洋美術館の建築を美術館専門建築業者に依頼する var artMuseumBuilder2 = ArtMuseumBuilder("東洋美術館").apply { addFloor(1) addFloor(2) addRoom(1, 101) addRoom(2, 201) } // 国立博物館の建築を博物館専門建築業者に依頼する var museumBuilder = MuseumBuilder("国立博物館").apply { addFloor(1) addRoom(1, 101) } // ホーゲーシネマズの建築を映画館専門建築業者に依頼する var movieTheaterBuilder = MovieTheaterBuilder("HOGEシネマズ").apply { addFloor(1) addRoom(1, 101) } // 街に建築するリスト作成 productList.add(artMuseumBuilder1.getProduct()) productList.add(artMuseumBuilder1.getProduct()) productList.add(artMuseumBuilder1.getProduct()) productList.add(artMuseumBuilder2.getProduct()) productList.add(artMuseumBuilder2.getProduct()) productList.add(museumBuilder.getProduct()) productList.add(movieTheaterBuilder.getProduct()) for (product in productList) { println(product.show()) } } }クリエイターはどんな建物を街に建てたいかを考え、それぞれの建築業者へ依頼し、必要な分だけ建ててもらいます。
コンビニとかにしたほうがわかりやすかったろうか...あんまり作りこんでもしょうがないので簡潔な構成にしていますが、住所プロパティなんかをProductクラスに保持させ、建ててもらう
getProduct()
たびに住所プロパティを設定できるようにすればよりイメージしやすかったか?以上で製品(複合オブジェクト)を量産するという目的は達成できました。
街作りの途中で映画館をもう一軒追加したい!場合もmovieTheaterBuilder.getProduct()
を呼べば同じ建物を複製(量産)することができます。各Builder#getProduct()メソッドのreturn product.clone() as Product
がいい仕事してますね。[output] 【建物】美術館:西洋美術館1 棟目【階層】1 階 【部屋】 101 号室【部屋】 102 号室【階層】2 階 【部屋】 201 号室【階層】3 階 【部屋】 301 号室 【建物】美術館:西洋美術館2 棟目【階層】1 階 【部屋】 101 号室【部屋】 102 号室【階層】2 階 【部屋】 201 号室【階層】3 階 【部屋】 301 号室 【建物】美術館:西洋美術館3 棟目【階層】1 階 【部屋】 101 号室【部屋】 102 号室【階層】2 階 【部屋】 201 号室【階層】3 階 【部屋】 301 号室 【建物】美術館:東洋美術館1 棟目【階層】1 階 【部屋】 101 号室【階層】2 階 【部屋】 201 号室 【建物】美術館:東洋美術館2 棟目【階層】1 階 【部屋】 101 号室【階層】2 階 【部屋】 201 号室 【建物】博物館:国立博物館1 棟目【階層】1 階 【部屋】 101 号室 【建物】映画館:HOGEシネマズ1 棟目【階層】1 階 【部屋】 101 号室もし、
product.clone()
していなければ下記のような結果になり、何の役にも立たない建築業者ができあがります。[output] 【建物】美術館:西洋美術館3 棟目 ... 【建物】美術館:西洋美術館3 棟目 ... 【建物】美術館:西洋美術館3 棟目 ... 【建物】美術館:東洋美術館2 棟目 ... 【建物】美術館:東洋美術館2 棟目 ... 【建物】博物館:国立博物館1 棟目 ... 【建物】映画館:HOGEシネマズ1 ...
- 投稿日:2019-03-18T21:22:55+09:00
【Gang of Four】デザインパターン学習 - Abstract Factory
抽象工場
オブジェクト指向の基本的な思想としてオブジェクト間の結合度が低いほうが良い、といったものがあります。
javaではinterfaceを用いることで、具象クラスを使う側が意識する必要がないようにできます。インターフェース
Screen.javainterface Screen { public void view(); }具象クラス
TopScreen.javapublic class TopScreen implements Screen { public void view() { System.out.println("トップ画面"); } }LoginScreen.javapublic class LoginScreen implements Screen { public void view() { System.out.println("ログイン画面"); } }使う側
Client.javapublic class Client { public void displayTopScreen() { Screen top = new TopScreen() top.view(); } public void displayLoginScreen() { Screen login = new LoginScreen() login.view(); } }一見なんの問題もないように見えますが、各クラスをインスタンス化する部分である
Screen top = new TopScreen()
とScreen login = new LoginScreen()
において、どうしても具象クラスを意識する必要がでてきます。
これは結合度が低いほうが良いという思想に反することになるうえ、使用しているコンストラクタを修正すればインスタンス化している箇所をすべて修正しなければならなくなります。これを
factoryクラス
というインスタンスを生成してくれるクラスを作成することによって解決しますが、そのfactoryクラス
も抽象クラスと具象クラスに分けることで、より柔軟なオブジェクト生成工場を作ろうというのが本パターンです。目的
互いに関連したり依存し合うオブジェクト群を、その具象クラスを明確にせずに生成するためのインタフェースを提供する。
構成要素
・AbstractFactory AbstractProductクラスを生成するインターフェースを定義する
・ConcreteFactory ConcreteProductクラスを生成するインターフェースを定義する
・AbstractProduct 生成される部品の共通部分を抽出した抽象クラス
・ConcreteProduct 生成される部品
・Client 利用者実装
ではサンプルコードです。
車に必要な部品を製造する工場を実装し、下記のようなフローでディーラーへ車両を陳列します。ディーラーがメーカーへ車両を要求 -> メーカーが工場へ車用部品を要求 -> 工場がメーカーへ各種部品を返却 -> メーカーが車を組み立ててディーラーへ返却 -> ディーラーに車両が陳列される。
AbstractFactory AbstractProductクラスを生成するインターフェースを定義する
車用部品製造工場クラスCarFactory.ktpackage abstractfactory open class CarFactory { open fun makeEngine(displacement: Int): Engine { return Engine(displacement) } open fun makeTire(position: Int): Tire { return Tire(position) } open fun makeSteering(weight: Int): Steering { return Steering(weight) } }AbstractProduct 生成される部品の共通部分を抽出した抽象クラス
エンジンクラスEngine.ktpackage abstractfactory open class Engine(displacement: Int) { open val type = "普通のエンジン" val displacement = displacement }タイヤクラス
Tire.ktpackage abstractfactory open class Tire(position: Int) { open val type = "普通のタイヤ" val position = position }ハンドルクラス
Steering.ktpackage abstractfactory class Steering(weight: Int) { val type = "普通のハンドル" val weight = weight }車クラス
Car.ktpackage abstractfactory open class Car(type: String, engine: Engine, tire: Tire, steering: Steering) { val type = type val engine = engine val tire = tire val steering = steering fun getConstitution(): String { return "【車種】:" + type + "," + "【エンジン】:" + engine.type + "," + "【タイヤ】:" + tire.type + "," + "【ハンドル】:" + steering.type } }Client 利用者
自動車メーカークラスMaker.ktpackage abstractfactory class Maker { /** * 普通の車製造 */ fun getCar(): Car { return make("ファミリーカー", CarFactory()) } /** * 製造 */ private fun make(type: String, factory: CarFactory): Car { val engine = factory.makeEngine(1000) val tire = factory.makeTire(1) val steering = factory.makeSteering(100) return Car(type, engine, tire, steering) } }自動車ディーラークラス
AutomobileDealer.ktpackage abstractfactory class AutomobileDealer { val cars = mutableListOf<Car>() init { openingUp() } /** * ディーラーにある車一覧 */ fun showCars() { cars.forEach { println(it.getConstitution()) } } /** * 店舗開店 */ private fun openingUp() { val maker = Maker() cars.add(maker.getCar()) } }AutomobileDealer#showCars()を呼び出せば、陳列されている車両(のスペック)が出力されます。
[output] 【車種】:ファミリーカー,【エンジン】:普通のエンジン,【タイヤ】:普通のタイヤ,【ハンドル】:普通のハンドル以上で完成しましたが、突然社長が今までの製造ラインはそのままに、同様のラインでスーパーカー用部品も製造できる工場を作れといいだしました。
ConcreteFactory ConcreteProductクラスを生成するインターフェースを定義する
スーパーカー用部品製造工場クラスSuperCarFactory.ktpackage abstractfactory class SuperCarFactory: CarFactory() { override fun makeEngine(displacement: Int): Engine { return HiPerformanceEngine(displacement) } override fun makeTire(position: Int): Tire { return HiGripTire(position) } }ConcreteProduct 生成される部品
高出力エンジンクラスHiPerformanceEngine.ktpackage abstractfactory class HiPerformanceEngine(displacement: Int): Engine(displacement) { override val type = "高出力エンジン" }ハイグリップタイヤクラス
HiGripTire.ktpackage abstractfactory class HiGripTire(position: Int): Tire(position) { override val type = "ハイグリップタイヤ" }メーカーにスーパーカー製造メソッドを追加します。
Client 利用者
メーカークラスMaker.ktpackage abstractfactory class Maker { /** * 車製造 */ fun getCar(): Car { return make(CarFactory()) } /** * スーパーカー製造 */ fun getSuperCar(): Car { return make("スーパーカー", SuperCarFactory()) } /** * 製造 */ private fun make(type: String, factory: CarFactory): Car { val engine = factory.makeEngine(1000) val tire = factory.makeTire(1) val steering = factory.makeSteering(100) return Car(type, engine, tire, steering) } }ディーラーは新たな車両(スーパーカー)をメーカーに要求します。
AutomobileDealer.ktpackage abstractfactory class AutomobileDealer { val cars = mutableListOf<Car>() init { openingUp() } /** * ディーラーにある車一覧 */ fun showCars() { cars.forEach { println(it.getConstitution()) } } /** * 店舗開店 */ private fun openingUp() { val maker = Maker() cars.add(maker.getCar()) cars.add(maker.getSuperCar()) cars.add(maker.getSuperCar()) } }AutomobileDealer#showCars()を呼び出せば、スーパーカーが新たに陳列されていることがわかります。
[output] 【車種】:ファミリーカー,【エンジン】:普通のエンジン,【タイヤ】:普通のタイヤ,【ハンドル】:普通のハンドル 【車種】:スーパーカー,【エンジン】:高出力エンジン,【タイヤ】:ハイグリップタイヤ,【ハンドル】:普通のハンドル 【車種】:スーパーカー,【エンジン】:高出力エンジン,【タイヤ】:ハイグリップタイヤ,【ハンドル】:普通のハンドル以上で社長の要求に応じることができました。
- 投稿日:2019-03-18T17:52:47+09:00
【Kotlin】FragmentでViewPagerを使う時の注意点(再読み込みできない)
BottomNavigationViewのタブで切り替え表示をするFragmentのうちの1つで
ViewPagerを使って実装をしていてBottomNavigationViewのタブを切り替えると実行されるはずの読み込み処理が実行されず、かつ直前に読み込んでいたデータも表示されなくなった
という現象が起きて嵌りました。
【画面構成(階層)】
MainActivity(BottomNavigationViewを使用)
|- ParentFragment(ViewPagerを使用)
|- ViewPager
|- ChildFragments(ViewPagerの各ページに表示するFragment達)先に解決策の載せておきます。↓↓
解決策
ViewPagerにセットするFragmentPagerAdapterのインスタンスを生成する時に
childFragmentManager
を渡す。これで解決しました。
FragmentPagerAdapter(解決後)val adapter = FragmentPagerAdapter(childFragmentManager) //← ここ viewPager.adapter = adapter class AroundPagerAdapter(fragmentManager: FragmentManager?) : FragmentPagerAdapter(fragmentManager) { // 省略 }FragmentPagerAdapter(解決前)val adapter = FragmentPagerAdapter(fragmentManager) viewPager.adapter = adapter class AroundPagerAdapter(fragmentManager: FragmentManager?) : FragmentPagerAdapter(fragmentManager) { // 省略 }実際に起きていたこと
BottomNavigationViewのタブから別タブに切り替えて再びそのタブに戻ってきたとき、
FragmentPagerAdapter#getItem()が呼ばれず、 ViewPager 内が再読込みされない現象が起きていました。
なぜ起きたのか、、、
①FragmentPagerAdapterはメモリ上にViewPagerの子Fragment(ChildFragments)を全て保持する
→ タブを切り替えた時は以前のものが再利用される
FragmentPagerAdapter②BottomNavigationViewのタブ切り替えだとParentActivityは生きたまま
→ FragmentManagerはViewPager内の子Fragment達(ChildFragments)を保持したままになり、 FragmentPagerAdapter#getItem() が呼ばれない①②に対して、childFragmentManagerを使うと
保持される ViewPager 内のChildFragmentsはParentFragmentのFragmentManagerで管理される。
→タブを切り替えたときにはParentFragmentのインスタンスを再生成するのでそのタイミングでFragmentManagerがリセットされる
→新しくChildFragmentsのインスタンスを生成するので「FragmentPagerAdapter#getItem()」 が呼ばれて再読込みが始まるようになる。
(余談)
ViewPager のスワイプが起きたときは ParentFragment が生きている限り、 FragmentManager 内で ChildFragments が管理されるので、通常通り効率的にスワイプできるようになる。まとめ
かなり嵌りましたが、解決してみるとあっけなかったですね 笑
以上です。
- 投稿日:2019-03-18T15:54:34+09:00
Kotlin文法メモ~コンストラクタ~
初めに
kotlin でのコンストラクタに関するメモ
コンストラクタとは
クラスの中の変数であるメンバを初期化するためのもの。
インスタンスが作成されたとき最初に実行される。コンストラクタの実装
コンストラクタは以下のようにして実装できます。
以下のコードのクラスでは、車のメーカーの種類としてtype
、製造年としてmodel
を用意してあります//CarContsructorの隣に()がない class CarContsructor{ var type:String? = null var model:Int?=null constructor(type:String,model:Int){ this.type = type this.model = model } }左側の
this.type
とthis.model
はクラスの中のメンバを指していて、右のtype
とmodel
はコンストラクタの引数の値になっています。また、以下のような書き方も可能です。この場合、
val car = CarConstructor()
のように使えます。//CarContsructorの隣に()がある class CarContsructor(){ var type:String? = null var model:Int?=null constructor(type:String,model:Int):this(){ this.type = type this.model = model } }先にコンストラクタのとこが呼ばれた後に、メンバの定義がされて初期化される流れになってますね。
constructor
を書くのが面倒っていう人は以下のようにしてプライマリーコンストラクタを利用して短く書けます。。すっきりしてこっちのほうがいいですね。
上の2つのコードと比較してかなり行数が省略されています。class CarOptions(type:String,model:Int){ var type:String? = type var model:Int?= model }
- 投稿日:2019-03-18T14:04:53+09:00
Navigation component の利用
iOSのStoryboardのように、画面遷移をレイアウトエディタから設定できる Navigation componentを使ってみたメモ
参考ドキュメント
Get started with the Navigation component | Android Developers
Pass data between destinations | Android Developers
1. ナビゲーショングラフを追加する
- 「Project」ウィンドウでresディレクトリを右クリックし、「New」>「Android Resource Files」を選択。[ New Resource File ]ダイアログが表示される。(図1)
- 名前を入力。ここでは「nav_graph」とする。
- [Resource type]ドロップダウンリストから[Navigation]を選択し、[ OK ]をクリック。
2. アクティビティにNavHostを追加する
activity_main.xml
にNavHostFragment
を追加する。
3.ナビゲーショングラフに遷移先を追加する
4.デスティネーションを接続する
アトリビュートにIDを設定する
action_blankFragment_to_fragmentOther
5.ナビゲーションを呼び出す
各フラグメントに
jumpButton
,jumpButton2
を設定しました。
それぞれ以下のように、Navigationインスタンスのnavigateメソッドを実行しますFragmentOther.kt... override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val view = inflater.inflate(R.layout.fragment_fragment_other, container, false) view.jumpButton2?.setOnClickListener{ Navigation.findNavController(it).navigate( R.id.action_back_to_blank ) } return view } ...以上で2つのフラグメント間を遷移します
6.遷移間でデータをやり取りする
navigateを通してbundleをやりとりできます
BlankFragment.kt... // 遷移先フラグメントにBundleを送信 val bundle = bundleOf("myArgs" to "send from BlankFragment") Navigation.findNavController(it).navigate( R.id.action_blankFragment_to_fragmentOther, bundle) ...FragmentOther.kt... override fun onViewCreated(view: View, savedInstanceState: Bundle?) { // 遷移元から送信されたBundleデータを取得 fragmentOtherTextView.text = arguments?.getString("myArgs") } ...また、safeArgsを利用すると型安全にやりとりできらしい。
用語
ナビゲーショングラフ
画面間の関係を設定するxmlファイル。
ナビゲーションホスト
空のコンテナ。ユーザーがアプリな尾を移動するときに行き先が入れ替わります。
デスティネーション
遷移先のフラグメント
プロジェクトファイル
以下に今回使用したAndroid Studioプロジェクトファイルをアップロードしました。
ymmtyuhei/NavigationEditorTrial
- 投稿日:2019-03-18T01:14:39+09:00
Nginx,Redis,MySQLを使ってほんの少し実践的なRails ActionCableと、iOS/Androidのサンプルアプリを作って全体像を学ぶ〜Android編〜
前置き
「Rails ActionCableで双方向通信してみたい」「モバイルアプリでリアルタイム通信アプリ作りたい」と思いサンプルアプリを作ってみました。個々の詳細については既に解説してくださっている記事はありますので、大まかに環境構築やソースコードを記事にします。以下の3部構成になっています。
お遊びサンプルの紹介
以下のアニメーションGIF(ちょっと荒いですね
)をご覧ください。各ユーザーのアクティブ状況を表示して、メッセージをやりとります。もっと砕けた表現をするならば、筆者の愛犬たちが寝起きして、鳴いたり、遠吠えしたり、唸ったりします。
このアプリでは大きく2つのActionCableの使い方があります。
- 同じルーム内の全ユーザーにブロードキャスト
- ルームに入る
現在ルーム内にいるユーザー(以下、アクティブユーザー)にルームに入ったことを通知し、アクティブユーザーを取得します。 そして、各ユーザーのアクティブ状況を表示します。- 「ワンワン」ボタンと「ワオーーン」ボタン
文字入力で任意の文字が送れないだけで、チャットでいうところの「メッセージ」とほぼ同義です。ボタンに対応したメッセージを送信します。
※アニメーションGIFでは「わんわん」「ワオォーン」になっています。途中から文字を修正しました![]()
- ルームから出る
「←」をタップしたり、アプリを閉じたりするとルームから出たことにします。ルームに入るときと同様にアクティブユーザーを取得して、各ユーザーのアクティブ状況を表示します。- 自分にブロードキャスト
- 「独り言」
「独り言」ということで自分のみメッセージを受信します。構成
名前 バージョン macOS Mojave 10.14.3 AndroidStudio 3.3.2 Kotlin 1.3.21 AVD(API) Pixel(28), Pixel2(27 / 28)
hosts(AVD)
よろしければ筆者の記事をご覧ください。筆者はこのサンプルアプリを作る過程でAVDのhostsについて知り記事にしました。hosts10.0.2.2 devnokiyo.example.comソースコードはGitHubに公開しています。よろしければご覧ください。
actioncable-client-javaを導入する
ライブラリを利用して開発しますのでgradleに追記してAndroidStudioで同期します。
ソースコードの説明
サンプルアプリはActionCableと本質的に関係ない部分も多いので、GitHubに公開しているソースコードの原形を保ちながら主要な部分を抽出しました。ソースコードの中にコメントで説明します。
app/src/main/java/com/devnokiyo/actioncableapp/activities/BarkActivity.ktclass BarkActivity : AppCompatActivity() { private val cableUrl = "ws://devnokiyo.example.com/cable" // ActionCableのコネクションのエンドポイント private val channelIdentifier = "RoomChannel" // チャンネル名 private lateinit var client: Consumer // ActionCableのコネクションに関連するインスタンス private lateinit var channel: Subscription // ActionCableのチャンネルを関連するインスタンス private var handler = Handler() // メインスレッドで実行するハンドラー override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_bark) // 「ワンワン」ボタンなどのリスナーを設定する。 initListener() } override fun onResume() { super.onResume() // ActionCable関連のインスタンスを初期化・接続する。 initClientAndChannel() } override fun onPause() { super.onPause() // 画面を閉じるときはコネクションを切断する。 // サンプルでは便宜上onPause()で実装しておく。 client.disconnect() } private fun initListener() { bawBawButton.setOnClickListener { // 「ワンワン」ボタン押下時はRoomChannelのbarkアクションを呼出す。 // 送信情報) "content":"bawbaw" bark(bark = "bawbaw") } WaooonButton.setOnClickListener { // 「ワオーーン」ボタン押下時はRoomChannelのbarkアクションを呼出す。 // 送信情報) "content":"waooon" bark(bark = "waooon") } mumblingButton.setOnClickListener { // 「独り言」ボタン押下時はRoomChannelのmumblingアクションを呼出す。 // 送信情報) 無し。アクションを呼出すのみ。 channel.perform("mumbling") } } private fun initClientAndChannel() { // コネクションのエンドポイントを指定する。 // 送信情報) 呼出し元アクティビティから取得したaccount // 【補足】ユーザーの割出しと認証が必要ならOpenID Connectのアクセストークンなどになると思います。 client = ActionCable.createConsumer(URI("$cableUrl/?account=$account")) // チャンネルを作成する。サブスクライブは手動で行う。 // 送信情報) "room":ルーム名(ID) channel = client.subscriptions.create(Channel(channelIdentifier).apply { addParam("room", room) }) channel.onConnected { // サブスクライブしたら、ルームに入ったことになる。 // 同じルームのアクティブユーザーに通知するのでRoomChannelのgreetingアクションを呼出す。 channel.perform("greeting") } channel.onReceived { data -> // UIスレッド(メインスレッド)で実行する。 handler.post { // 自他問わずアクティブユーザーより送信された情報をこのコールバックで受信する。 RoomChannelResponse.create(data)?.let { response -> // 誰に関する情報か判定する。自身も含まれる。 // 受信情報) "account":ユーザーのアカウント // findUserStatusViewメソッドは以下のいずれかのクラス変数を返却する。 // chiyoUsv // eruUsv // otomeUsv val userStatusView = findUserStatusView(response.account) when (response.type) { SocketType.RoomIn -> { // ルームに入ったらユーザーのステータスを更新する。 // 受信情報) "type":"in" // 受信情報) "roommate":"[アクティブユーザーのアカウント...]" // updateUserStatusメソッドはアクティブユーザーを以下のようにする。 // 表示内容:(^○^) // オンラインの色(緑) updateUserStatus(accounts = response.roommate, type = response.type) // ルームに入ったユーザーは挨拶する。 // 受信情報) "type":"in" 表示内容: (^○^) (言語:日本語) // getResourceStringメソッドはstrings.xml内の指定したname属性の内容を取得する。 userStatusView.bark.text = getResourceString(response.type.rawValue) } SocketType.RoomOut -> { // ルームから出たユーザーは挨拶する。 // 受信情報) "type":"out" 表示内容: ( ˘ω˘ ) (言語:日本語) userStatusView.bark.text = getResourceString(response.type.rawValue) // オフラインの色(赤)に変更する。 userStatusView.online.setBackgroundColor(Color.RED) } SocketType.Mumbling -> { // 受信情報) // "type":"mumbling" // "content":"(゚Д゚;)" 表示内容: (゚Д゚;) (言語:不問) バックエンドの固定値なので言語設定に依存しない // 【補足】「独り言」は自身が送信した情報をActionCableを経由して自身のみが受信します。 response.content?.let { content -> userStatusView.bark.text = content userStatusView.rockBark() } } SocketType.Bark -> { // 受信情報) // "type":"bark" // "content":"bawbaw" / "wooon" 表示内容: ワンワン / ワオーーン (言語:日本語) response.content?.let { content -> userStatusView.bark.text = getResourceString(content) userStatusView.rockBark() } } } } } } // 【補足】他にも以下のコールバックが用意されています。 // channel.onFailed { e: ActionCableException -> } // channel.onDisconnected {} // channel.onRejected {} // 【補足】以下のプロパティで再接続のポリシーを設定出来るようです。(厳密に確認していません。) // val options = Consumer.Options().apply { // reconnection = true // reconnectionMaxAttempts = 30 // reconnectionDelay = 3 // reconnectionDelayMax = 30 // } // client = ActionCable.createConsumer(URI("$cableUrl/?account=$account"), options) // コネクションに接続してチャンネルをサブスクライブする。 client.connect() } private fun bark(bark: String) { // 「ワオーーン」ボタン押下時はRoomChannelのbarkアクションを呼出す。 // 送信情報) "content":bark channel.perform("bark", JsonObject().apply { addProperty("content", bark) }) } // res/values/strings.xml内の指定したname属性の内容を取得する。 private fun getResourceString(name: String): String = getString(resources.getIdentifier(name, "string", packageName)) }終わりに
チャットアプリのサンプルが定番なので、少し違うアプローチでサンプルを作っていたはずなのですが、結局仕組みは似たり寄ったりになってきました。iOS版ではハマったことがありましたが、iOS版がある程度仕上がってからAndroid版を実装したので仕様でハマるところはありませんでした。Xcode/Swiftを見ながらAndroidStudio/Kotlinへ書写した感じですね。iOS版同様に細かい作込みはしていませんが、バックエンドとアプリの双方を実装して、やりたいことの表現と仕組みを概ね理解することができました。