20190127のKotlinに関する記事は11件です。

JavaDo #14 Kotlinハンズオン

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Java開発者に送るKotlinのClass part.2

可視性

JavaとKotlinの可視性はやや異なります。

  • Kotlinのデフォルト可視性はpublic
  • Kotlinにpackage privateは無い
  • Kotlin独自の可視性internalがある (同一モジュール内で参照可)
  • Kotlinはデフォルトで継承不可 (Javaのfinal扱い)

継承

KotlinでAnimalクラスを継承したDogクラスを定義してみます。

open class Animal { // 継承可なクラスにするためopenをつける
    open fun greet() { // オーバーライド可能な関数にするためopenをつける
    }
}

class Dog: Animal() { // コロンに続けて親クラス名を
    override fun greet() { // override修飾子をつける
        println("Bow wow")
    }
}

クラスを継承するときは、
class サブクラス名: スーパークラスのコンストラクタ と記述します。

コンストラクタに引数が必要な場合を見てみましょう。

open class Animal(val name: String) {
    open fun greet() {
    }
}

// Dogのコンストラクタ引数 `name` をAnimalクラスのコンストラクタに渡す
class Dog(name: String): Animal(name) {
    override fun greet() {
        println("Bow wow")
    }
}

コンパニオンオブジェクト

Kotlinにはstatic修飾子はありません。
(あれ?無いですよね?)

Kotlinのクラスでstaticメンバやstatic関数を扱いたい場合は、
コンパニオンオブジェクトを使います。

コンパニオンオブジェクトとは、
特別なシングルトンインスタンスです。

class Dog {
    companion object {
        val ancestorName: String = "wolf"
    }

    fun greet() {
        println("Bow wow")
    }
}

val ancestor = Dog.ancestorName

companion objectの中で ancestorName というプロパティを定義しました。
このプロパティにアクセスするには Dog.ancestorName と、staticメンバのようにアクセスします。

※ただし、Javaとの互換性のため、staticとしてコンパイルするためのアノテーションは存在します。

object宣言

Kotlinにはシングルトンを簡単に実装する仕組みがあります。

object ResourceManager {
    fun clean() {
        // ...
    }   
}

ResourceManager.clean()

objectキーワードに続けてシングルトンなクラスを定義できます。
通常のクラスと同様にプロパティや関数を定義できますが、
object宣言ではコンストラクタだけ定義できません。
シングルトンなインスタンスにアクセスするにはクラス名を用います。
Javaのstaticメンバへのアクセスに似ています。

dataクラス

データを保持するためのシンプルなクラスは、頻繁に実装しますよね?

data class Point(val x : Int, 
                 val y: Int) {
}

dataキーワードに続けてクラスを定義します。
data classはプライマリコンストラクタで宣言されたプロパティを全て、以下の関数で考慮します。

  • equals関数...一致チェックにプロパティの値を考慮
  • hashCode関数...ハッシュ値生成にプロパティの値を考慮
  • toString関数...プロパティの値を出力

また、copy関数が自動生成されます。
これは、任意のプロパティを変更しながらコピーすることができます。

fun moveTo(point: Point, newX: Int): Point {
    return point.copy(x = newX)
}

プロパティは全てvalで宣言し、クラスはImmutableとしながら、
値を書き換えたコピーは簡単に生成できます。

sealedクラス

sealedクラスは同一ファイル内でしか継承できないクラスです。

sealed class Animal {
}

class Dog: Animal() {
}

class Cat: Animal() {
}
fun greet(animal: Animal) = when(animal) {
    is Dog -> "bow wow"
    is Cat -> "meow meow"
    // else -> elseケースは不要!
}

上記のgreet関数の中のwhenにご注目ください。
elseケースがありません。
これはsealedクラスの効果です。

innerクラス

Javaではクラスの中にクラスを定義する場合、static修飾子をつけるか否かで意味合いが変わります。Kotlinの記述方法と比較してみます。

※あるクラスの中に定義されたAというクラス

Java Kotlin
static class A class A
class A inner class A

Kotlinのデフォルトは
Javaのstatic付きネストクラスと同義となります。

プロパティとバッキングフィールド

Kotlinのプロパティは1行で宣言できますが、
ゲッター、セッターをそれぞれカスタマイズすることが可能です。

class Dog(name: String) {

    var name: String = name
        get() { // カスタムゲッター
            // ゲッターの中ではfield変数が利用できる
            // このゲッターはnameに"ちゃん"をつけて返す
            return field + "ちゃん"
        }
        set(value) { // カスタムセッター
            // セッターは引数名を指定する
            // このセッターは名前の前から空白を除去してフィールドにセットする
            field = value.trimMargin()
        }

}

デリゲート

Kotlinのクラスはデフォルトで継承不可です。
継承をせずに機能を拡張していく仕組みがあります。

Movableというインターフェースと、
Movableを実装したMovableImplクラスを定義しました。

interface Movable {
    fun walk()
    fun run()
    fun currentCount(): Int
}

class MovableImpl: Movable {

    private var count: Int = 0

    override fun walk() {
        this.count += 1
    }
    override fun run() {
        this.count += 3
    }
    override fun currentCount(): Int = this.count
}

このMovableの機能を拡張したDogクラスを定義します。

class Dog(movable: MovableImpl): Movable by movable {

}

byキーワードに続けて、Movableの機能を委譲するインスタンスを指定します。
このDogクラスはMovableインターフェースに関する実装が何もありませんが、
自動的にMovableImplに委譲されます。

val dog = Dog(MovableImpl())
dog.walk()
dog.walk()
dog.run()

println(dog.currentCount()) // 5

プロパティデリゲート

プロパティのゲッター、セッターの振る舞いを委譲することができます。
このようなクラスを定義します。

class Dog {
    var name: String by NameFormatter()
}

class Cat {
    var name: String by NameFormatter()
}

DogクラスもCatクラスもnameというプロパティを持ちます。
どちらもbyに続いてNameFormatterを指定しています。
下のコードを実行してみます。

val dog = Dog()
dog.name = "        ぽち"
println(dog.name) //ぽちちゃん
val cat = Cat()
cat.name = "        たま"
println(cat.name) //たまちゃん

いずれも、nameに代入した文字列から空白を除去し、
ゲッターではサフィックスとして"ちゃん"が付加されています。

それでは、NameFormatterの実装を見てみましょう。

class NameFormatter {

    private var name: String = ""

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return this.name + "ちゃん"
    }
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        this.name = value.trimStart()
    }
}

operatorキーワードに続けて、getValueメソッドとsetValueメソッドを実装します。

このように、プロパティの挙動を外部に委譲することができます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Java開発者に送るKotlinのNull安全

null許容型とnull非許容型

Kotlinの型システムはnullになり得る型と、
なり得ない型を区別します。
nullになり得る型は型名の後に?をつけます。

