【Java】設計書からDTOクラスを生成する【PowerShell】

DTOクラスは基本的にロジックを有さない。なので、実際の開発現場でも「設計書からコードを生成するようにできないか」みたいな話題が良く上がる。

というわけで、ここで仮組みしてることにした。

目次

ざっくり要件

こんな感じでやっていく。

  • 設計書に記載されている内容からDTOクラスを生成する
  • 設計書ファイルの複数入力可能
  • Javaファイルの複数出力可能
  • setter/getterの出力有無を選べる
  • 親クラスの指定ができる
  • importやpackageはEclipseにお願いするので不要
  • 設計書のフォーマットがいじられても大丈夫なつくり(努力目標)

入力する設計書ファイルのレイアウト

以下のようなレイアウトにした。DTO設計書ってだいたいこういう感じだよな?

また、シート名は「データ項目」固定とした。

シート名が可変になってしまうと、各シートを読み込み→対象のシートであるかどうかを何かしらの色付けして判定、となり面倒じゃね?と思った次第。

実際のコード(PowerShell)

GUI付きなので、ちょっとコードが長い。ちなみに、ChatGPTに作ってもらったやつ。マジ有能!

Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

$form = New-Object System.Windows.Forms.Form
$form.Text = "DTO Generator"
$form.Size = New-Object System.Drawing.Size(600, 500)
$form.StartPosition = "CenterScreen"

$listBox = New-Object System.Windows.Forms.ListBox
$listBox.Location = New-Object System.Drawing.Point(20, 20)
$listBox.Size = New-Object System.Drawing.Size(400, 150)
$form.Controls.Add($listBox)

$fileButton = New-Object System.Windows.Forms.Button
$fileButton.Text = "ファイル追加"
$fileButton.Location = New-Object System.Drawing.Point(440, 20)
$fileButton.Size = New-Object System.Drawing.Size(100, 30)
$form.Controls.Add($fileButton)

$outputLabel = New-Object System.Windows.Forms.Label
$outputLabel.Text = "出力先フォルダ:"
$outputLabel.Location = New-Object System.Drawing.Point(20, 190)
$outputLabel.Size = New-Object System.Drawing.Size(100, 20)
$form.Controls.Add($outputLabel)

$outputBox = New-Object System.Windows.Forms.TextBox
$outputBox.Location = New-Object System.Drawing.Point(20, 220)
$outputBox.Size = New-Object System.Drawing.Size(400, 20)
$form.Controls.Add($outputBox)

$folderButton = New-Object System.Windows.Forms.Button
$folderButton.Text = "参照"
$folderButton.Location = New-Object System.Drawing.Point(440, 220)
$folderButton.Size = New-Object System.Drawing.Size(100, 30)
$form.Controls.Add($folderButton)

$runButton = New-Object System.Windows.Forms.Button
$runButton.Text = "生成実行"
$runButton.Location = New-Object System.Drawing.Point(20, 260)
$runButton.Size = New-Object System.Drawing.Size(100, 40)
$form.Controls.Add($runButton)

$logBox = New-Object System.Windows.Forms.TextBox
$logBox.Multiline = $true
$logBox.ScrollBars = "Vertical"
$logBox.Location = New-Object System.Drawing.Point(20, 320)
$logBox.Size = New-Object System.Drawing.Size(520, 120)
$form.Controls.Add($logBox)

$fileButton.Add_Click({
    $fileDialog = New-Object System.Windows.Forms.OpenFileDialog
    $fileDialog.Filter = "Excel Files (*.xlsx)|*.xlsx"
    $fileDialog.Multiselect = $true
    if ($fileDialog.ShowDialog() -eq "OK") {
        $fileDialog.FileNames | ForEach-Object { $listBox.Items.Add($_) }
    }
})

$folderButton.Add_Click({
    $folderDialog = New-Object System.Windows.Forms.FolderBrowserDialog
    if ($folderDialog.ShowDialog() -eq "OK") {
        $outputBox.Text = $folderDialog.SelectedPath
    }
})

