きり丸の技術日記

技術検証したり、資格等をここに残していきます。

JavaScript(TypeScript)のMapでgerOrElseと同等の動きを簡潔に書く

JavaだとMapのvalueを取得する際に、取得できなかった場合のデフォルト値を返すgetOrElseメソッドが用意されていますが、JavaScriptにはありません。今回の記事では、その同等の挙動を簡潔に書けるようにします。

環境

対応

Null合体演算子(??)を組み合わせれば、getOrElseと同等の挙動させられます。

// Mapの定義
const actionMap: Map<String, number> = new Map<String, number>([
  ["a", 1],
  ["b", 2],
  ["c", 3]
]);
// 取得できなければ0を返却する
const result: number = actionMap.get("a") ?? 0;

getOrThrow

getOrElseはデフォルト値を返すためのメソッドです。似たメソッドに、データが取得できなかった場合にエラーを返すgetOrThrowがあります。

同様にgetOrThrowも実行できないか確認していましたが、次のエラーメッセージが出力されて、簡潔に実装することは難しそうです。

Support for the experimental syntax 'throwExpressions' isn't currently enabled

素直にifで処理分岐するしかなさそうです。

function getValue3(actionMap: Map<String, number>, key: String): number {
  const result: number | undefined = actionMap.get(key);
  if (result == null) {
    throw new Error("Error");
  }
  return result;
}

ソースコード

終わりに

TypeScriptだと型定義が必要ですので、分かる範囲では1行にまとめてしまいたいですね。

型定義は非必須ですが、警告が出ているままコーディングしていると本当に大事な警告を見逃してしまいますので、できる範囲ではキッチリ作業していきたいです。

参考情報

Javaでストラテジーパターンを素振りする

ストラテジーパターンというGoFのデザインパターンが良いコード悪いコードでも紹介されていたので、素振りします。

自分の言葉で上手な言語化ができていないので、メリットは参考情報や良いコード悪いコードの購入、または別の方の記事を参考にしてください。

環境

  • Java
    • 17

今回の処理

複数の言語の文章を、ひとつの言語に変換する。

※ 変換の具体的なロジックは実装しない。

対応

文章と言語名が記載されたDTOを生成する。

@Data
public class Question {
  private String sentence;
  private Locale locale;
}

ストラテジーパターンを使用しやすいように、FCCを使用してQuestionのリストを用意し、LocaleをKeyとしたMap型に変換する。

@Data
@Builder
public class QuestionList {
  List<Question> value;
  public Map<Locale, List<Question>> groupByLocale() {
    return value.stream()
        .collect(Collectors.groupingBy(
            Question::getLocale
        ));
  }
}

翻訳用のinterfaceを用意する。

public interface Translator {
  List<Question> translate(List<Question> question);
}

interfaceを継承した翻訳クラスを用意する。

public class FromEnToJa implements Translator {

  @Override
  public List<Question> translate(List<Question> question) {
    return question.stream()
        .map(e -> e.toBuilder().locale(Locale.JAPANESE).build()
        ).collect(Collectors.toList());
  }
}

public class FromFrToJa implements Translator {

  @Override
  public List<Question> translate(List<Question> question) {
    return question.stream()
        .map(e -> e.toBuilder().locale(Locale.JAPANESE).build()
        ).collect(Collectors.toList());
  }
}

public class NoopTransrate implements Translator {

  @Override
  public List<Question> translate(List<Question> question) {
    return question;
  }
}

LocaleをKeyとし、実際に処理するクラスをvalueとしたMapを用意する。

private final Map<Locale, Translator> toJaActionMap =
    Map.of(
        Locale.ENGLISH, new FromEnToJa(),
        Locale.JAPANESE, new NoopTransrate(),
        Locale.FRENCH, new FromFrToJa()
    );

文章とLocaleを元に、すべてを日本語に変換する処理をする。

public QuestionList translate() {
  final List<Question> list = new ArrayList<>();
  // 処理を移譲しているので、中身が違う翻訳処理を行う。
  this.groupByLocale().forEach(
      (key, value) -> list.addAll(toJaActionMap.get(key).translate(value))
  );
  return new QuestionList(list);
}

ソースコード

終わりに

正直、今回の例示とした委譲処理はメリット薄いです。ただ、ストラテジーパターンを素振りしておくことで、見通しの悪い処理を切り出せるという意識を持っておきたいです。