val num: Int = null // コンパイルエラー nullになり得ない
val num: Int? = null // null許容型であればOK

null許容型の関数やプロパティに直接アクセスすることは出来ません。
コンパイルエラーとなります。

val num: Int? = null // null許容型として宣言
val floatNum = num.toFloat() // コンパイルエラー

NullPointerExceptionのリスクが大きく軽減されます。

安全呼び出し

Javaでこのようなnullチェックは良く見かけるかと思います。

// Java
Dog dog = //nullかも知れない
dog.run(); // NullPointerExceptionの可能性アリ
if (dog != null) {
    dog.run(); // Nullチェック済み
}  

同じことをKotlinでも実装してみます。

val dog: Dog? = //nullかも知れない
dog.run() // コンパイルエラー
if (dog != null) {
    dog.run() // nullチェック済みのブロックではOK
}

null許容型オブジェクトの関数は直接呼び出すことが出来ません。
nullチェック済みのifブロックの中ではnull非許容型に自動的にキャストされ、
関数を呼び出すことが出来ます。
これをスマートキャストと言います。

また、Kotlinにはもっと便利な糖衣構文があります。

val dog: Dog? = // nullかも知れない
dog?.run()

?.に続けて関数の呼び出しやプロパティにアクセスできます。
上の例だとdogがnullだった場合は、ただnullが返されるだけです。
これを安全呼び出しと言います。

安全キャスト

Javaでこのようなダウンキャストのコードを見かけるかと思います。

/*
 * DogクラスはAnimalクラスのサブクラスです
 */
Animal animal = // ...

((Dog)animal).run(); // ClassCastExceptionの可能性アリ

if (animal instanceof Dog) {
    Dog dog = (Dog)animal;
    dog.run();
}

同じことをKotlinで実装してみます。

/*
 * DogクラスはAnimalクラスのサブクラスです
 */
val animal: Animal = // ...

(animal as Dog).run() // // ClassCastExceptionの可能性アリ

if (animal is Dog) {
    animal.run()
}

Kotlinのキャストはas演算子を使います。
キャストに失敗るすとClassCastExceptionがスローされます。

Javaのinstanceof相当するのはis演算子です。
こちらもスマートキャストによって、
ifブロックの中で変数animalはDog型に自動的にキャストされ、
Dogクラスのメソッドがそのまま呼び出せます。

また、Kotlinにはもっと便利な糖衣構文があります。

val animal: Animal = // ...
(animal as? Dog)?.run()

as?演算子によって、上の例ですとDog?型にキャストされます。
キャストできない場合はnullが返るため安全です。

非null表明

安全呼び出し(.?をつける) 以外にnull許容型を扱う方法があります。

val num: Int? = // nullかも知れない
val floatNum = num.toFloat() // コンパイルエラー
val floatNum = num!!.toFloat() // コンパイルOK しかしNullPointerExceptionの可能性アリ

.!! を付けます。
必要な場面もありますが、極力使うべきではないでしょう。

エルビス演算子

Javaでこのような分岐処理をよく見かけるかと思います。

// Java
Dog dog = getDog(); // getDogの戻り値はnullかも知れない
if (dog == null) {
    dog = new Dog();
}
dog.run();

kotlinには「もしnullだったら」の時に便利な演算子があります。

    val dog = getDog() ?: Dog()
    dog.run()

?:エルビス演算子の
左側にはnull許容型の変数
右側にはnullだった場合に実行される処理を記述します。
代替オブジェクトの生成やExceptionをthrowするのが一般的でしょう。

非null許容型のクラスプロパティ

クラスのプロパティを宣言する際に、null許容型にせざるを得ない場合があるかと思います。
コンストラクタをオーバーライド出来ないフレームワークのクラス、
あるコールバックの後でなければ初期化出来ないプロパティなど...

しかしそれらは通常、null許容型で扱うのは面倒で、実質的に非null許容型で扱いたい場合があります。 (実際にKotlinで開発しているとよくあります)

そんな時はlateinit修飾子が便利です。

// ログイン用のViewクラス
class LoginView {
    // TextViewやButtonなどの子要素を持つが、
    // これらはリソースから生成される...という仮想UIシステム
    lateinit var emailTextView: TextView
    lateinit var passwordTextView: TextView
    lateinit var loginButton: Button

    // Viewがリソースからロードされると子要素が取得可能となる
    fun onLoadViewFromResource(viewResource: View) {
        this.emailTextView = viewResource.children[0]
        this.passwordTextView = viewResource.children[1]
        this.loginButton = viewResource.children[2]
    }
}

lateinit修飾子は、初期化を遅らせることが出来ますが、
初期化前にプロパティにアクセスするとExceptionとなるので注意が必要です。

この他に、lazyというプロパティデリゲートがあります。
初期化にコストのかかるプロパティなどを遅延初期化させる時に便利です。

class LoginView {

    val loginConnection: LoginConnection by lazy {
        LoginConnection() // LoginConnectionは生成にコストがかかる
    }

}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Java開発者に送るKotlinの関数とラムダ

関数の定義

関数は以下のように定義します。

// Int型の引数をインクリメントして返す関数
fun increment(i: Int): Int {
    return i + 1
}

fun 関数名(引数リスト): 戻り値の型
これが基本形です。

名前付き引数

引数の多い関数の呼び出しでは、どの値がどの引数に対応しているのか
分かりづらくなることがあります。

Kotlinはこの問題を解消する機能があります。

/*
 * たこ焼きを注文します
 *
 * @param isKarashiMayo からしマヨネーズを選択するか否か
 * @param isPutAonori 青のりののせるか否か
 *
 * @return たこ焼きのインスタンスが返ります
 */
fun oderTakoyaki(isKarashiMayo: Boolean, isPutAonori: Boolean): Takoyaki {
    // ...
}

val takoyaki = oderTakoyaki(true, false)
// どっちフラグが青のりだっけ?


val takoyaki = oderTakoyaki(isKarashiMayo = true, isPutAonori = false)
// 呼び出し側に仮引数名をつけることができます
// コードの意図が読み取り易い


val takoyaki = oderTakoyaki(isPutAonori = false, isKarashiMayo = true)
// 名前を付けると、関数に定義されている順番でなくてもOK

デフォルト引数

引数の数が違うだけのオーバーロードが多数存在するクラスが、よくあると思います。
例えば...

Kotlinではデフォルト引数を用いることで、このようにバリエーションが増えてしまう状況を回避できます。

/*
 * たこ焼きを注文します
 *
 * @param isKarashiMayo からしマヨネーズを選択するか否か
 * @param isPutAonori 青のりののせるか否か
 *
 * @return たこ焼きのインスタンスが返ります
 */
fun oderTakoyaki(isKarashiMayo: Boolean = false, isPutAonori: Boolean = true): Takoyaki {
    // ...
}

