システム開発どこに時間をかけるか

こんにちは mtj です

ソフトを作る上でどこに時間(費用)をかけるかはなかなか悩むとこだと思います。
物理的な装置と繋いでデータを取るシステムを作る上で無しで動くようにするテスト機能、デバッグ機能という物はシステムの動作自体には必要ありません。
実際に装置と繋いで動けば問題ないためです。

しかしそれは装置が無い限り動作を行えないソフトが出来上がってしまいます。
想定されるデータ、通信等を行っての動作テストが行えません。

結果 動作には不要なとこに時間をかけなかったせいで結局時間がかかる事になったりします。

仕様書等も同様で開発している間は不要に思える事が多いかもしれません
しかしいざそのシステムを久しぶりに使う、改造する時になった場合何を作ったか思い出せない可能性があります
改造するためにコードを見て作っていた物を把握しながら作る事になり、結果時間がかかってしまいます。

開発だけでなく 画面のデザイン等も重要になります。
必要な箇所には時間をかけてデザインし、UXにかかわらないようなところはそれなりの時間をかけてデザインのような感じでどこに力をかけるか考えるのはかなり大事な事だと思います。

必要な費用、期間でいいソフトを作るのが自分たちの腕の見せ所だと感じます。

AutoHotKeyにより日報の入力を半自動化

今回は題の通りで、日報の入力作業をAutoHotKeyによって半自動化した話となります。

弊社の日報はWebで入力する形式です。
しかし、自分は前日の日報をコピーして編集・記入することが多いためWebで直接入力せず、
①テキストファイルで作成→②翌日は前日のファイルをコピー・編集し作成→③Webに転記
のように入力していたのですが、
この③のWebへの転記作業にて1項目ずつコピペする必要があり大変煩わしく思っていました。
※今年度からは日報に詳細は記載する必要はなくなったため簡素な入力でも問題ないのですが、
 翌日の作業の整理と翌週の朝礼で報告する内容の確認のために
 自分は以前と同じように入力しています。

煩わしく思いつつ特に何もしていなかったのですが、
転記作業自体は自動化すれば良いことに今更気が付きました。
そこでチャットAIにWebへの入力作業を自動化するのに適した手法を聞いてみたところ、
単純な作業なら作業自動化用スクリプト言語のAutoHotKeyが良さそうだと教えてもらい、
AutoHotKeyで転記処理を実装することにしました。

一旦チャットAIにコードを書いてもらったのですが上手く動作しなかった(※)ので、
それをひな型に自分で組み直しました。
※AutoHotKeyで何ができるのかわかっていなかったのもあり、そもそも指示が良くなかったです。
実装したコードは下記になります。

内容としては、
1. 起動時にクリップボードから入力データを読取
2. ホットキーで始動する処理内で、入力データをデータごと・項目ごとにキー送信
を実現しているコードとなります。
コードの前に参考までに、入力データ例(※)と入力画面を貼っておきます。
※入力データのフォーマットは今回の実装に合わせて少し調整しました。

組んでいる途中で思ったのですが、
単純な処理なのでC#でSendKeyを使用して実現するのでも良かった気がしますね。
ですがスクリプト言語は手軽ではあるので、一手段として触っておくのは意義がある気がします。
AutoHotKeyの文法自体は癖がなく書きやすいと感じました。
ただ代入が=ではなく:=なため、何度も癖でコロンを忘れてしまいエラーになりました。。。

以上、日報入力の半自動化(テキスト手動編集後のWebへの入力作業自動化)を行った話でした。
今後も自動化可能な作業に見つかった際は折を見て自動化するようにしたいと思います。

入力データ例

9999
0.5
[雑務]
1. 掃除・朝礼
---
12345678
7.5
[計測ソフト開発]
1. 仕様書作成
2. 実装中
---
_
_
[明日の予定]
1. 計測ソフト開発
2. サーバープログラムビルド送付
---
_
_
[出退]
08:55
18:15

入力画面

実装したコード:DailyReportAutomation.ahk

