mike-neckのブログ

Java or Groovy or Swift or Golang

クリーンアーキテクチャーの読書メモ(7)

§8 OCP: オープン・クローズドの原則

  • 1988年 Bertrand Meyer
    • ソフトウェアの構成要素は拡張に対しては開いていて、修正に対しては閉じていなければならない(『アジャイルソフトウェア開発の奥義』)
    • = ソフトウェアの振る舞いは既存の成果物を変更せずに拡張できるようにすべき
  • ソフトウェアよりコンポーネントレベルで重大なインパクトを持つ
    • よくある Controller/Service/Model/View/Infraコンポーネント
    • 矢印のしっぽ側のコンポーネントが矢印の向かってる先のコンポーネントを参照(import)している
    • View(ThymeleafView)を変更した際に、 Controller を変更しなくて良い
    • 他のすべてを変更しても Model(BusinessModel)を変更しなくて良い
    • Model が上位のレベルのコンポーネント
    • レベル概念に基づいた変更からの保護階層ができていることが OCP
    • 依存関係逆転の法則を使って、矢印の方向を制御する

f:id:mike_neck:20190320020600p:plain
矢印がすべて向かってくる BusinessModel コンポーネントが最上位レベルのコンポーネント

Hello r2dbc with Kotlin

リアクティブな感じで RDB に接続できるやつ。内容的にはバファさんのブログの下位互換未満。

bufferings.hatenablog.com


準備1

使ったデータベースは postgres で、docker で用意した。とりあえず、こんな感じのテーブルを作っておく。

create table users(
  id bigint not null primary key ,
  name varchar(31) not null unique ,
  created timestamp not null 
);

ついでにデータを入れておく

insert into users (id, name, created)
values (1, 'test-user', '2019-01-01 10:00:00'),
       (2, 'test-admin', '2019-01-15 10:00:00'),
       (3, 'test-owner', '2019-02-01 10:00:00'),
       (4, 'test-guest', '2019-02-15 10:00:00');

準備2

gradle は次のような感じ(Kotlin)

import java.net.URI

plugins {
    id("org.jetbrains.kotlin.jvm").version("1.3.20")
}

repositories {
    jcenter()
    mavenCentral()
    maven {
        url = URI.create("https://repo.spring.io/libs-milestone/")
    }
}
dependencies {
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.1.1")

    implementation("io.projectreactor:reactor-core:3.2.6.RELEASE")
    implementation(group = "io.r2dbc", name = "r2dbc-postgresql", version = "1.0.0.M7")

    testImplementation("org.junit.jupiter:junit-jupiter:5.4.0")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit5:1.3.21")
    testImplementation("io.projectreactor:reactor-test:3.2.6.RELEASE")
}

ちょっと動かす

テストデータは入っているので、クエリーを投げるところです

@Test
fun noKoroutine() {
  val options = ConnectionFactoryOptions.builder()
      .option(ConnectionFactoryOptions.DRIVER, "postgresql")
      .option(ConnectionFactoryOptions.HOST, "localhost")
      .option(ConnectionFactoryOptions.PORT, 5432)
      .option(ConnectionFactoryOptions.USER, "postgres-user")
      .option(ConnectionFactoryOptions.PASSWORD, "postgres-pass")
      .option(ConnectionFactoryOptions.DATABASE, "postgres")
      .build()
  val connectionFactory: ConnectionFactory = ConnectionFactories.get(options)
  val conn = Mono.from(connectionFactory.create())
  val users = conn.flatMapMany { connection ->
    connection.createStatement(
        //language=SQL
        "SELECT u.id, u.name, u.created FROM users AS u WHERE u.name = $1")
        .bind("$1", "test-user")
        .execute() }
      .flatMap { result ->
        result.map { row, meta ->
          val name = row.get("name", String::class.javaObjectType)
          val id = row.get("id", Long::class.javaObjectType)
          val created = row.get("created", Instant::class.java)
          return@map if (created != null && name != null && id != null) User(id, name, created)
          else null
        }
      }
      .filter { it != null }
      .cast(User::class.java)
  runBlocking {
    users.buffer().consumeEach {
      println(it)
    }
  }
}

実行結果

f:id:mike_neck:20190317201815p:plain


番外編

Kotlin-Coroutine を使うといい感じのコードになるのかと思ったけど、

// GlobalScope.flux 内
val result = connection.createStatement(
        //language=SQL
        "SELECT u.id, u.name, u.created FROM users AS u WHERE u.name = $1")
        .bind("$1", "test-user")
        .execute()
        .awaitSingle()

result.map { row, _ ->
    val name = row.get("name", String::class.javaObjectType)
    val id = row.get("id", Long::class.javaObjectType)
    val created = row.get("created", Instant::class.java)
    return@map if (created != null && name != null && id != null) User(id, name, created)
    else null
}.consumeEach { if (it != null) channel.offer(it) }

f:id:mike_neck:20190317212450p:plain

AbstractByteBuff#getCharaSequence のあたりで、 ByteBuffreadableBytes0 になってしまう現象に遭遇したので、諦めました

Kotlin Coroutine はもう少し業務寄りの部分で多様なデータが踊っているような箇所で使って、こういう複雑にデータをさわらない箇所ではあまり効果がなさそうに思いました(要検討)。

pixela-java-client の CI(circle-ci) を1日1回まわすための lambda 作った

以前書いたとおり、CI でライブラリーのアップデートをslack に送るようにしてみました。

mike-neck.hatenadiary.com

しかし、特に開発することがなくなったりすると依存ライブラリーのアップデートにも気づけなくなるので、1日1回 CI (circle-ci) をまわすために、lambda 関数を作ってみた。

