気軽に楽しくプログラムと遊ぶ

自分が興味があってためになるかもって思う情報を提供しています。

現場システム開発の進め方を考えてみる

システム開発を効率的に進めるための優先順位や力を抜く部分、効率化についての整理をしてみます。

前提

  • 開発機能:なんとなくこんな機能というイメージはあるが、コードは読んだことがない
  • 言語:Javaでの開発は慣れているが、新たに使うKotlinについては未経験
  • チーム:リードエンジニア、メンバーが先行して開発を進めているところに途中から参画
  • 協業チーム:フロント、アプリは別部隊があり、そちらがテストできるレベルの資材を提供することがひとまずの目的
  • 開発内容:品質の良くないソースの作り換え、Java → Kotlin移行
  • 工期:短納期ではないが、余裕があるスケジュールではない。

仕様理解、開発方針の把握、実装のバランスを取りつつ、最高パフォーマンスを発揮するにはどう行動すべきか。

目的

  • 協業チームがテストできる最低限の品質の実装を組み上げる
  • チームのコード規約や求める品質をクリアする
  • テストコードはカバレッジ80%を目指す
  • Kotlin知見を蓄積し、今後の開発効率化に繋げる。チーム貢献できる情報集約ができたらなおよし

タスク概要

スケジュール

  • 協業チームの開発状況の把握。納期必達かの確認

設計

  • IFの再確認:既にIF設計はされているが、矛盾点ある可能性あり。
  • 実装設計:処理内の細かな修正方法は未検討のため、検討する必要あり。 一部、DB→Redisへ変更する必要あり。
    • 周辺機能理解:機能全体を踏まえた適切な実装が行う。

実装

  • 機能
    • APIの枠作成
    • コードの自動変換:ざっくり変換して変換前後理解
    • コメントで処理フローをざっくり記載
    • 処理実装ステップ:Java → Kotlin変換したソースの段階的、移植
  • Kotlin学習
    • リードエンジニアソースのリーディング:チームコード規約理解、Kotlinの書き方基本理解

効率化のポイント

  • 作業計画
    • 概要タスク整理と優先順位、概算見積もり。一日にこなす作業量のイメージをしておく。
  • 実装方法の事前理解
    • よく使う実装方法を体系的に整理しておく。辞書を作っておく
    • テストコードの抽象化、JavaSandプロジェクトへの転記
  • 共通機能の利用
    • 共通機能の理解、利用の想定。機能全体の概要を掴んでおくことが肝要。
  • レビュー指摘を減らす
    • 実装済みソース読み込みでコード規約の理解、WIPでの細切れPR

作業の進め方

ここまでを踏まえて、どう作業を進めるのか。

  • 既存コード理解(作成機能)
  • 既存コード理解(周辺機能、共通機能)
  • 利用する共通機能把握、Kotlinイデオム整理
  • 疑問点整理、重要ポイント確認
  • 設計書理解と実装設計(実装ステップ)
  • 設計疑問点整理と確認
  • API枠、処理概要コメント実装
  • ステップ実装

以上

Kotlin 基礎学習

Kotlin とは?

  • 型推論のある静的型付けのOBJ指向言語
  • Javaとの連携が言語仕様に存在。KotlinからJavaのクラスやメソッドを呼べたりもする

オンライン実行環境

以下の本家サイトの実行環境で、実際に試しながら学習を進めると捗ります。

Kotlin Playground: Edit, Run, Share Kotlin Code Online

Javaとの違い

  • 文末のセミコロンが不要
    • 参照型しかない 。プリミティブ型はない
    • val:読み出しのみ、var:読み書き可
  • 定数:const。const val MAX_NUM = 10

null関係

nullチェックなしコードはコンパイル時にエラー

// non-null type
var a: String = "moji"
a = null // コンパイルエラー

// null type
var a: String? = "moji"
a = null // エラーにならない

nullじゃないとき実行する

var str: String? = null // null typeにnull挿入
str?.split(",") // strがnullじゃないときだけsplit