#Requires AutoHotkey v2.0
;開発使用バージョン:v2.0.19
;-------------------------------------------------------------------------------
;[グローバル変数]
; 標準出力へのグローバル参照:
;  ファイル名に*を指定してFileOpenすることで標準出力へ書き込み
;  実行時にmoreコマンドを指定して出力のリダイレクトを行う必要あり
;  (moreなしだとエラーになる)
;  次のように実行→ AutoHotkey64.exe DailyReportAutomation.ahk | more
global Stdout := FileOpen("*", "w")

;-------------------------------------------------------------------------------
;[起動時初期化処理:クリップボードを読み取り入力用レコードデータ作成]
if !ClipWait(1) {
    ; クリップボードが空ならエラー終了
    MsgBox("No text found in clipboard.", "Error", 16)
    ExitApp
}
buff := ClipboardAll() ; クリップボードを退避
clipboard := A_Clipboard ; クリップボードを取得
records := Parse(clipboard) ; クリップボードからレコードリストをパース
for item in records
    Log(item.ToString() . "`n")
A_Clipboard := buff ; 退避していたクリップボードを復元
if (records.Length = 0) {
    ; クリップボードが空ならエラー終了
    MsgBox("No records in clipboard.", "Error", 16)
    ExitApp
}
if (records.Length > 10) {
    ; クリップボードが10以上ならエラー終了
    MsgBox("Too much records in clipboard.", "Error", 16)
    ExitApp
}
; エラーなければ常駐開始

;-------------------------------------------------------------------------------
;[常駐時ホットキー]
;1. Windows + s:入力処理開始(開始前に先頭の入力欄にカーソルを合わせておくこと
#s:: {
    ; IMEが全角か半角かチェックし、全角(true)なら半角に切り替えます
    if (CheckImeIsFullOrHalf("Infortec 日報"))
        SwithImeToFullorHalf()
    ; 与えられたレコードデータの項目を順に入力
    for record in records {
        ; 工番入力→案件名入力→内容入力→コメント欄飛ばす→進捗欄飛ばす→時間入力
        ClearSend(record.ProjectNo)
        Next()
        ClearSend(record.ProjectName)
        Next()
        ClearSend(record.Content)
        Next()
        Next()
        Next()
        ClearSend(record.Time)
        Next()
    }
    ExitApp ; アプリ終了
}

;2. Windows + q:アプリ終了
#q:: {
    ExitApp
}

;-------------------------------------------------------------------------------
;[クラス]
;レコードクラス
class RecordData{
    ;コンストラクタ:工番、時間、案件名、内容
    __New(projectNo, time, projectName, content){
        this.ProjectNo := projectNo
        this.Time := time
        this.ProjectName := projectName
        this.Content := content
    }

    ;Func 文字列化
    ToString(){
        txts := [this.ProjectNo, this.Time, this.ProjectName, this.Content]
        return Join(txts, "`n")
    }
}

;-------------------------------------------------------------------------------
;[関数群]
;Func 指定したタイトルを持つウィンドウのIMEが全角か半角かチェックします
CheckImeIsFullOrHalf(windowTitle) {
    ; 日報ウィンドウのIMEの状態を取得
    SetTitleMatchMode 1 ; タイトル部分一致
    WM_IME_CONTROL := 0x0283
    IMC_GETOPENSTATUS := 0x0005
    imeWnd := DllCall("imm32.dll\ImmGetDefaultIMEWnd", "Uint", WinExist(windowTitle))
    imeStatus := DllCall("user32.dll\SendMessageA", "UInt", imeWnd, "UInt", WM_IME_CONTROL, "Int", IMC_GETOPENSTATUS, "Int", 0)
    Log(imeStatus ? "全角" : "半角")
    return imeStatus
}

;Func IMEの全角/半角を切り替えます。
SwithImeToFullorHalf() {
    Send("{vkF3sc029}") ; 半角/全角キー送信
}

;Func 入力欄をクリアして与えられた文字列を入力します
ClearSend(text) {
    Sleep 80
    Send("^a")
    Sleep 20
    Send("{Del}")
    Sleep 20
    SendText(text)
    Sleep 80
}

;Func 次の入力欄へ移動します
Next(){
    Send("{Tab}")
    Sleep 20
}