// 呼び出し時に引数を省略可能。省略した場合は、デフォルト値が使用される。
val takoyaki = oderTakoyaki()

拡張関数

既存のクラスに、後から勝手に関数を追加できます。
独自の日付フォーマットでパースする関数をString型に追加してみましょう。

fun String.toDate(): Date {
    val format = SimpleDateFormat("yyyy年MM月dd日")
    return format.parse(this)
}

val str = "2019年03月09日"
val date = str.toDate()

fun 拡張対象の型.関数名(引数リスト): 戻り値
↑のように定義します。
ちなみに、通常の関数は...
fun 関数名(引数リスト): 戻り値の型
↑こうでした。
関数名の前に拡張対象の型をつけると拡張関数になります。

ちなみに、この拡張対象のオブジェクトをKotlinではレジーバーオブジェクトと呼びます。

単一式関数

関数が1つの式からなる場合は、{ }を省略することが可能です。
単一式関数は戻り値の型が推論可能な場合、戻り値の型を省略することもできます。

// 通常の関数
fun double(x: Int): Int {
    return x * 2
}

// 単一式関数で記述
fun double(x: Int): Int = x * 2

// 単一式関数で戻り値の型を推論
fun double(x: Int) = x * 2

ラムダ式

ラムダ式は↓のように書きます。

val sum = { x: Int, y: Int ->
    x + y
}

{引数リスト-> 本体 }
または引数がなければ
{ 本体 }
となります。

ラムダ式のパラメータが1つの場合は暗黙の変数itが使用できます。

val double: (Int)->Int = {
    it * 2
}

尚、関数型は次のように記述します。
(引数の型) -> 戻り値の型

inline関数

ラムダ式は実行コストの高い仕組みです。
Kotlinのラムダ式は無名クラスとして実現されています。変数をキャプチャしたラムダ式は実行されるたびに無名クラスのインスタンスが生成されます。
(Kotlin1.1 Java8バイトコード生成は直接Javaのラムダ式にコンパイルされるらしい)

そこで、inline関数です。
inline関数は呼び出される箇所に関数本体を置き換えてコンパイルされます。

inline fun edit(context: DataBaseContext, block: (DataBaseContext.Transaction)->Unit) {
    val transaction = context.beginTransaction()
    block(transaction)
    transaction.commit()
}

inline関数が効力を発揮するのは
引数にラムダ式を受け、それを内部で実行する関数です。
inline関数では、引数のラムダ式も一緒に呼び出し元に展開します。

上のinline関数editを呼び出すサンプルです。

val dataBaseContext = DataBaseContext()

edit(dataBaseContext) { transaction ->
    val newObject = Any()
    transaction.add(newObject)
}

edit関数はコンパイル時に展開され、次のようなコードになります(イメージです)

val dataBaseContext = DataBaseContext()

// ここからedit関数が展開
val transaction = dataBaseContext.beginTransaction()
val newObject = Any()
transaction.add(newObject)
transaction.commit()
// ここまでedit関数が展開

もう一つのinlineの出番

Kotlinのジェネリックは実行時に型情報は失われます。

例えば、次の関数はコンパイルエラーとなります。

fun <T> mapFilter(list: List<Any>): List<T> {
    return list.filter { it is T }
        .map { it as T }
}

mapFilter関数の型パラメータTは実行時に失われるためです。
そこで、inline関数にreified修飾子を付加します。

//          ↓ここ!
inline fun <reified T> mapFilter(list: List<Any>): List<T> {
    return list.filter { it is T }
        .map { it as T }
}

このようにすることで、実行時に型情報を保持したまま展開されるため
mapFilter関数がコンパイルできるようになります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Java開発者に送るKotlinのClass

Java開発者の方に向けて、Kotlinのクラスについて書きます。

クラスの定義

classキーワードに続いて、クラス名を記述します。
Dogクラスを定義してみます。

class Dog {
}

クラスの継承やインターフェースの実装はコロンに続けて記述します。
先ほどのDogクラス、Animalクラスを継承させてみます。

class Dog: Animal() {
}

さらに、Walkableインターフェースを実装してみましょう。

class Dog: Animal(), Walkable {
}

プロパティ

Kotlinのクラスはプロパティを持つことができます。
これはJavaのフィールド、ゲッター、セッターを合わせたものです。
DogクラスにnameというString型のプロパティを宣言してみましょう。
varキーワードを使います。

class Dog {
    var name: String = "pochi"
}


val dog = Dog()
val aName = dog.name // ゲッターとして動作
dog.name = "taro" // セッターとして動作

読み取り専用にしたければ、valキーワードを使いましょう。

class Dog {
    val name: String = "pochi"
}

val dog = Dog()
val aName = dog.name // ゲッターとして動作
dog.name = "taro" // コンパイルエラー!

valキーワードは再代入不可のため、クラス内部からも値を変更できません。
クラス内で変更可能とする場合はprivate setキーワードを使います。

class Dog {
    var name: String = "pochi"
        private set

    fun updateName(newName: String) {
        this.name = newName
    }
}

val dog = Dog()
dog.updateName("taro")

ここで、「いやいや、JavaでフィールドをPublicにしたのと一緒でしょ?」と思った方は鋭いです。
Kotlinのプロパティはゲッター、セッターそしてフィールドが明確に分かれています。
詳しくはこちら

プライマリコンストラクタ

Kotlinのコンストラクタは少し変わった位置に書きます。

class Dog constructor(/* ここがコンストラクタ */) {
}

constructorキーワードは省略できます。アノテーションを付加する場合などは省略できません。
この位置に書くコンストラクタをプライマリコンストラクタと呼びます。
コンストラクタと言っても、記述できるのはプロパティとコンストラクタの引数リストです。

コンストラクタでプロパティ定義する

コンストラクタ引数にvarvalキーワードをつけると、そのままプロパティになります。

class Dog(var name: String) {
}

↑は↓と同義です。

class Dog(name: String) {
    var name: String = name
}

JavaとKotlinを見比べてみる

JavaでDogクラスを定義します。
nameとageというフィールドがあり、それぞれゲッターとセッターが定義されています。

public class Dog {

    private String name;
    private int age;

    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

}

これをKotlinで書くと?

class Dog(var name: String, var age: Int)

これだけで済んでしまいます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Googleアシスタント + Kotlinでアクションのバックエンドを作ってみる

はじめに

2019/01/16にActions on Google(AoG)Client LibraryのJava/Kotlin版が発表されました :tada:

公式では、Kotlin + Google App Engine上のJavaサーブレットで構築する方法が紹介されています。

KotlinにはKtorというWebフレームワークがあります。
今回はフルKotlinでGoogleアシスタントのアクションを構築する方法を紹介します。

使う技術