nullじゃないとき値設定(エルビス演算子)

Company.setName(name?: "ななしの会社")

スコープ関数

// hogeがnullでないときのみ{}の処理を実行させる関数
hoge?.let { 
    textView.text = it.name
}

他にrun, also, applyなどが存在。おいおい理解していく。

演算子

値比較、参照比較

val value1 = "a"
val value2 = "b"
if (value1 == value2) // 値比較で false。否定は !=

var list1 = listOf(1,2,3)
var list2 = listOf(1,2,3)
if (list1 === list2) // true =が3つ。javaは== だったので間違えないように

文字列

文字列中に埋め込み文字

var shopName: String = "sevene"
println("${shopName}")
println("$shopName") // 型推論可能な場合は{}は省略可能

改行入り文字列

行頭の| は目印の記号。trimMarginで除去可能

fun main(args: Array<String>) {
    var strStory = """
           |本日は
           |晴天なり
           |明日もそうなる
           """.trimMargin() 
 
    println("$strStory")
}

キャスト

val hoge = obj as Hoge // objをHoge型に変換する

関数 function

基本の書き方

fun strConbine(a: String, b: String): String {
    return a + b
}

fun strConbine(a: String, b: String): = return a + b
    
fun strConbine(a: String, b: String): Unite {  // Unite はJavaのvoid。省略可
    println(a + b)
}

名前付き引数

引数が多いかつ、同じ型の場合、どの項目に設定しているのか間違えやすい場合がある
その場合に値設定時にあえて関数側の引数名をあえて指定して関数を呼び出す書き方です。
JavaではBuilderパターンを使って回避したりするが、いかんせんコード量が増えがち。

class Book(
    val title: String,
    val price: Int,
    val author: String,
    val isbn: String,
    val remarks: String
) {
    override fun toString(): String {
        return "Book(title='$title', price=$price, author='$author', isbn='$isbn', remarks='$remarks')"
    }
}

呼び出し側のコードは以下です。

val book = Book(
    title = "Kotlinイン・アクション",
    price = 4115,
    author = "Dmitry Jemerov",
    isbn = "978-4839961749",
    remarks = "サイズ: 23.6x18.2x3.6 cm")

println(book)

関数オブジェクト

fun main() {
    val function = ::func
    println( function() )

    val function2: () -> String = ::func // 型を明示する書き方
    println( function2() )
}

fun func(): String {
    return "test"
}

ラムダ式

fun main() {
    // ラムダ式、記述省略なし。=の前が型(ラムダ)、後が関数。関数の ->の前が引数、後が処理
    val square: (Int) -> Int = { i: Int -> i * i }  
    val square2: (Int) -> Int = { i -> i * i }      // 関数内の引数を型推論
    val square3 = { i:Int -> i * i }                // 関数の型を型推論
    val square4: (Int) -> Int = { it * it }         // 引数が1つの場合はitの名で変数が使える。引数部分も省略
}

制御文

for

// インデックス
for (i in array.indices)
    print(array[i])

// インデックスと値
for ((index, value) in array.withIndex()) {
    println("the element at $index is $value")
}

// null考慮
for (i in list.orEmpty())  {
}

while

while (x > 0) {

}

配列、コレクション

配列

val list = arrayOf(1, 2, 3)               // 型類推
val list = intArrayOf(1, 2, 3)            // int指定

println(list[0])      // 要素アクセス

for(item in list){   // ループ
    println(item)
}

コレクション

//. リスト(要素は重複あり)
val list = listOf(1, 2, 3) // 変更不可
val list = mutableListOf(1, 2, 3) // 変更可
val list = mutableListOf<TMP>() // 空で初期化

// Set(重複なし)
val list = setOf(1, 2, 2, 3)  // 変更不可、変更時はmutableSetOf

// Map
val map = mapOf(    // 変更不可、変更時はmutableMapOf
    1 to "one",
    2 to "two",
    3 to "three"
)

for(item in map)
    println("${item.key} to ${item.value}")

