にんにんにん

エンジニアな日々を書いていきます

BDD(ビヘイビア駆動開発)

BDD(ビヘイビア駆動開発)

新しいチームにジョインして、BDDが導入されていたのでメモとしてまとめる。 Androidでの実装方法やフレームワークは別途まとめる。

What is BDD?

  • TDD(テスト駆動開発)の拡張版。
  • TDDでは、  - どこから始めるか?  - 何をテストするか?  - 何をテストしないか? という課題がある。その課題解決のための手法。
  • システムの予想される動作を確認するためのテストに重点を置く。
  • BDDでは、ユーザーストーリーに重きを置き、エンジニアがユーザーの視点から実装すべき機能を考えることを促す。

Given-When-Then

  • BDDはGiven-When-Thenの構造で記述される。

Given: 振る舞いまたはアクションを受け取るシステムの状態

When: 発生すると最終結果を引き起こす振る舞いまたはアクション

Then: 所定の状態で所定の振る舞いによって引き起こされる結果

参考

https://circleci.com/ja/blog/how-to-test-software-part-ii-tdd-and-bdd/ https://shiftasia.com/ja/column/%E3%82%A2%E3%82%B8%E3%83%A3%E3%82%A4%E3%83%AB%E9%96%8B%E7%99%BA%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8Btdd%E3%81%A8bdd/

In App Messagingの遷移先をWebViewで開く

In App Messaging

Firebase In-App Messaging を使ってみる  |  Firebase Documentation

Firebase In App Messagingを使用すると、簡単にプロモーション用のポップアップダイアログやバナーをアプリに表示することができます。 コンソールから表示したい画像だったり、ボタンの文言だったりを設定することができ、 タップしたあとの遷移先も指定することができます。

遷移先へのアクセス

Androidの場合、デフォルトで外部ブラウザ(たいていChrome)が起動します。 しかし、要件によってはWebViewで表示したい場合があると思います。

Firebase In-App Messaging のメッセージの動作を変更する  |  Firebase Documentation

FirebaseInAppMessagingClickListener を実装すれば、メッセージをクリックした際の動作を追加することはできますが、 あくまで追加なので、もともとの外部ブラウザを起動する挙動を変更することはできません。

そこで、直接遷移先URLを指定するのではなく、URLスキーム経由で起動するようにします。

hogeapp://web?url=https://hogehoge.com

urlパラメータなどを付与して、それをアプリ側で拾ってあげて、 Deeplinkをhandlingする過程でWebViewを起動するようにしてあげると、WebViewで指定URLを見ることができるかと思います。

JetpackCompose + Paging3でPaging実装

Paging3を使用。JetpackCompose用のオプションがあるので、build.gradleに追加する。

    // optional - Jetpack Compose integration
    implementation "androidx.paging:paging-compose:1.0.0-alpha15"

Paging3の実装に則り、PagingSourceを定義。 この辺はcodelabを見るとわかりやすい。

Android のページングの基本  |  Android Developers

class SamplePagingSource : PagingSource<Int, String>() {
    companion object {
        const val STARTING_KEY = 0
    }
    override fun getRefreshKey(state: PagingState<Int, String>): Int = 0

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, String> {
        val start = params.key ?: STARTING_KEY
        val range = start.until(start + params.loadSize)
        delay(3000L)
        return LoadResult.Page(
            data = range.map { number ->
                number.toString()
            },
            prevKey = when (start) {
                STARTING_KEY -> null
                else -> ensureValidKey(range.first - params.loadSize)
            },
            nextKey = range.last + 1
        )
    }
}

そして、Pagerを作成。 本来であればViewModel等でPagerを持つことになりそう。

    val pager = Pager(
        PagingConfig(10, enablePlaceholders = false),
        pagingSourceFactory = {
            SamplePagingSource()
        }
    )

そして、ここからがJetpackComposeの話になるが、collectAsLazyPagingItems()を使って、PagerをLazyPagingItemsに変換する。

val lazyPagingItems = pager.flow.collectAsLazyPagingItems()

developer.android.com

LazyColumnに追加する。

    LazyColumn(
        modifier = Modifier
            .fillMaxWidth()
            .nestedScroll(nestedSrollInterop)
    ){
        items(
            items = lazyPagingItems,
            key = { message -> message }
        ) { item ->
            Box(
                modifier = Modifier
                    .height(56.dp)
                    .fillMaxWidth()
                    .background(Color.Gray),
                contentAlignment = Alignment.Center
            ) {
                Text(item.toString())
            }
        }
     }
     item {
            Box(
                modifier = Modifier.fillMaxSize(),
                contentAlignment = Alignment.Center
            ) {
                CircularProgressIndicator()
            }
     }

必要に応じて、下部にCircularProgressIndicator()を追加してあげると良い。

動作させるとこんな感じ。

DataBindingを有効化する

今更な話ですが、、、

AndroidStudioにて、DataBindingを有効化するには、

アプリモジュールスコープのbuild.gradleファイルに、以下の内容を記述する

android {
    ....
    dataBinding {
        enabled = true
    }
}