次の表に、各言語で使う技術構成をまとめました。
GoogleアシスタントのバックエンドはNode.js + Cloud Functions For Firebaseの構成で作られることが多いです。
今回は、Kotlin + Ktor + Google App Engine(GAE)で作ります! :muscle:
Ktor, GAEは環境構築の一番最初の部分から解説していきます。

言語 フレームワーク 実行環境
Kotlin Ktor PaaS: Google App Engien
Node.js (Express) FaaS: Cloud Functions For Firebase

Ktorについて

Ktor とは、Kotlinの軽量で簡単に使えるWebフレームワークです。
おなじみJetBrains社が開発をしており、安心感があります。

去年(2018)の11月にv1.0がリリースされており、Kotlinサーバサイドでの利用が期待が高まっています。
- Ktor 1.0 Released: A Connected Applications Framework by JetBrains | Kotlin Blog

構築手順

Actions on GoogleプロジェクトとDialogflowエージェントの作成

こちらは従来どおりの作り方と変わりありません。

Ktorの環境構築

Intellij IDEにKtorプラグインをインストールしてプロジェクトを作成するのが簡単です。

Ktorプロジェクトを作成できたら、Hello Worldを表示してみましょう。
src/Application.kt を↓のように編集し、Gradleタスクの「run」を押して実行してみましょう。
成功すれば、http://0.0.0.0:8080/ にアクセスした時に「hello world」がブラウザに表示されます。

package com.example.kotlin.aog

import io.ktor.application.*
import io.ktor.response.*
import io.ktor.routing.get
import io.ktor.routing.routing

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

@Suppress("unused") // Referenced in application.conf
@kotlin.jvm.JvmOverloads
fun Application.module(testing: Boolean = false) {
    // ↓↓↓↓↓ 以下を追加する ↓↓↓↓↓
    routing {
        get("/"){
            call.respond("hello world")
        }
    }
}

AoG Java/Kotlinクライアントライブラリの使い方

build.gradle に以下を追記し、Gradle Syncを実行します。

compile "com.google.actions:actions-on-google:1.0.0"

IntentHandler作成

新規にConversationComponentsApp.ktを作成してください。
Handlerの作成ポイントは

  1. DialogflowAppを継承させる。
  2. ForIntentアノテーションを使ってDialogflowで定義したIntent名と紐付ける。
  3. ResponseBuilderを使ってレスポンスを組み立てる。

コードはこんな感じです。例ではSimpleResponseを使っていますが、
他のBasicCardCarouselTableなどは公式がサンプルをGithubあげています。
気になる方は、こちらを見てみるといいでしょう。

class ConversationComponentsApp : DialogflowApp() {

    @ForIntent("Default Welcome Intent")
    fun welcome(request: ActionRequest): ActionResponse {
        val responseBuilder = getResponseBuilder(request)
        responseBuilder
            .add(SimpleResponse()
                .setDisplayText("純情Action!")
                .setTextToSpeech("純情Action!Action!"))
        return responseBuilder.build()
    }
}

POSTリクエストのエンドポイント用意

続いて、作ったIntentHandlerにDialogflowからのデータを渡せるようにしましょう。
DialogflowからはPOSTリクエストでJSONが送られてくるので、次のコードの通りに / をルートパスとしてJSONを受け取ります。

call.receiveText()でJSONデータを受け取り、
先程作ったした ConversationComponentsApp#handleRequestにヘッダー情報と一緒に渡します。
これによりIntent名に対応してレスポンスが返されます。
最後に、call.respond を使ってDialogflow側に返却してあげます。

fun Application.module(testing: Boolean = false) {
    val actionsApp= ConversationComponentsApp()
    routing {
        post("/"){
            // body取得
            val jsonBody: String = call.receiveText()
            // header取得
            val headerMap  = getHeaderMap(call.request)
            // IntentHandlerにbody,header情報を渡す
            val jsonResponse = actionsApp.handleRequest(jsonBody, headerMap).get()
            call.respond(jsonResponse)
        }
    }
}
// ヘッダー情報をHashMapにKey-Valueでつっこんであげる。
fun getHeaderMap(request: ApplicationRequest): Map<String, String>{
    val headers= request.headers
    val headerMap = HashMap<String, String>()
    headers.forEach{ k, v -> headerMap[k] = v.toString() }
    return headerMap
}

GAEへのデプロイ準備

ターミナル編

Google Cloud SDKをMacにインストールし、初期化まで完了させます。

初期化中にGoogle Cloud Platformプロジェクトを選択する必要があります。Actions on GoogleプロジェクトIDと同じものを選びましょう。

設定できたら、現在の設定に間違いないか確認しておくといいでしょう。

$ gcloud config list
[app]
promote_by_default = true
[core]
account = <AoGプロジェクト作成時と同じGoogleアカウントのメールアドレス>
disable_usage_reporting = False
project = <Actions on GoogleのプロジェクトID(GCP Project id)>

GAEのJavaプラグインをインストールします。

$ gcloud components install app-engine-java
$ gcloud components update

Intellij IDE編

Build.gradleに以下を追加してください。
これで、GAE用のGradleタスクが活用できるようになります。

buildscript {
    ... 
    dependencies {
        ...
        classpath "com.google.cloud.tools:appengine-gradle-plugin:1.3.4"
    }
}
apply plugin: 'war'
dependencies {
    ...
    providedCompile "com.google.appengine:appengine:1.9.60"
}

appengine {  
    tools.cloudSdkHome="<google-cloud-sdkディレクトリへのパス>"
}

ここは任意ですが、ディレクトリ構成を整理しておきます。
kotlinファイルは、src/main/kotlinに、プロジェクトルートに存在するktorの設定ファイルをsrc/main/resourcesに移動させます。
また、src/main/webappというディレクトリを追加します。詳細については↓を参考ください:bow:

ディレクトリツリー構造はこんな感じになります。

...
.
└── main
    ├── kotlin
    │   ├── Application.kt
    │   └── ConversationComponentsApp.kt
    ├── resources
    │   ├── application.conf
    │   └── logback.xml
    └── webapp
        └── WEB-INF
            ├── appengine-web.xml
            ├── logging.properties
            └── web.xml

GAEへデプロイ! と動作確認!

ここまでの設定が完了したら、
./gradlew appengineDeployかIDEのGradleタスクからappengineDeployを実行してください。
成功すると、https://<gcp-project-id>.appspot.comのURLがコンソールに表示されるので、
こちらをメモっておき、DialogflowのFullfillmentに設定すれば完了です!