$runButton.Add_Click({
    if ($listBox.Items.Count -eq 0) {
        [System.Windows.Forms.MessageBox]::Show("ファイルを追加してください!")
        return
    }
    if ([string]::IsNullOrWhiteSpace($outputBox.Text)) {
        [System.Windows.Forms.MessageBox]::Show("出力先フォルダを指定してください!")
        return
    }

    $logBox.Clear()
    $logBox.AppendText("実行開始...`r`n")

    $excel = New-Object -ComObject Excel.Application
    $excel.Visible = $false

    foreach ($file in $listBox.Items) {
        try {
            $logBox.AppendText("処理中: $file`r`n")
            $workbook = $excel.Workbooks.Open($file)
            $sheet = $workbook.Sheets.Item("データ項目")

            $className = ""
            $parentClass = ""
            $headerRow = 0
            $fieldCol = 0
            $generateGetter = $true

            for ($r = 1; $r -le 100; $r++) {
                for ($c = 1; $c -le 10; $c++) {
                    $val = $sheet.Cells.Item($r, $c).Value2
                    if ($val -eq "クラス名") { $className = $sheet.Cells.Item($r, $c + 1).Value2 }
                    if ($val -eq "親クラス名(任意)") { $parentClass = $sheet.Cells.Item($r, $c + 1).Value2 }
                    if ($val -eq "setter/getter") {
                        $flag = $sheet.Cells.Item($r, $c + 1).Value2
                        if ($flag -ne "あり") { $generateGetter = $false }
                    }
                    if ($val -eq "フィールド名") {
                        $headerRow = $r
                        $fieldCol = $c
                    }
                }
            }

            $lines = @()
            if ($parentClass) {
                $lines += "public class $className extends $parentClass {"
            } else {
                $lines += "public class $className {"
            }
            $lines += ""

            $fields = @()
            $row = $headerRow + 1
            while ($sheet.Cells.Item($row, $fieldCol).Value2) {
                $name = $sheet.Cells.Item($row, $fieldCol).Value2
                $type = $sheet.Cells.Item($row, $fieldCol + 1).Value2
                $comment = $sheet.Cells.Item($row, $fieldCol + 2).Value2
                $anno = $sheet.Cells.Item($row, $fieldCol + 3).Value2

                if ($anno) {
                    $anno.Split(" ") | ForEach-Object { $lines += "    $_" }
                }

                $lines += "    /** $comment */"
                if ($generateGetter) {
                    $lines += "    private $type $name;"
                } else {
                    $lines += "    public $type $name;"
                }
                $lines += ""

                $fields += [PSCustomObject]@{ Name = $name; Type = $type }
                $row++
            }

            if ($generateGetter) {
                foreach ($f in $fields) {
                    $cap = $f.Name.Substring(0,1).ToUpper() + $f.Name.Substring(1)
                    $lines += "    public $($f.Type) get$cap() {"
                    $lines += "        return $($f.Name);"
                    $lines += "    }"
                    $lines += ""
                    $lines += "    public void set$cap($($f.Type) $($f.Name)) {"
                    $lines += "        this.$($f.Name) = $($f.Name);"
                    $lines += "    }"
                    $lines += ""
                }
            }

            $lines += "}"

            $outPath = Join-Path $outputBox.Text "$className.java"
            $lines | Set-Content -Path $outPath -Encoding UTF8
            $logBox.AppendText("生成完了: $outPath`r`n")
            $workbook.Close($false)
        } catch {
            $logBox.AppendText("エラー発生: $_`r`n")
        }
    }

    $excel.Quit()
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel) | Out-Null
    [System.Windows.Forms.MessageBox]::Show("すべてのDTOクラスを生成しました!", "完了", "OK", "Information")
})

[void]$form.ShowDialog()

動かしてみる

作業ディレクトリ

今回の作業ディレクトリはこんな感じ。行ったり来たりするのが面倒なため、全部同じところに突っ込んだ。

入力ファイル

そして入力ファイルはこんな感じ。

1つめのファイルは親クラスあり、setter/getterあり。

2つめのファイルは親クラスなし、setter/getterなし。

いざ実行!

準備ができたので、この状態で起動してみる。

GUIでファイルの指定&出力先ディレクトリの指定が可能となっており、1番下の窓に実行ログが出力される。

あれこれ指定して実行、ちょっと待ったら処理完了。

出力結果

下2つが実際に出力されたもの。

まずは1号のほう。こっちは親クラスあり、setter/getterあり。

public class TestUserInfo extends DtoBase {

    @Id
    /** ユーザID */
    private String userId;

    @NotNull
    @Size(max=20)
    /** ユーザ名 */
    private String userName;

    @Size(max=3)
    /** 年齢 */
    private int age;

    @Email
    /** メールアドレス */
    private String mail;

    /** ユーザロール情報 */
    private UserRoleInfo userRoleInfo;

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getMail() {
        return mail;
    }

    public void setMail(String mail) {
        this.mail = mail;
    }

    public UserRoleInfo getUserRoleInfo() {
        return userRoleInfo;
    }

    public void setUserRoleInfo(UserRoleInfo userRoleInfo) {
        this.userRoleInfo = userRoleInfo;
    }

}

そして2号。こっちは親クラスなし、setter/getterなし。

public class TestCalcInfo {

    /** 金額 */
    public int amount;

    /** 支払日 */
    public Date paymentDate;

    /** 品目 */
    public String item;

    /** 個数 */
    public int quantity;

}

どちらも想定通りに出力された。やったぜ。

とはいえ

個人的には、設計書からコードを自動生成するのはあまり良くないと思っている。自動生成するために設計書のフォーマットをガチガチに固めることになるし、ちょっと列挿入とか行挿入したらツールが上手く動かなくなってしまうというリスクもある。

はたして、プログラムが理解しやすいドキュメントは、人間が理解しやすいドキュメントなのだろうか?ツール化するために可読性を犠牲にすることにならないだろうか?

今回作ったDTO程度のものならまあいいかなって思うが、

  • どこまで自動化するか
  • ツール化のせいで設計書が煩雑にならないか

をしっかり考えた上でツール化するかどうかを決めるべきかと。

最後に

やってるうちに気づいたんだけど、ChatGPTに設計書投げ込んだらコード出力してくれるんだよね。あれ?もう俺要らなくね?

よかったらシェアしてね!
  • URLをコピーしました!
やまぐろ
この記事を書いた人
SESで業務アプリケーション開発、エンドユーザ向け機能などの開発に携わっている文系(経営学)卒エンジニア。
当サイトでは読書記録を残したり、ガジェットのレビューをしたりしています。
たまにエンジニアっぽい記事を書いたりすることも。

コメント

コメントする

このサイトは reCAPTCHA によって保護されており、Google のプライバシーポリシー および 利用規約 に適用されます。

reCaptcha の認証期間が終了しました。ページを再読み込みしてください。

目次