地味ながら、JavaGoldで勉強したBiConsumerを使うタイミングが分かっていなかったので、今回のストラテジーパターンを素振りすることで使用できてよかったです。

// ここが内部的にはBiConsumerで処理されています。
this.groupByLocale().forEach(
  (key, value) -> list.addAll(toJaActionMap.get(key).translate(value))
);

参考情報

JavaのStaticメソッドをMockitoでモックする(ネストしたメソッドも値を返すようにする)

Javaのオープンチャットに「staticメソッドをMockitoでモックして値を返したい。NestするとNullが返るので、それを回避したい」という要望があったので、素振りしました。

環境

SpringBootStarterTestを基本としています。

  • Java
    • 17
  • org.springframework.boot:spring-boot-starter-test
    • 2.6.4
  • (org.mockito:mockit-core)
    • 4.6.0
  • org.mockito:mockito-inline
    • 4.6.1

下準備

Mockitoでstaticメソッドをモックにするには、mockito-inlineが必要なため、gradleの依存関係に含めます。SpringBootStarterTestにはmockito-coreが含まれていますので、バージョン違いで怪しい動きをする場合以外は不要です。

  • testImplementation 'org.mockito:mockito-inline:4.6.1'

対象コード

次のgetFirstメソッドの戻り値を変更することを目的とします。また、getFirstに依存しているgetSecond, getThirdの戻り値がnullではなく、依存しているgetFirstの戻り値であることが期待値です。

public class Target {
  public static String getFirst() {
    return "1";
  }
  public static String getSecond() {
    return getFirst();
  }
  public static String getThird() {
    return getSecond();
  }
}

対応

try-with-resources区でMockito.mockStaticを囲むと、そのブロック内の挙動が書き換わります。Mockito.mockStatic第1引数にモック対象のクラスを渡し、第2引数にどのようにモックにするかという挙動を指定します。

今回はNestしたメソッドの挙動も書き換えたいため、Mockito.CALLS_REAL_METHODSを渡します。Mockitoではwhenでメソッドの挙動を書き換えますが、デフォルトだとwhenメソッドで指定したメソッドのみしか書き換わりません。getFirstメソッドだけでなく、getSecondメソッドも書き換える必要があります。

Mockito.CALLS_REAL_METHODSパラメータを渡す場合は元のメソッドを呼ぶように書き換わります。この状態でgetFirstメソッドを書き換えることにより、getSecondメソッドがgetFirstメソッドを呼ぶようになるため、構造が深くなっても期待する値を返却してくれます。

// モック前
assertThat(Target.getFirst()).isEqualTo("1");
assertThat(Target.getSecond()).isEqualTo("1");
// モック中
try (var mocked = Mockito.mockStatic(Target.class, Mockito.CALLS_REAL_METHODS)) {
  // getFirstメソッドの戻り値が1から123に変更される
  mocked.when(() -> Target.getFirst()).thenReturn("123");

  assertThat(Target.getFirst()).isEqualTo("123");
  assertThat(Target.getSecond()).isEqualTo("123");
}
// モック後
assertThat(Target.getFirst()).isEqualTo("1");
assertThat(Target.getSecond()).isEqualTo("1");

第二引数を渡さないときの挙動

// モック前
assertThat(Target.getFirst()).isEqualTo("1");
assertThat(Target.getSecond()).isEqualTo("1");
// モック中
try (var mocked = Mockito.mockStatic(Target.class)) {
  // getFirstメソッドの戻り値が1から123に変更される
  mocked.when(() -> Target.getFirst()).thenReturn("123");

  assertThat(Target.getFirst()).isEqualTo("123");
  // whenでgetSecondを指定していないため、モック状態のnullが返却される
  assertThat(Target.getSecond()).isEqualTo(null); 
}
// モック後
assertThat(Target.getFirst()).isEqualTo("1");
assertThat(Target.getSecond()).isEqualTo("1");

ソースコード

終わりに

正直なところ、staticメソッドを書き換えるパターンというのは悪手であることが多いので、私はあまりやらないです。DIも実質的にはstaticメソッドみたいなところはあるのですが、差し替えやすいという点においてDIの方が使いやすいです。

Mockitoの第2引数にMockito.CALLS_REAL_METHODSを渡すことにより、パーシャルモックをしやすいというのは非常に自分の中での勉強になりました。パーシャルモックはあまり好まれないようなのですが、デメリットがイマイチ理解できていないため、個人的には今後のプロジェクトでは積極的に使用していきたいです。