for((key, value) in map)
    println("${key} to ${value}")

クラス

データクラス

データのみを保持するクラスは、classの前にdataを付与して宣言

// 定義
data class User(val name: String = "", val age: Int = 0)

// 初期化
val jack = User(name = "Jack", age = 1)  // 名前付き引数
val olderJack = jack.copy(age = 2)       // OBJのコピー&一部書き換え
val jane = User("Jane", 35)  // 通常初期化

// 参照
val (name, age) = jane       // 要素複数Get、nameやageを後続で参照可能

// 定義&コンストラクタ
data class User(val name: String = "", val age: Int = 0) {
        constructor(name: String, age: Int, email: String) : this(name, age)
}

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

fooというstaticメソッドを定義しているようなもの??よくわからない。

class MyClass {
    companion object { }
}

fun MyClass.Companion.foo() {
    println("hoge")
}

fun main(args: Array<String>) {
    MyClass.foo()
}

通常クラス

class Hoge {
    // プロパティの定義。getterとsetterが定義される。valならgetterのみ
    var name: String = ""

    // コンストラクタ
    constructor(name: String) {
        this.name = name
    }

    // メソッドの定義
    fun printMethod(strPrint: String) {
        println(strPrint)
    }
}

プライマリコンストラク

// クラスの引数に直接値を渡して初期化するタイプのコンストラクタ
class Hoge(n: String) {
    val name = n
}

// クラスの引数に直接値を渡して初期化するタイプのコンストラクタ、プロパティーも同時に定義
class Hoge(val name: String) {
}

継承

// openをつけると継承可能となる
open class Base {
}

class Hoge : Base() {
}

インターフェース

interface Hoge {
    fun hoge()
}

// 継承と同じ書き方になる
class Sample: Hoge {
    override fun hoge() { // 実装クラスはoverrideを付与
        println("hogehoge")
    }
}

// 無名クラスでインターフェースを実装
class Sample {
    private val h = object: Hoge { // 無名クラス(object)にHogeを実装(具象化)する
        override fun hoge() {
            println("hogehoge")
        }
    }
}

Github Actions 補助機能メモ

Github Actionsで使える補助的な機能についてメモしておきます。

補助的な機能

  • CIをスキップする
    • コミットメッセージに[skip ci]を含める。どこに書いてもいい。
  • ドラフトPR
    • 修正途中のPRをWIPとして作成可能。マージできないPRとなる。

Jenkins Pipelineとは

Jenkins pipelineを使ったデプロイジョブが動作不良になった経緯があり、基本概念を調べてまとめておきます。

Pipelineとは

Jenkins2.0においてPipeline Pluginが標準装備になりました。

Pipelineは、デプロイのパイプライン(ステージ毎の工程)を可視化するツールで、Groovyスクリプトで記述できます。

f:id:tamata78:20210604145521p:plain

公式ドキュメント

https://jenkins.io/doc/pipeline/

Groovyスクリプト内の概念

node → stage → step

node('master') {
  stage('手順1') {
    sh 'step1'
    sh 'step2'
    sh 'step3'
  }
}

nodeが実行マシンのイメージ。
ステージ毎に各ステップを実行していくという形で記述する。

MySQL 実行計画(EXPLAIN)

MySQLにおける実行計画の見方について記載します。

EXPLAIN 結果確認

クエリコストをみたい場合

format=jsonをつけてexplainを実行する。

explain format=json 【クエリ】

結果Json

query_costの箇所にコストが表示される