f:id:mike_neck:20190318001112p:plain
起動イメージ

コードはこんな感じ

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/pkg/errors"
    "io/ioutil"
    "log"
    "net/http"
    "os"
)

func main() {
    debug := os.Getenv("DEBUG_APP")
    if debug != "" {
        log.Println("running debug mode")
        err := callCircleCi()
        if err != nil {
            log.Fatalln("exit by", err)
        }
    } else {
        log.Println("running application mode")
        lambda.Start(callCircleCi)
    }
}

func callCircleCi() error {
    token := os.Getenv("CIRCLE_CI_TOKEN")
    if token == "" {
        log.Println("error no token(CIRCLE_CI_TOKEN) available")
        return errors.New("no token(CIRCLE_CI_TOKEN) available")
    }
    buildUrl := os.Getenv("CIRCLE_CI_URL")
    if buildUrl == "" {
        log.Println("error no url(CIRCLE_CI_URL) available")
        return errors.New("error no url(CIRCLE_CI_URL) available")
    }
    circleCiUrl := fmt.Sprintf("%s?circle-token=%s", buildUrl, token)

    buildRequest := CircleCiBuildRequest{"master"}
    jsonBytes, err := json.Marshal(&buildRequest)
    if err != nil {
        log.Printf("error marshal json %v, detail: %v\n", buildRequest, err)
        return errors.Wrapf(err, "marshal object %s", buildRequest)
    }

    body := bytes.NewReader(jsonBytes)
    request, err := http.NewRequest(http.MethodPost, circleCiUrl, body)
    if err != nil {
        log.Printf("error creating new request, detail: %v\n", err)
        return errors.Wrap(err, "error creating new request")
    }
    request.Header.Add("content-type", "application/json")
    request.Header.Add("accept", "application/json")

    client := http.DefaultClient
    response, err := client.Do(request)
    if err != nil {
        log.Printf("error request to circle-ci, detail: %v\n", err)
        return errors.Wrap(err, "error request to circle-ci")
    }

    respBody, err := ioutil.ReadAll(response.Body)
    if err != nil {
        log.Printf("error read body, status: %d", response.StatusCode)
    }

    if response.StatusCode != 200 {
        responseBody := string(respBody)
        log.Printf("error response %d, %s, body: %s", response.StatusCode, response.Status, responseBody)
        return errors.New(fmt.Sprintf("error response %d, body; %s", response.StatusCode, responseBody))
    }

    var circleCiResponse CircleCiResponse
    err = json.Unmarshal(respBody, &circleCiResponse)
    if err != nil {
        responseBody := string(respBody)
        log.Printf("error unmarshal json: %s, detail: %v", responseBody, err)
        return errors.Wrapf(err, "error unmarshal json: %s", responseBody)
    }

    log.Printf("success: %v", circleCiResponse)
    return nil
}

type CircleCiBuildRequest struct {
    Branch string `json:"branch"`
}

type CircleCiResponse struct {
    Status int    `json:"status"`
    Body   string `json:"body"`
}

で、 sam テンプレートはこんな感じ

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: "circle-ci-caller function"

Resources:
  CircleCiCallerFunction:
    Type: AWS::Serverless::Function
    Properties:
      Description: "Call Circle CI Build"
      CodeUri: build/linux/
      Handler: app
      Runtime: go1.x
      Tracing: Active
      Events:
        CircleCiCallerFunction:
          Type: Schedule
          Properties:
            Schedule: "rate(1 day)"

Outputs:
  CreatedFunction:
    Description: "Created function"
    Value: !GetAtt CircleCiCallerFunction.Arn

結果

アップデートせえよって怒られが発生した

f:id:mike_neck:20190318001648p:plain

結果2

pixela に色が毎日つくかも…(図は3/17のショートモード)

https://pixe.la/v1/users/pixela-java-client/graphs/run-ci?mode=short&date=20190317

クリーンアーキテクチャーの読書メモ(6)

第三部 設計の原則

  • クリーンなコードを書くための原則「SOLID 原則」
    • 変更に強い中間レベルのソフトウェア構造
    • 理解しやすい中間レベルのソフトウェア構造
    • コンポーネントの基盤として、多くのソフトウェアシステムで利用できる中間レベルのソフトウェア構造
  • 単一責任の原則(Single Responsibility Principle)
  • オープン・クローズドの原則(Open Closed Principle)
  • リスコフの置換原則(Liskov Substitution Principle)
  • インターフェース分離の原則(Interface Segregation Principle)
  • 依存関係逆転の原則(Dependency Inversion Principle)

§7 SRP: 単一責任の原則

  • よくある間違い
    • モジュールはたった一つのことだけを行うべき
  • 正しい
    • モジュールを変更する理由はたった一つだけである
    • (言い換え)モジュールはたった一人のユーザーやステークホルダーに対して責務を追うべきである
    • (言い換え)モジュールはたったひとつのアクター(ユーザーなど変更を望む人の集団)に対して責務を追うべきである
  • 例 - 単一責任の原則に違反している例と解決策の例
    • (図1)単一責任の原則に違反しているクラス Employee (3つのメソッドがそれぞれ別々のアクターに対する責務を負っている)
    • 解決策1 : 僕は好きではない
    • 解決策2
      • メソッドオブジェクトを作って、処理を移譲する(図3)

f:id:mike_neck:20190317233338p:plain
(図1)SRP に違反

f:id:mike_neck:20190317235302p:plain
図2

f:id:mike_neck:20190317235409p:plain
図3