まあ、補完が働いてくれるのでわかるかと

マルチスレッドを考慮したSingleton

例えば、以下のようなJavaコードがあったとします。

public class Singleton {
  private static Singleton singleton = null;

  private Singleton() {
    System.out.println("インスタンスを生成しました");
  }

  public static Singleton getInstance() {
    if (singleton == null) {
      singleton = new Singleton();
    }
    return singleton;
  }
}

このクラスはインスタンスが1個しか生成されなさそうですが、残念ながら複数生成されてしまう場合があります。
それは、マルチスレッド下でgetInstance()を呼んだときです。
つまり、複数のスレッドからほぼ同時にgetInstance()を呼んだとき、状態としてはsingletonはnullであるため、インスタンスは呼び出された分だけ生成されます。

わざとsleep処理をして同時に呼び出されるようにしてみると

public class Singleton {
  private static Singleton singleton = null;

  private Singleton() {
    System.out.println("インスタンスを生成しました");
    slowdawn();
  }

  public static synchronized Singleton getInstance() {
    if (singleton == null) {
      singleton = new Singleton();
    }
    return singleton;
  }

  private void slowdawn() {
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

そして、呼び出してみます。

public class Main extends Thread {
  public Main(String name) {
    super(name);
  }

  public static void main(String[] args) {
    System.out.println("Start.");
    new Main("A").start();
    new Main("B").start();
    new Main("C").start();
    System.out.println("End.");
  }

  @Override
  public void run() {
    Singleton obj = Singleton.getInstance();
    System.out.println(getName() + ": obj = " + obj);
  }
}

実行結果はこうなります。

Start.
End.
インスタンスを生成しました
インスタンスを生成しました
インスタンスを生成しました
B: obj = Singleton@12d18a03
A: obj = Singleton@68fb341a
C: obj = Singleton@68fb341a

Process finished with exit code 0

AとCが同じインスタンスなのに対して、Bは違うインスタンスであることがわかります。

そこで、getInstance()にsyncronized をつけてあげることで、ほかのスレッドからブロックします。 こうしてあげることで、スレッドセーフになり、Singletonが成り立ちます。

public class Singleton {
  private static Singleton singleton = null;

  private Singleton() {
    System.out.println("インスタンスを生成しました");
  }

  public static syncronized Singleton getInstance() {
    if (singleton == null) {
      singleton = new Singleton();
    }
    return singleton;
  }
}

実行すると、

Start.
End.
インスタンスを生成しました
A: obj = Singleton@68fb341a
C: obj = Singleton@68fb341a
B: obj = Singleton@68fb341a

Process finished with exit code 0

すべて同じインスタンスとなりました。

アノテーション

@Overrideとか、@Deprecatedとか、日ごろJavaを書いていると出てくるこいつら

こいつらを付けるとコンパイル時にエラーを検出できる、などふわっとしたくらいにしか思っていなかったが、
その理解でいるのも危険だなと感じたので、まとめてみることにしました。
(個人的に、アノテーションのことを解説している入門書ってあまりない気がしている)

こいつらのことをアノテーションと呼び、JDK1.5より導入された機能です。
アノテーションをクラスやメソッドなどに付与することで、その名の通り「注釈」することができます。

アノテーションの種類

アノテーションは、持つデータの数によって次の3種類に分類されます。

また、次のアノテーションもあります。

マーカー

例えば、@Overrideの場合、以下のinterfaceがあるとします。

public interface Clickable {
    void click();
}

これを無名クラスとして実装するとき、

public class Main {
    public static void main(String[] args) {
        Clickable clickable = new Clickable() {
            @Override
            public void click() {
                System.out.print("click");
            }
        };
        clickable.click();
    }
}

となるかと思いますが、この時click()がオーバーライドされていることを明示しているのが@Overrideです。 試しに、@Overrideを取り外してみると、

public class Main {
    public static void main(String[] args) {
        Clickable clickable = new Clickable() {
            public void click() {
                System.out.print("click");
            }
        };
        clickable.click();
    }
}

実行

click
Process finished with exit code 0

コンパイルエラーはなく、実行できてしまいました。

文法的に間違いではなく、コンパイルには問題がないことがわかります。
マーカーは、人間のためのアノテーションで、このclickメソッドがコードの読み手に「clickはClickableの抽象メソッドを実装したものですよ」
と伝えています。
もっとも、interfaceであれば、実装したメソッドであることは一目でわかるのですが、classを継承した場合、それが親クラスのメソッドなのか子クラスのメソッドであるのか一目ではわからないため、こうしてアノテーションをつけることで可読性が上がります。
ただ、継承はよほどのことがない限り使うものではないとは思っています。

ちなみに、その@Overrideの中身をのぞいてみると

package java.lang;

import java.lang.annotation.*;

/**
 * Indicates that a method declaration is intended to override a
 * method declaration in a supertype. If a method is annotated with
 * this annotation type compilers are required to generate an error
 * message unless at least one of the following conditions hold:
 *
 * <ul><li>
 * The method does override or implement a method declared in a
 * supertype.
 * </li><li>
 * The method has a signature that is override-equivalent to that of
 * any public method declared in {@linkplain Object}.
 * </li></ul>
 *
 * @author  Peter von der Ah&eacute;
 * @author  Joshua Bloch
 * @jls 9.6.1.4 @Override
 * @since 1.5
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

となっています。

アノテーションの定義は

<publicなどの修飾詞> @interface アノテーション名 { }

で、@Overrideの中身は空であることがわかります。

単一アノテーション

先ほどのClickableにdoubleClickという抽象メソッドを生やしたとします。

public interface Clickable {
    void click();
    void doubleClick();
}

これを実装して、clickメソッドしか呼び出さなかったとします。

public class Main {
    public static void main(String[] args) {
        Clickable clickable = new Clickable() {
            @Override
            public void click() {
                System.out.print("click");
            }

            @Override
            public void doubleClick() {
                System.out.print("doubleClick");
            }
        };
        clickable.click();
    }

}

この時、ClickableをIDE上で見てみると、

f:id:moroku0519:20180211123750p:plain

lintにdoubleClickが使われていないと怒られています。
ここで、doubleClickメソッドに@SupressWarning("unused")を付けてみます。

f:id:moroku0519:20180211124307p:plain

doubleClickメソッドの警告が消えました。(別の警告が出ているのはご愛敬...)

このように、本来ならばlintエラーが出てしまう個所に@SuppressWarningを付けることで、警告をなくすことができます。
ただし、警告をやみくもに消すのではなく、きちんとした理由があるときのみ使用します。

例えば、Androidで参照されていないメソッドがあるが、EventBusのSubscriberであり、警告されなくてもいいもの、など。

ちなみに、@SuppressWarningの実装は

package java.lang;

import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;

/**
 * Indicates that the named compiler warnings should be suppressed in the
 * annotated element (and in all program elements contained in the annotated
 * element).  Note that the set of warnings suppressed in a given element is
 * a superset of the warnings suppressed in all containing elements.  For
 * example, if you annotate a class to suppress one warning and annotate a
 * method to suppress another, both warnings will be suppressed in the method.
 *
 * <p>As a matter of style, programmers should always use this annotation
 * on the most deeply nested element where it is effective.  If you want to
 * suppress a warning in a particular method, you should annotate that
 * method rather than its class.
 *
 * @author Josh Bloch
 * @since 1.5
 * @jls 4.8 Raw Types
 * @jls 4.12.2 Variables of Reference Type
 * @jls 5.1.9 Unchecked Conversion
 * @jls 5.5.2 Checked Casts and Unchecked Casts
 * @jls 9.6.3.5 @SuppressWarnings
 */
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    /**
     * The set of warnings that are to be suppressed by the compiler in the
     * annotated element.  Duplicate names are permitted.  The second and
     * successive occurrences of a name are ignored.  The presence of
     * unrecognized warning names is <i>not</i> an error: Compilers must
     * ignore any warning names they do not recognize.  They are, however,
     * free to emit a warning if an annotation contains an unrecognized
     * warning name.
     *
     * <p> The string {@code "unchecked"} is used to suppress
     * unchecked warnings. Compiler vendors should document the
     * additional warning names they support in conjunction with this
     * annotation type. They are encouraged to cooperate to ensure
     * that the same names work across multiple compilers.
     * @return the set of warnings to be suppressed
     */
    String[] value();
}

となっていおり、

String[] value();

というデータをひとつだけ持っています。

本来アノテーション

@アノテーション名(キー名=値)

という形で指定しますが、単一アノテーションは一つだけしかキーを持たないため、キー名を省略できます。そのため、

@SuppressWarning("unused")

というように、書くことができました。

少々長くなってしまったので、フルアノテーションとメタアノテーションについては次回まとめます。

参照型のキャスト

Javaにおける、スーパークラスとサブクラスの関係性について勉強したので、メモ

class Pet {
    中身は省略
}

class RobotPet extends Pet{
    中身は省略
} 

があったとして、

Pet p = new RobotPet();

スーパークラス型の変数は、サブクラスのインスタンスを参照することができる

これは、暗黙で

Pet p = (Pet) new RobotPet()

と、スーパークラスにキャストされているからである

逆に、

RobotPet r = new Pet();

とするとき、暗黙のキャストは行われず、コンパイルエラーとなる

この場合は明示的なキャスト

RobotPet r = (RobotPet) new Pet();

が必要 この時のキャストのことをダウンキャストと呼ぶ

不用意にダウンキャストを行って、下位クラス型の変数に上位クラス型のインスタンスを参照させることは、原則として避けるべきとされている

なぜならば、rの参照先は、サブクラスRobotPetではなく、スーパークラスPetであるため、サブクラスのメソッドを実行しようとするとエラーが走る。しかも、コンパイルエラーではなく、実行時エラーとなるため、コンパイル時にエラーが検知できないためである

TextView textView = (TextView) findViewById(R.id.hoge);

textViewに対して、TextViewが持つメソッドを実行しようとしてエラーになる可能性があり、本来この書き方はよくないのだなとわかる