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に設計書投げ込んだらコード出力してくれるんだよね。あれ?もう俺要らなくね?
コメント