...
Deployed service [default] to [https://<gcp-project-id>.appspot.com]
...

今回説明したコードはGithubにあげておきました。参考にしてみてください。
- https://github.com/1coin178/actions-on-google-kotlin-ktor-start-kit

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kotlin+Googleアシスタントでアクションのバックエンドを作ってみる

はじめに

2019/01/16にActions on Google(AoG)Client LibraryのJava/Kotlin版が発表されました :tada:

公式では、Kotlin + Google App Engine上のJavaサーブレットで構築する方法が紹介されています。

KotlinにはKtorというWebフレームワークがあります。
今回はフルKotlinでGoogleアシスタントのアクションを構築する方法を紹介します。

使う技術

次の表に、各言語で使う技術構成をまとめました。
GoogleアシスタントのバックエンドはNode.js + Cloud Functions For Firebaseの構成で作られることが多いです。
今回は、Kotlin + Ktor + Google App Engine(GAE)で作ります! :muscle:
Ktor, GAEは環境構築の一番最初の部分から解説していきます。

言語 フレームワーク 実行環境
Kotlin Ktor PaaS: Google App Engine
Node.js (Express) FaaS: Cloud Functions For Firebase

Ktorについて

Ktor とは、Kotlinの軽量で簡単に使えるWebフレームワークです。
おなじみJetBrains社が開発をしており、安心感があります。

去年(2018)の11月にv1.0がリリースされており、Kotlinサーバサイドでの利用が期待が高まっています。
- Ktor 1.0 Released: A Connected Applications Framework by JetBrains | Kotlin Blog

構築手順

Actions on GoogleプロジェクトとDialogflowエージェントの作成

こちらは従来どおりの作り方と変わりありません。

開発環境の準備

↑のGihtubリポジトリにテンプレートを作成しておきました。環境構築の際にはぜひお使いください!
ダウンロード後、Intellij IDEで開いてください。

AoG Java/Kotlinクライアントライブラリの使い方

次に本題のAoG Java/Kotlinクライアントライブラリの使い方を解説していきます。

AoG Java/Kotlinクライアントライブラリは、
build.gradle に↓を指定することでダウンロードしています。

compile "com.google.actions:actions-on-google:1.0.0"

IntentHandler作成

Dialogflowからのリクエストを受け取り処理するためのIntentHandlerを作成します。

新規にConversationComponentsApp.ktを作成してください。
IntentHandlerの作成ポイントは

  1. DialogflowAppを継承させる。
  2. ForIntentアノテーションを使ってDialogflowで定義したIntent名と処理を紐付ける。
  3. ResponseBuilderを使ってレスポンスを組み立てる。

コードはこんな感じです。

class ConversationComponentsApp : DialogflowApp() {

    @ForIntent("Default Welcome Intent")
    fun welcome(request: ActionRequest): ActionResponse {
        val responseBuilder = getResponseBuilder(request)
        responseBuilder
            .add(SimpleResponse()
                .setDisplayText("純情Action!")
                .setTextToSpeech("純情Action!Action!"))
        return responseBuilder.build()
    }

    @ForIntent("Good Bye")
    fun goodBye(request: ActionRequest): ActionResponse {
        val responseBuilder = getResponseBuilder(request)
        responseBuilder
            .add("会話を終了します。さようなら")
            .endConversation()    // 会話を終了させる。 Node.js版でのconv.close
        return responseBuilder.build()
    }
}

例ではSimpleResponseを使っていますが、他にBasicCardCarouselTableなどのサンプルを公式がGithubあげてくれています。
気になる方は、こちら↓も見てみるといいでしょう!

POSTリクエストのエンドポイント用意

続いて、作ったIntentHandlerにDialogflowからのデータを渡せるようにしましょう。
DialogflowからはPOSTリクエストでJSONが送られてくるので、次のコードの通りに / をルートパスとしてJSONを受け取ります。

call.receiveText()でJSONデータを受け取り、
先程作ったした ConversationComponentsApp#handleRequestにヘッダー情報と一緒に渡します。
これによりIntent名に対応してレスポンスが返されます。
最後に、call.respond を使ってDialogflow側に返却してあげます。

fun Application.module(testing: Boolean = false) {
    val actionsApp= ConversationComponentsApp()
    routing {
        post("/"){
            // body取得
            val jsonBody: String = call.receiveText()
            // header取得
            val headerMap  = getHeaderMap(call.request)
            // IntentHandlerにbody,header情報を渡す
            val jsonResponse = actionsApp.handleRequest(jsonBody, headerMap).get()
            call.respond(jsonResponse)
        }
    }
}

// ヘッダー情報をHashMapにKey-Valueでつっこんであげる。
fun getHeaderMap(request: ApplicationRequest): Map<String, String>{
    val headers= request.headers
    val headerMap = HashMap<String, String>()
    headers.forEach{ k, v -> headerMap[k] = v.toString() }
    return headerMap
}

GAEへのデプロイ準備

ターミナル編

Google Cloud SDKをMacにインストールし、初期化まで完了させます。

初期化中にGoogle Cloud Platformプロジェクトを選択する必要があります。Actions on GoogleプロジェクトIDと同じものを選びましょう。

設定できたら、現在の設定に間違いないか確認しておくといいでしょう。

$ gcloud config list
[app]
promote_by_default = true
[core]
account = <AoGプロジェクト作成時と同じGoogleアカウントのメールアドレス>
disable_usage_reporting = False
project = <Actions on GoogleのプロジェクトID(GCP Project id)>

GAEのJavaプラグインをインストールします。

$ gcloud components install app-engine-java
$ gcloud components update

Intellij IDE編

GAE関連の設定を解説します。

build.gradleに↓を記入しておくことで、GAE用のGradleタスクが活用できるようになります。

buildscript {
    ... 
    dependencies {
        ...
        classpath "com.google.cloud.tools:appengine-gradle-plugin:1.3.4"
    }
}

...
apply plugin: 'war'
... 

dependencies {
    ...
    providedCompile "com.google.appengine:appengine:1.9.60"
}

appengine {  
    // ご自身の環境に合わせて変更してください。
    tools.cloudSdkHome="<google-cloud-sdkディレクトリへのパス>"
}

テンプレートコードのディレクトリ構成を整理しておきます。
ディレクトリツリー構造はこんな感じになっています。

...
.
└── main
    ├── kotlin
    │   ├── Application.kt
    │   └── ConversationComponentsApp.kt
    ├── resources
    │   ├── application.conf
    │   └── logback.xml
    └── webapp
        └── WEB-INF
            ├── appengine-web.xml
            ├── logging.properties
            └── web.xml

kotlinファイルは、src/main/kotlinに、プロジェクトルートに存在するktorの設定ファイルがsrc/main/resourcesにあります。
src/main/webappというディレクトリがあるのですが、詳細については↓を参考にしてみてください:bow:

GAEへデプロイ! と動作確認!

ここまでの設定が完了したら、
./gradlew appengineDeployかIDEのGradleタスクからappengineDeployを実行してください。
成功すると、https://<gcp-project-id>.appspot.comのURLがコンソールに表示されるので、
こちらをメモっておき、DialogflowのFullfillmentに設定すれば完了です!

...
Deployed service [default] to [https://<gcp-project-id>.appspot.com]
...
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kotlin + Googleアシスタントでアクションのバックエンドを作ってみる

はじめに

2019/01/16にActions on Google(AoG)Client LibraryのJava/Kotlin版が発表されました :tada:

公式では、Kotlin + Google App Engine上のJavaサーブレットで構築する方法が紹介されています。

KotlinにはKtorというWebフレームワークがあります。
今回はフルKotlinでGoogleアシスタントのアクションを構築する方法を紹介します。

使う技術

次の表に、各言語で使う技術構成をまとめました。
GoogleアシスタントのバックエンドはNode.js + Cloud Functions For Firebaseの構成で作られることが多いです。
今回は、Kotlin + Ktor + Google App Engine(GAE)で作ります! :muscle:
Ktor, GAEは環境構築の一番最初の部分から解説していきます。

言語 フレームワーク 実行環境
Kotlin Ktor PaaS: Google App Engine
Node.js (Express) FaaS: Cloud Functions For Firebase

Ktorについて

Ktor とは、Kotlinの軽量で簡単に使えるWebフレームワークです。
おなじみJetBrains社が開発をしており、安心感があります。

去年(2018)の11月にv1.0がリリースされており、Kotlinサーバサイドでの利用が期待が高まっています。
- Ktor 1.0 Released: A Connected Applications Framework by JetBrains | Kotlin Blog

構築手順

Actions on GoogleプロジェクトとDialogflowエージェントの作成

こちらは従来どおりの作り方と変わりありません。

開発環境の準備(テンプレートを利用)

↑のGihtubリポジトリにテンプレートを作成しておきました。環境構築の際にはぜひお使いください!
ダウンロード後、Intellij IDEで開いてください。

AoG Java/Kotlinクライアントライブラリの使い方

次に本題のAoG Java/Kotlinクライアントライブラリの使い方を解説していきます。

AoG Java/Kotlinクライアントライブラリは、
build.gradle に↓を指定することでダウンロードしています。

compile "com.google.actions:actions-on-google:1.0.0"

IntentHandler作成

Dialogflowからのリクエストを受け取り処理するためのIntentHandlerを作成します。

新規にConversationComponentsApp.ktを作成してください。
IntentHandlerの作成ポイントは

  1. DialogflowAppを継承させる。
  2. ForIntentアノテーションを使ってDialogflowで定義したIntent名と処理を紐付ける。
  3. ResponseBuilderを使ってレスポンスを組み立てる。

コードはこんな感じです。

class ConversationComponentsApp : DialogflowApp() {

    @ForIntent("Default Welcome Intent")
    fun welcome(request: ActionRequest): ActionResponse {
        val responseBuilder = getResponseBuilder(request)
        responseBuilder
            .add(SimpleResponse()
                .setDisplayText("純情Action!")
                .setTextToSpeech("純情Action!Action!"))
        return responseBuilder.build()
    }

    @ForIntent("Good Bye")
    fun goodBye(request: ActionRequest): ActionResponse {
        val responseBuilder = getResponseBuilder(request)
        responseBuilder
            .add("会話を終了します。さようなら")
            .endConversation()    // 会話を終了させる。 Node.js版でのconv.close
        return responseBuilder.build()
    }
}

例ではSimpleResponseを使っていますが、他にBasicCardCarouselTableなどのサンプルを公式がGithubあげてくれています。
気になる方は、こちら↓も見てみるといいでしょう!

POSTリクエストのエンドポイント用意

続いて、作ったIntentHandlerにDialogflowからのデータを渡せるようにしましょう。
DialogflowからはPOSTリクエストでJSONが送られてくるので、次のコードの通りに / をルートパスとしてJSONを受け取ります。

call.receiveText()でJSONデータを受け取り、
先程作ったした ConversationComponentsApp#handleRequestにヘッダー情報と一緒に渡します。
これによりIntent名に対応してレスポンスが返されます。
最後に、call.respond を使ってDialogflow側に返却してあげます。

fun Application.module(testing: Boolean = false) {
    val actionsApp= ConversationComponentsApp()
    routing {
        post("/"){
            // body取得
            val jsonBody: String = call.receiveText()
            // header取得
            val headerMap  = getHeaderMap(call.request)
            // IntentHandlerにbody,header情報を渡す
            val jsonResponse = actionsApp.handleRequest(jsonBody, headerMap).get()
            call.respond(jsonResponse)
        }
    }
}

// ヘッダー情報をHashMapにKey-Valueでつっこんであげる。
fun getHeaderMap(request: ApplicationRequest): Map<String, String>{
    val headers= request.headers
    val headerMap = HashMap<String, String>()
    headers.forEach{ k, v -> headerMap[k] = v.toString() }
    return headerMap
}

GAEへのデプロイ準備

ターミナル編

Google Cloud SDKをMacにインストールし、初期化まで完了させます。

初期化中にGoogle Cloud Platformプロジェクトを選択する必要があります。Actions on GoogleプロジェクトIDと同じものを選びましょう。

設定できたら、現在の設定に間違いないか確認しておくといいでしょう。

$ gcloud config list
[app]
promote_by_default = true
[core]
account = <AoGプロジェクト作成時と同じGoogleアカウントのメールアドレス>
disable_usage_reporting = False
project = <Actions on GoogleのプロジェクトID(GCP Project id)>

GAEのJavaプラグインをインストールします。

$ gcloud components install app-engine-java
$ gcloud components update

Intellij IDE編

GAE関連の設定を解説します。

build.gradleに↓を記入しておくことで、GAE用のGradleタスクが活用できるようになります。

buildscript {
    ... 
    dependencies {
        ...
        classpath "com.google.cloud.tools:appengine-gradle-plugin:1.3.4"
    }
}

...
apply plugin: 'war'
... 

dependencies {
    ...
    providedCompile "com.google.appengine:appengine:1.9.60"
}

appengine {  
    // ご自身の環境に合わせて変更してください。
    tools.cloudSdkHome="<google-cloud-sdkディレクトリへのパス>"
}

テンプレートコードのディレクトリ構成を整理しておきます。
ディレクトリツリー構造はこんな感じになっています。

...
.
└── main
    ├── kotlin
    │   ├── Application.kt
    │   └── ConversationComponentsApp.kt
    ├── resources
    │   ├── application.conf
    │   └── logback.xml
    └── webapp
        └── WEB-INF
            ├── appengine-web.xml
            ├── logging.properties
            └── web.xml

kotlinファイルは、src/main/kotlinに、プロジェクトルートに存在するktorの設定ファイルがsrc/main/resourcesにあります。
src/main/webappというディレクトリがあるのですが、詳細については↓を参考にしてみてください:bow:

GAEへデプロイ! と動作確認!

ここまでの設定が完了したら、
./gradlew appengineDeployかIDEのGradleタスクからappengineDeployを実行してください。
成功すると、https://<gcp-project-id>.appspot.comのURLがコンソールに表示されるので、
こちらをメモっておき、DialogflowのFullfillmentに設定すれば完了です!

...
Deployed service [default] to [https://<gcp-project-id>.appspot.com]
...
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Actions on Google + Kotlin + Ktorでアクションのバックエンドを作ってみる

はじめに

2019/01/16にActions on Google(AoG)Client LibraryのJava/Kotlin版が発表されました :tada:

公式では、Kotlin + Google App Engine上のJavaサーブレットで構築する方法が紹介されています。

KotlinにはKtorというWebフレームワークがあります。
今回はフルKotlinでGoogleアシスタントのアクションを構築する方法を紹介します。

使う技術

次の表に、各言語で使う技術構成をまとめました。
GoogleアシスタントのバックエンドはNode.js + Cloud Functions For Firebaseの構成で作られることが多いです。
今回は、Kotlin + Ktor + Google App Engine(GAE)で作ります! :muscle:
Ktor, GAEは環境構築の一番最初の部分から解説していきます。

言語 フレームワーク 実行環境
Kotlin Ktor PaaS: Google App Engien
Node.js (Express) FaaS: Cloud Functions For Firebase

Ktorについて

Ktor とは、Kotlinの軽量で簡単に使えるWebフレームワークです。
おなじみJetBrains社が開発をしており、安心感があります。

去年(2018)の11月にv1.0がリリースされており、Kotlinサーバサイドでの利用が期待が高まっています。
- Ktor 1.0 Released: A Connected Applications Framework by JetBrains | Kotlin Blog

構築手順

Actions on GoogleプロジェクトとDialogflowエージェントの作成

こちらは従来どおりの作り方と変わりありません。

Ktorの環境構築

Intellij IDEにKtorプラグインをインストールしてプロジェクトを作成するのが簡単です。

Ktorプロジェクトを作成できたら、Hello Worldを表示してみましょう。
src/Application.kt を↓のように編集し、Gradleタスクの「run」を押して実行してみましょう。
成功すれば、http://0.0.0.0:8080/ にアクセスした時に「hello world」がブラウザに表示されます。

package com.example.kotlin.aog

import io.ktor.application.*
import io.ktor.response.*
import io.ktor.routing.get
import io.ktor.routing.routing

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

@Suppress("unused") // Referenced in application.conf
@kotlin.jvm.JvmOverloads
fun Application.module(testing: Boolean = false) {
    // ↓↓↓↓↓ 以下を追加する ↓↓↓↓↓
    routing {
        get("/"){
            call.respond("hello world")
        }
    }
}

AoG Java/Kotlinクライアントライブラリの使い方

build.gradle に以下を追記し、Gradle Syncを実行します。

compile "com.google.actions:actions-on-google:1.0.0"

IntentHandler作成

新規にConversationComponentsApp.ktを作成してください。
Handlerの作成ポイントは

  1. DialogflowAppを継承させる。
  2. ForIntentアノテーションを使ってDialogflowで定義したIntent名と紐付ける。
  3. ResponseBuilderを使ってレスポンスを組み立てる。

コードはこんな感じです。例ではSimpleResponseを使っていますが、
他のBasicCardCarouselTableなどは公式がサンプルをGithubあげています。
気になる方は、こちらを見てみるといいでしょう。

class ConversationComponentsApp : DialogflowApp() {

    @ForIntent("Default Welcome Intent")
    fun welcome(request: ActionRequest): ActionResponse {
        val responseBuilder = getResponseBuilder(request)
        responseBuilder
            .add(SimpleResponse()
                .setDisplayText("純情Action!")
                .setTextToSpeech("純情Action!Action!"))
        return responseBuilder.build()
    }
}

POSTリクエストのエンドポイント用意

続いて、作ったIntentHandlerにDialogflowからのデータを渡せるようにしましょう。
DialogflowからはPOSTリクエストでJSONが送られてくるので、次のコードの通りに / をルートパスとしてJSONを受け取ります。

call.receiveText()でJSONデータを受け取り、
先程作ったした ConversationComponentsApp#handleRequestにヘッダー情報と一緒に渡します。
これによりIntent名に対応してレスポンスが返されます。
最後に、call.respond を使ってDialogflow側に返却してあげます。

fun Application.module(testing: Boolean = false) {
    val actionsApp= ConversationComponentsApp()
    routing {
        post("/"){
            // body取得
            val jsonBody: String = call.receiveText()
            // header取得
            val headerMap  = getHeaderMap(call.request)
            // IntentHandlerにbody,header情報を渡す
            val jsonResponse = actionsApp.handleRequest(jsonBody, headerMap).get()
            call.respond(jsonResponse)
        }
    }
}
// ヘッダー情報をHashMapにKey-Valueでつっこんであげる。
fun getHeaderMap(request: ApplicationRequest): Map<String, String>{
    val headers= request.headers
    val headerMap = HashMap<String, String>()
    headers.forEach{ k, v -> headerMap[k] = v.toString() }
    return headerMap
}

GAEへのデプロイ準備

ターミナル編

Google Cloud SDKをMacにインストールし、初期化まで完了させます。

初期化中にGoogle Cloud Platformプロジェクトを選択する必要があります。Actions on GoogleプロジェクトIDと同じものを選びましょう。

設定できたら、現在の設定に間違いないか確認しておくといいでしょう。

$ gcloud config list
[app]
promote_by_default = true
[core]
account = <AoGプロジェクト作成時と同じGoogleアカウントのメールアドレス>
disable_usage_reporting = False
project = <Actions on GoogleのプロジェクトID(GCP Project id)>

GAEのJavaプラグインをインストールします。

$ gcloud components install app-engine-java
$ gcloud components update

Intellij IDE編

Build.gradleに以下を追加してください。
これで、GAE用のGradleタスクが活用できるようになります。

buildscript {
    ... 
    dependencies {
        ...
        classpath "com.google.cloud.tools:appengine-gradle-plugin:1.3.4"
    }
}
apply plugin: 'war'
dependencies {
    ...
    providedCompile "com.google.appengine:appengine:1.9.60"
}

appengine {  
    tools.cloudSdkHome="<google-cloud-sdkディレクトリへのパス>"
}

ここは任意ですが、ディレクトリ構成を整理しておきます。
kotlinファイルは、src/main/kotlinに、プロジェクトルートに存在するktorの設定ファイルをsrc/main/resourcesに移動させます。
また、src/main/webappというディレクトリを追加します。詳細については↓を参考ください:bow:

ディレクトリツリー構造はこんな感じになります。

...
.
└── main
    ├── kotlin
    │   ├── Application.kt
    │   └── ConversationComponentsApp.kt
    ├── resources
    │   ├── application.conf
    │   └── logback.xml
    └── webapp
        └── WEB-INF
            ├── appengine-web.xml
            ├── logging.properties
            └── web.xml

GAEへデプロイ! と動作確認!

ここまでの設定が完了したら、
./gradlew appengineDeployかIDEのGradleタスクからappengineDeployを実行してください。
成功すると、https://<gcp-project-id>.appspot.comのURLがコンソールに表示されるので、
こちらをメモっておき、DialogflowのFullfillmentに設定すれば完了です!

...
Deployed service [default] to [https://<gcp-project-id>.appspot.com]
...

今回説明したコードはGithubにあげておきました。参考にしてみてください。
- https://github.com/1coin178/actions-on-google-kotlin-ktor-start-kit

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kotlinのイライラするところ(IDE含め)

この記事の動機

kotlinでのプログラミングはいろいろと遊べて楽しい。
しかし同時に、イライラすることもある。

環境

Kotlin 1.3.11
IntelliJ idea Ultimate

① contract

contractは楽しい。でもいやなところもある。

operator fun 非対応。

inline operator fun Hoge.invoke(block: Hoge.()->Unit) のようなふざけた実装をしたいときはまれによく訪れる。Rendererとかを実装するときとかね。
contractはトップレベル関数でしか使えないのは周知だ。納得している。だが、なぜなのだ… 拡張関数だのに…

Hoge.kt
@ExperimentalContracts
inline operator fun Hoge.invoke(block: Hoge.()->Unit) {

    contract {

        callsInPlace(block, InvocationKind.EXACTLY_ONCE)

    }

    block(this)
}

 一見よさげな実装だが、実際には「Contracts are not allowed for operator functions」と怒られてしまう。何がお気に召さないのか。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kotlinでデザインパターン Template Method編

はじめに

本記事はJava言語で学ぶデザインパターン入門を参考にしながら
JavaではなくKotlinで実装してみようというものです。

Java言語で学ぶデザインパターン入門

調べてみる

Template Methodについて調べてみます。
WikpediaではTemplate Methodは次のように説明されています。

Template Method パターンの目的は、ある処理のおおまかなアルゴリズムをあらかじめ決めておいて、
そのアルゴリズムの具体的な設計をサブクラスに任せることである。
そのため、システムのフレームワークを構築するための手段としてよく活用される

毎回思うのですがWikipediaはあぁそうなんだ〜で終わる説明ですね。
Template Methodの理解を深めるためには実装したほうが早そうですので実装していきます。

実装してみる

次の順序でファイルにPersonを出力する処理をアルゴリズムとします。
このアルゴリズムをTemplate Methodパターンを用いて実装したいと思います。

処理1. ファイルをオープンする
処理2. ファイルにPersonを書き込む
処理3. ファイルをクローズする

Person

Personについてですが次のように定義しました。
特にデータにはこだわりありませんのでFirstNameとLastNameを持つ単純なデータクラスとしました。

data class Person(val firstName : String, val lastName : String)

AbstractWriter

PersonAbstractWriterという抽象クラスのwriteメソッドにアルゴリズムを定義します。
処理1はOpen、処理2はprocess、処理3はcloseというメソッドで処理をします。
この3つのメソッドはabstractとなっていますのでサブクラスで具体的な処理を定義します。

このようにTemplate Methodパターンではまず、
ある処理の大まかなアルゴリズムを定義した抽象クラスを作成します。

abstract class AbstractWriter() {
    abstract fun open()
    abstract fun process(data : Person)
    abstract fun close()

    fun write(data : List<Person>)
    {
        // 処理1. ファイルをオープンする
        open()

       // 処理2. ファイルにデータを書き込む
        data.forEach {
            process(it)
        }

        // 処理3. ファイルをクローズする
        close()
    }
}

ConcreteWriter

次に抽象クラスのAbstractWriterを継承したサブクラスを定義し、
abstractで宣言していたメソッドをOverrideし具体的にどのように出力するか処理を実装していきます。
今回はPersonをCSV形式またはテキスト形式で書き込めるように処理を実装していきたいと思います。

CsvWriter

CSV形式での出力では次のようにヘッダーを出力したいです。
なのでファイルをオープンするときにヘッダーを書き込んでおきます。
あとはCSV形式ですのでカンマ区切りで各プロパティを書き込むようにします。

lastName,firstName
姫路,太郎
榊,真喜子
喜々津,優輔
class CsvWriter(name : String) : AbstractWriter()
{
    private val name : String = name
    private val header : String = "lastName,firstName\n"
    private var file : FileWriter? = null

    override fun open() {
        file = FileWriter(name)
        file?.write(header)
    }

    override fun process(data: Person) {
        val csv = "${data.lastName},${data.firstName}\n"
        file?.write(csv)
    }

    override fun close() {
        file?.close()
    }
}

TextWriter

テキスト形式ですのでPersonをそのまま書き込むだけなのですが、
次のように名簿のようにしたいと思いましたのであわせて番号も書き込むことにしました。

1 姫路 太郎
2 榊 真喜子
3 喜々津 優輔
class TextWriter(name : String) : AbstractWriter()
{
    private val name : String = name
    private var count : Int = 0
    private var file : FileWriter? = null

    override fun open() {
        file = FileWriter(name)
    }

    override fun process(data: Person) {
        count++

        var text = "${this.count} ${data.lastName} ${data.firstName}\n"
        file?.write(text)
    }

    override fun close() {
        file?.close()
        count = 0
    }
}

Main

サブクラスの定義が終わりましたのでMainから呼び出します。

fun main(args : Array<String>) {

    val persons = listOf<Person>(Person("太郎", "姫路"), Person("真喜子", "榊"), Person("優輔", "喜々津"))
    val csvWriter = CsvWriter( "persons.csv")
    val textWriter = TextWriter("persons.txt")

    csvWriter.write(persons)
    textWriter.write(persons)
}

Mainを実行しますと次のようなファイルが次の内容で出力されます。

lastName,firstName
姫路,太郎
榊,真喜子
喜々津,優輔
1 姫路 太郎
2 榊 真喜子
3 喜々津 優輔

おわりに

結論としてTemplate Methodパターンでやりたいことは処理の共通化です。

  1. インタフェースの共通化
  2. 処理順序の共通化

特に2の処理順序の共通化がTemplate Methodパターンの利点だと思います。
今回の例ですとオープンはいつクローズはいつと順序が決められているため、
サブクラスごとにオープンやクローズのタイミングが違うとかは絶対に起きなくなります。

このようにプログラマによって指針が違うような事柄が含まれる場合は、
Template Methodパターンを使って処理を共通化しておくとサブクラスがより管理しやすくなると思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む