;Func テキストからレコードリストをパースします
Parse(text){
    ; 改行を正規化
    text := StrReplace(text, "`r`n", "`n")
    text := StrReplace(text, "`r", "`n")
    ; 項目ごとに文字列を分割
    items := StrSplit(text, "---")
    ; 項目が複数ないなら終了
    if (items.Length < 2)
        return []
    records := []
    for item in items {
        ; 項目からレコードを読み取る
        item := Trim(item, "`n")
        ; 行ごとに分割
        rows := StrSplit(item, "`n")
        ; 4行未満なら無効データとして飛ばす
        if (rows.Length < 4)
            Continue
        ; 行ごとの処理:主に複数行に跨る内容行の結合を行う
        contents := []
        for index, row in rows {
            ; プレースホルダ _ は空行に変換
            rows[index] := row := row = "_" ? "" : row
            ; 4項目目までは飛ばす
            if (index < 4){
                Continue
            }
            ; 4項目目以降を内容行配列に追加
            contents.Push(row)
        }
        content := Join(contents, "`n")
        ; レコード作成:工番1行目、時間2行目、案件名3行目、内容4行目~
        record := RecordData(rows[1], rows[2], rows[3], content)
        records.Push(record)
    }
    return records
}

;Func 文字列配列を区切り文字で連結
Join(array, delimiter){
    ret := ""
    ; 戻り値に配列内の各文字列を区切り文字と合わせて結合
    for str in array
	    ret .= str . delimiter
    ret := RTrim(ret, delimiter) ; 末尾の区切り文字を削除
    return ret
}

;Func ログ出力
Log(msg){
    Stdout.WriteLine(msg)
}

高速処理を実現するための地味だけど効くチューニング

製造業や研究開発の現場では、センサーデータや画像処理、シミュレーション結果など、膨大なデータをリアルタイムで処理するニーズが年々高まっています。先日開発したあるソフトウェアでは、1秒間に約30万件の2次元データを処理する必要がありました。これだけのデータ量になると、気にせず実装すると要求速度に間に合わなくなります。

今回は高速化のために以下のようなチューニングを行いました。どれも派手さはありませんが、確実に効くテクニックです。

1. クラスではなく構造体(struct)を使う

C#などの言語では、クラスはヒープに配置されるため、GC(ガーベジコレクション)の影響を受けやすくなります。構造体はスタックに配置されるため、生成・破棄のコストが低く、処理速度に貢献します。特に大量のデータを扱う場合、この差は無視できません。

2. ListやDictionaryを避け、配列を使う

コレクションクラスは便利ですが、内部的にメモリの再確保やハッシュ計算などが発生し、オーバーヘッドが大きくなります。今回は、あえて2次元配列を使い、アクセスパターンを最適化しました。これにより、CPUキャッシュの効率も向上しました。ただし単に2次元配列にするとコードの書き方が古臭く保守しずらくなるため、2次元配列ということを意識せずにアクセスするための拡張メソッドを大量に追加しました。

3. インスタンスのプーリング

毎回 new して破棄するのではなく、使い終わったインスタンスを再利用する「オブジェクトプール」を導入しました。これにより、GCの発生頻度を抑え、安定したスループットを実現しています。今回の場合、グラフ描画などのGUIで威力を発揮しました。

結果

これらの地道な最適化を積み重ねた結果、無事にお客様の要求する処理速度をクリアすることができました。見た目は地味ですが、こうした「低レイヤーの工夫」が、システム全体の性能を大きく左右します。見えないところで効く技術は大事です。

仕様の検討

こんにちは mtj です

システムの仕様の検討 難しい内容です しかしここさえしっかり決まればあとは実現するだけの内容です
システムの良し悪しはほぼここに詰まっていると思います(動作が重たい等は別の話)

検討する上でやはり不明点を洗い出す事かと思われます

例えばデータの入力が不明ならどういった場面でどのように入力するのか どのような人が入力するのか
操作するシチュエーションも大事です 座ってじっくり入力するのか さっと入力したいのか

そういった要点を押さえる 情報を集めるとなるといろんな人の関わりが必要となります
なのでやはりエンジニアもクライアントとの会話が必要になります

そういった会話の技能も高めていきたいと思いました