1. はじめに
この記事について
こんにちは。インプリムのhmineです。
今回は、PleasanterのGitHubに興味がある皆さまと、C#のシステム開発初学者から中級者へのレベルアップをめざす皆さまに向けて記事を執筆しました。
記事の全体像
記事の前半では、まず「プログラミング言語の書き方」を習得した先にある、「システムを開発するスキル」を取得する方法論について考えます。続けて、それと弊社(株式会社インプリム)のプロダクトである「Pleasanter」がどう関わるかという観点から、Pleasanterのソースコードの特徴をご説明します。
記事の後半では、最近更新されたソースコードの引用・解説を行います。
-
この記事で解説するC#の記法
- LINQの
Enmutable.Where
メソッドによるフィルタ処理(絞り込み処理)
- LINQの
最後まで読んでただいた際、「プリザンターのソースコードリポジトリを見てみたい」「GitHub上でOSSに貢献してみたい」といったご検討をいただけると嬉しいです!
2. Pleasanterのデバッグ環境構築の勧め
プログラミング言語の習得とシステム開発
「本や講座でプログラミング言語を学ぶ」という方法論は、多くのエンジニアの方が実践しているか、少なくともやってみようとしたことがあるやり方だと思います。
そこで避けられない問題は「本や講座のサンプルコードと業務システムのソースコードの間には、規模の面で大きな乖離がある」ということです。業務システムが抱えるファイル数や、その1ファイル当たりの行数は、本や講座のサンプルコードと比較すると非常に巨大であるはずです。
はじめてプログラマーの業務に就いた方には、最初の数年間は「巨大な資源を取り扱うにはどのように仕事に取り組めばよいか」を覚えていくミッションが待っています。
Pleasanterのパブリックなソースコードを活用しよう!
PleasanterのGitHubリポジトリをご紹介します。以下のリンク先をご覧ください。
GitHubリポジトリのREADMEは英語で書かれていますが、Qiita上には日本語の記事も充実していますのでご安心ください! もしお時間があれば、ぜひ以下の手順の通りVisual Studio上でPleasanterが動く開発環境をつくっていただきたいです!
Pleasanterは社会インフラを支える企業様に導入をいただいている、信頼性の高いプロダクトです。以下の記事で導入事例をいくつかご紹介しています。
以上のように、どなたでもアクセスできるリポジトリに、現役の業務システムのソースコードが公開されていますので、Pleasanterのソースコードは、C#によるシステム開発の規模感をイメージしたいと考えるエンジニアの皆さま(レベルは問わず)の一助になるのではないかと思います。
Pleasanterのソースコードの"クセ"
Pleasanterには、本や講座のソースコードとは少し違った考え方で書かれてる処理が多く存在します。これは、インプリム創始者の内田がPleasanterを個人開発でローンチした際に1、プログラムの基盤であるフレームワークもゼロからコーディングしたためです。
この件については、今後のQiitaの記事にて、Pleasanterのソースコードがもつ独自のコーディング手法についての解説ができればと思います!
3. Pleasanterのソース解説
それでは実際に最近のPleasanterのコミット履歴を元に、簡単にソースリーディングをしてみましょう。
LINQによる集合のフィルタ処理(絞り込み処理)
以下のページは、Microsoftが公開しているEnmutable.Where
メソッドのリファレンスです。
C#の正式な仕様は、このページのようなMicrosoftのドキュメントで確認します。
ただし、「このメソッドを利用すると結局何が嬉しいの?」という疑問については、Pleasanterのような業務システムのソースコードを見るほうが理解しやすい場合も多いです。
Enmutable.Where
メソッドがPleasanterでどのように使われているか見てみましょう。
Pleasanterのコード上のEnmutable.Where
メソッド
Pleasanter_1.4.1.0(2024年2月13日リリース)のコミット差分を見てみましょう。
上のリンクをクリックし、以下の手順でコードを確認してください。
- Ctrl+Fキーを同時に押してブラウザの検索欄を開く
- ブラウザの検索欄に
Implem.Pleasanter/Libraries/Settings/SiteSettings.cs
と入力してSiteSettings.csファイルの先頭まで移動する - 2870行目を確認する
2870 + .Where(o => FilterColumn(
2871 + context: context,
2872 + ss: this,
2873 + def: o,
2874 + selection: selection,
2875 + keyWord: keyWord))
.Where
を利用しているコードが見つかりました!
.Where
の引数に記述されているラムダ演算子(=>
)の左辺は、フィルタされる集合から取り出した単一のデータです。右辺で同じクラスのFilterColumn
というメソッドを呼び出していますが、これはo
を参照し True/False のいずれかのboolの返却値を戻す条件判定処理です。SiteSettings.cs
にFilterColumn
メソッドの内容が書いてありますので探してみてください。
一旦Pleasanterのコードは置いておき、仮にo
が数値であるシンプルな処理を考えます。.Where(o => o > 0)
というコードを実行することで、数値の集合が絞り込まれて、0を超える数値の集合が生成されます。o > 0
もTrue/False のいずれかのboolの戻り値を戻す条件判定式です。
以上より、SiteSettings.cs
のFilterColumn
メソッドは、o > 0
というシンプルな判定式と同じようにラムダ演算子(=>
)の右辺に記述できることを理解していただければと思います。
「まだEnmutable.Where
メソッドが分からないかも…」
という皆さまも、とりあえず記事の先を読み進めていただいてOKです!
この先はVisual Studioを実際に動かす作業となります。最後まで動かしてみた後で再度上記のMicrosoftのリファレンスの記事から読み直してみてください!
Visual Studioのデバッグ機能を使ってみよう!
"「エディタ」の選択肢一覧をフィルタする機能" について
先ほどご覧いただいたコードは、 "「エディタ」の選択肢一覧をフィルタする機能" で追加された処理です。この機能の大まかな説明としては、下図の[選択肢一覧]の中にあるリスト(初期状態で150項目以上)を、[検索ダイアログ]の操作を元にフィルタする(=絞り込む)機能です。
「エディタ」タブを開くまで
Visual Studio上でPleasanterの環境構築をした状態であれば、以下の手順を進めることで "「エディタ」の選択肢一覧をフィルタする機能" を操作できます。
まずは、テーブルを新規作成して「エディタ」タブを開いてみましょう。
- Pleasanterを起動、ログインする。手順は以下の記事(主に後半部)を参照
- テーブルを1つ新規作成する。手順は以下を参照
- 新規作成したテーブルについて、項目追加を行う画面である「エディタ」タブを開く。手順は以下を参照
検索ダイアログを使ってみよう
手順は以下のマニュアルの "「選択肢一覧」のフィルタ機能" の項に書かれています。
お手元のブラウザで検索ダイアログを操作し、選択肢一覧の内容が変化することを確認してください。検索ダイアログは各種フィルタ条件に対応しているので、クリックするボタンを変えたり、テキストボックスに入力する文字列を変えたりして、色々な入力パターンを試してみましょう。
🐛フィルタ処理(絞り込み処理)のデバッグ
ここまでで検索ダイアログの動きをひととおり確認できましたか?
それでは、Visual Studioのデバッグ機能でEnmutable.Where
メソッドを使用したフィルタ処理の動きを見てみましょう! 以下の順序で画面とVisual Studioを操作してください。
- ブラウザ上で検索ダイアログを開く
- Visual Studioで
Implem.Pleasanter/Libraries/Settings/SiteSettings.cs
を開く - Visual Studioで
private bool FilterColumn
の文字列を指定し、上記の.csファイルの検索を行ってFilterColumn
メソッドの内容を参照する - Visual Studioで
FilterColumn
メソッドの開始行にブレークポイントを付ける - ブラウザ上で検索ダイアログの 「分類」ボタン をクリックする
- ブラウザ上で画面の動作が止まったことと、Visual Studioのブレークポイントで処理が停止したことを確認する
- メソッド内で第3引数の def を確認できるように「ローカル」タブを開く
- F10キーを押して/「ステップオーバー」ボタンをクリックして処理を1行単位で進める
- 処理が
FilterColumn
メソッドの終端に到達した際、F10キー/「ステップオーバー」ボタンクリックを継続し、以下の事項を確認する- 次に実行するコード(行)はどこか?
-
「ローカル」タブに表示される
def
の内容をウォッチしてわかることは何か?
- ブラウザで画面を操作可能な状態に戻したい場合は、ブレークポイントを無効化または削除 → 「続行」ボタンをクリックする
[FilterColumn
メソッド記述箇所のイメージ]
※一部のコードを省略した上で、行の採番はこの記事独自で行っています。
※ソースコードは2024年5月1日時点の内容です。
01 private bool FilterColumn(
02 Context context,
03 SiteSettings ss,
04 ColumnDefinition def,
05 string selection,
06 string keyWord)
07 { // ←←←★★★ ブレークポイント付ける行です! ★★★
08 switch (selection)
09 {
10 case "KeyWord":
11 var keyWords = keyWord.Replace(" ", " ").Split(" ");
12 return keyWords.All(o => def.ColumnName.Contains(
13 // 以下略
14 case "Basic":
15 return new List<string>
16 // 以下略
17 case "Class":
18 case "Num":
19 case "Date":
20 case "Description":
21 case "Check":
22 case "Attachments":
23 return def.ColumnName.StartsWith(selection);
24 default:
25 return true;
26 }
27 }
💡デバッグを実行してわかること1 ~"繰り返して絞り込む"
先ほどの問いについて、答えは準備できましたか? この後解説がはじまりますので、ぜひあらかじめご自身の目で処理の動きを追ってみてください。
9.処理が
FilterColumn
メソッドの終端に到達した際、F10キー/「ステップオーバー」ボタンクリックを継続し、以下の事項を確認する
- 次に実行するコード(行)はどこか?
- 「ローカル」タブに表示される
def
の内容をウォッチしてわかることは何か?
Q. FilterColumn
メソッドの終端に到達した際、次に実行するコード(行)はどこか?
A. .Where
の右辺にある判定処理を全項目分、繰り返し実行する。
- 呼び出し元に戻りました
- 先に
.Where
でフィルタする別の条件があるため、その行が実行されます
---- 脇道の解説 ----
※読み飛ばして 3. に進んで構いません。- 上図の黄色くなった行に記載されている
.Contains
の判定は、「既に有効化され、画面の「現在の設定」側にある項目を除外する判定処理」です。 - その1行上のコードを読んでみます。これは、フィルタされる対象全体を取得する、
ColumnDefinitionHash.EditorDefinitions(context: context)
。実は、画面上の「現在の設定」と「選択肢一覧」を統合したリスト=該当のテーブルで有効化しうる全項目を取得する処理です。 - 再び上図の黄色くなった行を見てください。これは1つ目のフィルタ処理であるため、先に実行されました。
- この記事で取り上げている
FilterColumn
メソッドのフィルタ処理は、上図の黄色くなった行の.Where
の処理で除外されなかったオブジェクトに対する2つ目のフィルタ処理となります。 - つまりどういうこと?
→LINQで.Where
をメソッドチェーンで記述した際の正確な実行順がわかりました。
・×:1つ目のフィルタをすべて繰り返す → 2つ目のフィルタをすべて繰り返す
・○:右記処理を繰り返す〈1つ目のフィルタ処理 → 2つ目のフィルタ処理〉
- 上図の黄色くなった行に記載されている
- 上記の1つ目のフィルタ処理で除外されなかった項目を処理している場合は、再度
FilterColumn
メソッドの呼び出し元に到達します
-
FilterColumn
メソッドにブレークポイントが付いているため、再びFilterColumn
メソッドの中に入ります
Q. 「ローカル」タブに表示されるdef
の内容をウォッチしてわかることは何か?
A. FilterColumn
メソッドの繰り返し実行で、ColumnName等が異なるオブジェクトを順番に参照している。
デバッグすると、FilterColumn
メソッドの繰り返し実行により、ColumnName
に「"Locked"→"ClassA"→"ClassB"→"ClassC"…」が設定されているオブジェクトを順番に参照していることがわかります。なお、"ClassZ"の次は"NumA"のオブジェクトとなります。
ColumnName
について、今回はマニュアルの引用で解説します。
(引用元:テーブルの管理:エディタ:エディタの項目の設定)
キーワード検索のルール
- 「項目名」「表示名」「カラム名」のいずれかがキーワードと一致する項目を検索します。
- 「項目名」:「分類A」等のデフォルト名称。
- 「表示名」:「テーブルの管理:エディタ:項目の詳細設定:表示名」で設定した名称。
- 「カラム名」:「ClassA」などデータベース上のカラム名。
上記の引用にある「カラム名」は、デバッグで確認したColumnName
と同じものです。今回見ているFilterColumn
メソッドの引数ColumnDefinition def
には、メソッドが繰り返し呼び出される度に、「選択肢一覧」の中身が一つずつ渡っています。FilterColumn
メソッドの実行中に引数ColumnDefinition def
>ColumnName
を参照すると、「選択肢一覧」にある各選択肢の「カラム名」を調べることができます。
〈番外編〉ColumnName
について
---- かなり脇道なので、読み飛ばしても構いません!----
Pleasanterに作成したテーブルのレコードを保存する仕組みについて簡単に説明します。
お手元のRDBMS管理ツールを使用して、RDBMS上にある「Issues」および「Results」というテーブルに「Locked/ClassA/ClassB/ClassC/…」といったカラムがあることを確認してください。このようにPleasanterのレコードは、RDBMS上において、「ロック」項目の入力→「Locked」カラムに登録/「分類A」項目の入力→「ClassA」カラムに登録/…といった方式により、「登録先のカラムは項目ごとに固定」というルールで保存されています。
この「カラム名」の考え方は、レコードを扱う処理(=システム利用者向け機能)だけでなく、今回のようなテーブルの設定内容を扱う処理(=システム管理者向け機能)にも使われています。「選択肢一覧」は画面上に日本語名が表示されていますが、プログラム上ではカラム名で取り扱えるように処理が組まれているのです。
まとめ - 📝Enmutable.Where
の繰り返し処理について
[「選択肢一覧」に対するEnmutable.Where
(フィルタ処理)のイメージ]
[Enmutable.Where
(フィルタ処理)の結果のイメージ]
Enmutable.Where
を使うとシンプルなコードを書けますね! 繰り返し構文として有名なfor文と比較すると記述する行数が少なく、読みやすいですね!
💡デバッグを実行してわかること2 ~この選択肢はTrue or False?
Enmutable.Where
によるフィルタ処理では、集合を構成している要素(オブジェクト)ひとつひとつを参照して、所定の判定処理がTrueである要素(オブジェクト)のみの集合を生成していたことが、伝わりましたでしょうか?
最後に、FilterColumn
メソッドで行われている条件判定の内容も少しだけ見てみましょう!
ソースコードを再掲します。
01 private bool FilterColumn(
02 Context context,
03 SiteSettings ss,
04 ColumnDefinition def,
05 string selection,
06 string keyWord)
07 {
08 switch (selection) // ←←←★★★ ここから読みます! ★★★
09 {
10 case "KeyWord":
11 var keyWords = keyWord.Replace(" ", " ").Split(" ");
12 return keyWords.All(o => def.ColumnName.Contains(
13 // 以下略
14 case "Basic":
15 return new List<string>
16 // 以下略
17 case "Class":
18 case "Num":
19 case "Date":
20 case "Description":
21 case "Check":
22 case "Attachments":
23 return def.ColumnName.StartsWith(selection);
24 default:
25 return true;
26 }
27 }
08行目で判定しているselection
は、検索ダイアログでクリックしたボタンを判定する条件分岐です。「分類」ボタンをクリックするとselection
には"Class"が設定されます。(※この記事の趣旨とまったく異なる技術要素であるため、解説はありません。ご了承ください)
したがって、次に実行されるのは23行目です。
23行目に見え覚えのあるコードが書かれています。繰り返し処理の対象となる各選択肢がもつdef.ColumnName
の値が「"Locked"→"ClassA"→"ClassB"→"ClassC"…」等であることは、先ほど確認した通りです! これらの文字列に対して.StartsWith(selection)
を実行。つまり「選択肢のカラム名が"Class"という文字列ではじまっているか?」が、「分類」ボタンをクリックした場合に処理している判定条件なのです。
「分類」ボタンクリック時、def.ColumnName
に対するFilterColumn
メソッドの結果
- "Locked" → 返却値:False
- "ClassA" → 返却値:True
- "ClassB" → 返却値:True
- "ClassC" → 返却値:True …
- "NumA" → 返却値:False …
📝さらに調べてみたい皆さまへ
検索ダイアログの「分類」ボタン以外(かつ「キャンセル」「×」ボタン以外)をクリックした際も、FilterColumn
メソッドによる条件判定が実行されています。どのような判定が実行されているか、考えてみてください。調べていくとEnumerable.All
というLINQのメソッドについて理解が進むかと思います!
今回の解説はここまでです!
"「エディタ」の選択肢一覧をフィルタする機能" 追加の対応では、他にも様々な処理をC#やJavaScriptで記述し、機能をリリースすることができました。この記事では、その中でも最も重要なフィルタ処理について解説しました。
「選択肢一覧」に対するフィルタ処理の本体は、LINQのEnmutable.Where
メソッドである ということが伝われば嬉しいです!
4. おわりに
この記事の手順でつまったところがあれば、お気軽にコメントをください!
また、「Pleasanterを動かしてみたよ!」というご報告の記事を公開いただけると、わたしたち開発チームにとっての励みになりますので、ぜひ公開をお願いします!
さらに、Pleasanterの活用方法や、ソースコード本体への機能追加についても、皆さまのアイデアをお待ちしています!
-
個人開発について、本人によるLTの内容がログミーTechで公開されています。
「無償公開していたからこそ、サブスクで売上が立てられた」個人開発のオープンソースを元に法人化&マネタイズ - https://logmi.jp/tech/articles/330082 ↩