{
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "3599.86"
    },
    "nested_loop": [
      {
        "table": {
          "table_name": "s",
          "access_type": "ALL",
          "possible_keys": [
            "PRIMARY"
          ],
          "rows_examined_per_scan": 11309,
          "rows_produced_per_join": 113,
          "filtered": "1.00",
          "cost_info": {
            "read_cost": "2400.18",
            "eval_cost": "22.62",
            "prefix_cost": "2422.80",
            "data_read_per_join": "100K"
          },
          "used_columns": [
            "shop_id",
            "shop_nm",
            "menu_cd"
          ],
          ・
          ・
          ・

通常EXPLAINを実行した場合

以下のような形式で結果が表示

f:id:tamata78:20210521172119p:plain

結果項目

各項目の詳細は以下です。

項目名 結果候補 説明
id - 実行順番。同数字の場合、複数クエリが1クエリで実行
select_type PRIMARY 外部クエリ
DEPENDENT SUBQUERY 相関関係のあるサブクエリ
table - 対象テーブル名
partition - どのパーティションテーブルか
type const pk or uniqueインデックスを使用。最も速い
eq_ref -joinにおいてのconstと同義
ref constでないインデックスを使って等価検索(where k = v)を行った時に使用されるアクセス
range indexを用いた範囲検索
index フルインデックススキャン、インデックス全体をスキャンしているので遅い
ALL フルテーブルスキャン、インデックス未使用。一番遅い
possible_keys - optimizerがテーブルのアクセスに利用可能だと判断したインデックス
key - 実際にoptimizerによって使用されたキー
key_len - 選択されたキーの長さ。長さは短いほうが高速
ref - 検索条件でkeyと比較されている値やカラムの種類
const 定数値
結合相手の検索条件カラム JOIN時
rows - テーブルのfetch行数、見積もり
filtered - テーブル条件によってフィルタ処理される行の推定の割合
Extra - optimizerの戦略。項目多いので省略

JMeterの出力結果の見方

JMeterの出力結果の簡単な読み方について記載します。

Jmeterの出力結果の見方

$ apache-jmeter-5.3/bin/jmeter.sh  -n -t test.jmx
summary +   2297 in 00:00:24 =   95.4/s Avg:  1176 Min:     1 Max: 16483 Err:    15 (0.65%) Active: 113 Started: 120 Finished: 7
summary +   1303 in 00:00:06 =  210.9/s Avg:   429 Min:    22 Max:  5709 Err:     0 (0.00%) Active: 0 Started: 120 Finished: 120
summary =   3600 in 00:00:30 =  119.0/s Avg:   906 Min:     1 Max: 16483 Err:    15 (0.42%)
  • summary:リクエスト数 in 経過時間 = 秒間リクエスト数(リクエスト数/経過時間(s))
  • Avg:各リクエストの応答までの時間の平均
  • Err:エラー数と割合
  • Active:実行中リクエス
  • Started:開始リクエス
  • Finished:終了リクエス

上記では、1行目で120リクエストが開始されたが、7リクエストしか終わっていない。 エラーも発生しているため、分間(または秒間)にさばけるリクエスト数を減らす必要がある。

負荷テスト時のポイント

  • テスト実行は、3回やって外れ値少ないものを記録
  • さばける件数が少ない場合、秒間計測はスキップ

ssh時はローカルのlocaleを接続先に引き継いでしまう

タイトルの通りだけど、ssh時はローカルで設定したlocaleを引き継いでしまい、 接続先はen_USだったはずなのに、ja_JPになるということがある。

ssh -v でデバッグすると、接続先にロケール情報を引き継いでいることを確認できる。

[接続元]$ ssh -vi ***.pem ec2-user@***.***.***.***
OpenSSH_6.2p2, OSSLShim 0.9.8r 8 Dec 2011
debug1: Reading configuration data /etc/ssh_config
debug1: /etc/ssh_config line 20: Applying options for *
debug1: Connecting to ***.***.***.*** [***.***.***.***] port 22.
debug1: Connection established.
debug1: identity file test-cm.pem type -1
debug1: identity file test-cm.pem-cert type -1
・
・
・
debug1: Sending environment.
debug1: Sending env LANG = ja_JP.UTF-8

SSH接続時のLANG設定がOS設定値と異なる時の対処方法 | DevelopersIO

解決方法

/etc/ssh_configのEnvをssh時に送信する設定をコメントアウトするとlocale設定を引き継がなくなる。

 Host *
 #   SendEnv LANG LC_*