.NET Frameworkで多言語対応 その2
前回、多言語リソースを作成しましたが、それをどうやって利用するか書いていこうと思います。
Xaml内で使用する
今回はWPFでアプリケーションを作成したので画面はXamlで作成しました。
Xaml内でリソースにアクセスできるようにまずはWindowの属性に名前空間を追加します。
xmlns:prop="clr-namespace:VSS2SVN.Properties"
これでProperties名前空間内のクラスにアクセスできるようになります。
ウィンドウタイトルをリソースを使って表示する場合はこんな書き方になります。
Title="{x:Static prop:Resources.C_WindowTitle}"
言葉にすると「WindowのTitle属性(プロパティ)にpropで定義した名前空間のResourcesクラスのC_WindowTitleプロパティ(スタティック)をセットする」です。
項目のタイトルとかも同じようにリソースを当てはめていきます。
<Label Content="{x:Static prop:Resources.P_WorkingDirectory}" Style="{StaticResource title}" />
あとは実行したときの言語設定にしたがって表示されます、とても簡単です。
モデルの属性(アノテーション)で使用する
WPFで実装しているので画面の項目やボタンに対応するViewModelを作っています。そのプロパティにもリソースを用いて属性を付与しています。
[Display(Name = "P_WorkingDirectory", ResourceType = typeof(Resources))] public ReactiveProperty<string> WorkingDirectory { get; }
言葉にすると「WorkingDirecotryプロパティに表示名属性を付与、その名前はResourcesクラスのP_WorkingDirectoryを当てはめる」です。
メソッド内の記述で使用する
メソッド内部で使用するときは単純に直接記述するのが一番だと思います。
string directory = "c:\users\aaaa\appdata\local\temp"; string message = string.Format(Resources.C_FoundVSSProjectFolder, directory);
リソース文字列に整形が必要ならばFormatメソッドで整形し使う、そうじゃないならそのまま使う。これだけです。
文字列をリソースに追いやると、いろいろな場面で似たような文字列定数を宣言しなくて良くなるのが一番の利点ですね(当たり前)。
.NET Frameworkで多言語対応 その1
多言語対応
なんとなくですが多言語対応するアプリケーションを開発するための準備を始めました。
.NET Frameworkでは最も簡単な方法がResource.resxを対応する言語の数だけ用意するのがどうやら一般的なのでその方法を書き留めます。
設定の方法
通常のResource.resx内に英語で必要な文字列を定義していきます。
アセンブリの外部で使えるようにアクセス修飾子はPublic にします。(英語は適当です、気にしないでください)
次にProperties内にResources.ja-JP.resxファイルを作成します。
文字列の名前をResources.resxで定義したものと全く同じにして値を日本語にして追加していきます。
こちらもアクセス修飾子をPublicにします。忘れると上手くいかなくなりました。(理由は後述)
実行結果
実行するとこんな感じになります。
日本語Windowsなので日本語で表示されます。
プロジェクトファイルのSettingに言語を登録してみます。
追加した言語をスタートアップで設定します。
public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); VSS2SVN.Properties.Resources.Culture = VSS2SVN.Properties.Settings.Default.Language; } }
実行すると
ビルドしたフォルダをみると「ja-JP」フォルダがあります。その中にあるdllに日本語の文字列が格納されていてプログラム内の言語に従って使い分けされるようになっています。
resource.dllが本体と別れているので外部に公開されていないと本体側で使用できないことになります。なので前述のアクセス修飾子はPublicにしておく必要がありました。
この次
プログラム内で定義したリソース文字列をどうやって使っているかをソースコードで説明してみようと思います。
SlackとSubVersionの連携(Windows)
Slackはじめました
しばらく更新できなかったのですが、社内メンバーのコミュニケーション用にSlackを使用し始めましたのでその設定やなんやで手が空きませんでした。
やっとこれでよいかなという設定になったのでこれに関して少し書こうかと。
社内で運用しているシステムと何か連携させたいなと思っていたのでまずソース管理と連携してみました。
備忘録を兼ねてここに書き留めておこうと思います。
Subversion
会社ではSubversionを使っているので(Gitに変えたいな)これのコミット時にSlackにメッセージを投稿します。
アプリの追加
Slackのアプリをワークスペースに追加します。
追加するのはカスタムインテグレーションのIncoming Webhookです。
SubVersionをWindowsServerに設定して使っているのでWindowsのコマンドを使ってメッセージを送ろうかと思ったのですが、いろいろできるのはPowerShellなのでこれで作ります。
Incoming Webhookの設定
アプリを追加するとどのチャンネルへ投稿するかを聞いてくるのでチャンネルを選択します。
するとWebhook URLが表示されるのでこれを書き留めておきます。
Slack側は一旦ここで設定終了。
SubVersionの設定
リポジトリのhooksフォルダに「post-commit.cmd」「Post-Commit.ps1」を作りました。
post-commit.cmd
powershell -ExecutionPolicy RemoteSigned -File %1\hooks\Post-Commit.ps1 -repo %1 -rev %2
PowerShellの実行権限をRemoteSigned にしてローカルファイルの実行には制限がないようにセットしてPost-Commit.ps1を実行するようにします。 -repo と -rev はPost-Commit.ps1内で利用する引数です、Subversionリポジトリのパスとコミットしたリビジョンがセットされます。
Post-Commit.ps1
Param([string]$repo, [int]$rev) $author = svnlook author $repo -r $rev $author += "さんがコミットしました。" $log = svnlook log $repo -r $rev $log = $log -split "`r`n" $log = $log -join "`r`n" $changed = svnlook changed $repo -r $rev $changed = $changed -split "`r`n" $changed = $changed -join "`r`n" $changed = "```````r`n" + $changed + "`r`n```````r`n" $dir = $repo.Split("\") $repository = $dir[-1] $text = "`{`"text`": `"Repository:$repository Revision:$rev`r`n$author`r`n$log`r`n$changed`"`}" $Byte = [System.Text.Encoding]::UTF8.GetBytes($text) Invoke-RestMethod -Uri "https://hooks.slack.com/services/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" -Method Post -Body $Byte
PowerShellスクリプトの中身はこんな感じです。
細かく解説入れておきます。
基本事項ですが$...は変数です。
Param([string]$repo, [int]$rev)
Paramコマンドで実行時のコマンドライン引数を変数に代入します。
括弧()内の変数名を呼び出しのコマンドライン引数の名前と一致させて保持します。
次行意向でSubversionのコマンドを使うのでそれに必要な引数を保持しました。
$author = svnlook author $repo -r $rev $author += "さんがコミットしました。"
svnlookコマンドでコミットログを取得します。svnlook auther でコミットした人を取得できるので、その結果を変数に保持しておきます。
$log = svnlook log $repo -r $rev $log = $log -split "`r`n" $log = $log -join "`r`n"
svnlook logでコミットしたときに記入したメッセージを取得します。
PowerShellのエスケープシーケンスはよくある[\]ではなく[`]なので[``````]は[```]になります。なので[`r`n]は改行コードです。
取得した内容の改行コードがその後の文字列編集でなぜかわからないけど消えてしまうので、やっつけですがsplitコマンド配列に変換、joinコマンドで変換した配列を改行コードを付けて結合しています。
$changed = svnlook changed $repo -r $rev $changed = $changed -split "`r`n" $changed = $changed -join "`r`n" $changed = "```````r`n" + $changed + "`r`n```````r`n"
svnlookでコミットしたファイルの一覧が取得できます。
[``````]は[```]でSlackのコードブロック表示なのでコミットしたファイルを成形なしで表示するようにしています。
$dir = $repo.Split("\") $repository = $dir[-1]
リポジトリパスをディレクトリ毎に区切って最後のディレクトリを取得しています。配列の最後を取得する場合は気持ち悪いですが添え字を-1にすると良いみたいです。
$text = "`{`"text`": `"Repository:$repository Revision:$rev`r`n$author`r`n$log`r`n$changed`"`}"
Slackに渡す内容をJSON形式に成形しています。ここではシンプルに内容(text)に今まで取得した情報の全てを載せています。
$Byte = [System.Text.Encoding]::UTF8.GetBytes($text)
Slackで受け付ける文字がUTF-8なので.NET FrameworkのUTF8エンコーディングクラスでエンコードしています。
Invoke-RestMethod -Uri "https://hooks.slack.com/services/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" -Method Post -Body $Byte
最後がSlackに投稿する部分でLinuxのcurlと同じようなコマンドでInvoke-RestMethodっていう何を行うかはっきりとわかる丁寧な名前のコマンドで送信します。
HTTPメソッドがPost、送信部分はBodyに付けて送信します。
これでSubversionとSlackが連携出来ました。
インターフェイス その3
今回の内容
前回の終わりに次回はリフレクションを使って実装と書きましたが、その前にまず、値を単純に保持するだけのクラスとインターフェイスを作ってそれを読み込む処理を作ってみようと思います。
データを保持するクラスを追加
データ保持インターフェイスを定義
値を保持するだけのクラスが実装するインターフェイスをまず作ります。
interface IRow { void SetValues(IEnumerable<object> values); object this[int i] { get; } object this[string name] { get; } int FieldCount { get; } }
値を一括でセットするSetValuesメソッドと値を取得するデフォルトプロパティを実装しました。とりあえずIDataRecordインターフェイスと同じ感じにしておきました。
Rowクラスの実装
せっかくなので2種類作ります。
class FileRow : IRow { private List<object> Values = new List<object>(); public void SetValues(IEnumerable<object> values) { Values.Clear(); foreach(var value in values) { Values.Add(value); } } public object this[int i] { get { return Values[i]; } } public object this[string name] { get { throw new NotSupportedException(); } } public int FieldCount { get { return Values.Count; } } }
class TableRow : IRow { private Dictionary<string, object> Values = new[] { "Column1", "Column2", "Column3" }.ToDictionary(c => c, c => (object)null); public void SetValues(IEnumerable<object> values) { var keyValues = Values.Zip( values, (a, b) => new KeyValuePair<string, object>(a.Key, b) ); foreach(var kv in keyValues) { Values[kv.Key] = kv.Value; } } public object this[int i] { get { return Values.Values.Skip(i).First(); } } public object this[string name] { get { return Values[name]; } } public int FieldCount { get { return Values.Count; } } }
2つ作りました。1つ目はSetValuesで与えられた値一覧をそのまま保持するクラス、もう一つはあらかじめ項目名が決まっていて、決まった項目に値をセットするクラス。
2つ目のクラスのSetValuesメソッド内に出てくるZipメソッドはLINQの拡張メソッドで2つの一覧を短いほうに合わせて結合するメソッドです。
2つの要素に対してまとめて処理する際にとても便利なので使い方を覚えてしまうと良いと思います。
読み込み処理を記述
読み込み処理の抽象クラスを実装
読み込み処理の抽象クラスを作ります。
abstract class Reader<T> : IReader<T>, IReader where T : IRow { /// <summary> /// 生成関数 /// </summary> private Funccreator; /// <summary> /// コンストラクタ /// </summary> /// <param name="creator"></param> public Reader(Func<T> creator) { this.creator = creator; } /// <summary> /// オブジェクト生成 /// </summary> protected T CreateObject() { return creator(); } /// <summary> /// 実行 /// </summary> public abstract IEnumerable<T> Execute(); /// <summary> /// 実行(IReaderインターフェイス) /// </summary> IEnumerable IReader.Execute() { return Execute(); } }
このクラスはIReader<T>、IReaderインターフェイスを実装するように定義しています。
区別することができないメソッド定義「Execute」があるためIReader<T>インターフェイスがIReaderインターフェイスを継承するように定義することがません、そこで抽象クラスで両方を実装するようにまとめてしまいます。
クラスではインターフェイスを区別して実装するための仕組みがあります。
[インターフェイス名].[メソッド名]と記述することでインターフェイスのメソッドを区別して実装しています。
このようなかたちで実装するとこのオブジェクト(インスタンス)を利用するときにIReaderインターフェイスにキャストするとIReader.Executeメソッドが実行されますが、Reader<T>クラスにキャストすると普通に実装しているExecuteメソッドが実行されます。
そもそも違う動きをさせるつもりがないのでIReader.Executeメソッド内でも同じ動作になるように記述しています。
読み込み処理本体クラスを実装
IReader<T>抽象クラスを継承して処理本体を記述していきます。
1つ目はファイルを読み込むクラス
class FileReader<T> : Reader<T> where T : IRow { /// <summary> /// コンストラクタ /// </summary> /// <param name="creator">オブジェクト生成関数</param> public FileReader(Func<T> creator) : base(creator) { } public override IEnumerable<T> Execute() { using (StreamReader streamReader = new StreamReader(Properties.Settings.Default.FileName)) { while(!streamReader.EndOfStream) { var line = streamReader.ReadLine(); var values = line.Split(','); var obj = CreateObject(); obj.SetValues(values); yield return obj; } } } }
コンストラクタは抽象クラスにそのまま預けて、Execute内部では実行ファイルのSettingにあるファイルを読み込み、1行ごとインスタンスを生成し「,」で区切られた項目をインスタンスにセットして返却するように実装しています。
初めて「yield return」が現れましたが、特殊な返却処理です。
返却した内容は呼び出した側から見ると一覧(IEnumerable)になり、LINQの拡張メソッドが使用できます。
普通に記述するとループ構造が複雑になる場合に使える便利な書き方です。また、データを読み込む側と書き込む側でメソッドやクラスを分割するのに使用できるのでうまく分けると再利用性を上げたりできます。
もう一つはデータベース内のテーブルを読み込むクラス
class DbReader<T> : Reader<T> where T : IRow { /// <summary> /// コンストラクタ /// </summary> /// <param name="creator">オブジェクト生成関数</param> public DbReader(Func<T> creator) : base(creator) { } /// <summary> /// 読み込んだ内容をT型に変換して繰り返し返却 /// </summary> public override IEnumerable<T> Execute() { var factory = DbProviderFactories.GetFactory("Npgsql"); using (DbConnection connection = factory.CreateConnection()) { connection.ConnectionString = Properties.Settings.Default.ConnectionString; connection.Open(); using (DbCommand command = connection.CreateCommand()) { command.CommandText = "SELECT * FROM TABLE1"; using (DbDataReader dataReader = command.ExecuteReader()) { //DataReaderが値を取得しない場合ここで終了 if (!dataReader.HasRows) yield break; //DataReaderのフィールド名をLINQを使って列挙する //DataReaderのフィールド名は順序で取得するので順序をまず作成 var ordinals = Enumerable.Range(0, dataReader.FieldCount); while (dataReader.Read()) { //返却するオブジェクト生成 var obj = CreateObject(); var values = ordinals.Select(i => dataReader[i]); obj.SetValues(values); yield return obj; } } } } } }
ちょっと長くなっていますが、ファイルから読み込んでいる部分がテーブルから読み込んでいる以外の違いはありません。
Readerファクトリクラスを追加
ファクトリクラスでどんな読み込み処理を公開するか決めます。
static class ReaderFactory { public static IReader CreateReader(string readerName) { switch (readerName) { case "DbReader": return new DbReader<TableRow>(() => new TableRow()); case "FileReader": return new FileReader<FileRow>(() => new FileRow()); default: throw new InvalidOperationException("readerNameが無効です。"); } } }
引数でどの処理を行うか決まるように実装しています。実際使うときは設定を読み込んでそれに従って決めるとか、ユニットテストを行うときにはテスト用のReaderを生成するとか、いろいろここで変更するようにすればと思います。
利用する側の実装
最後に使う側を実装します。
class Program { static void Main(string[] args) { if (args.Length > 0) { IReader reader = ReaderFactory.CreateReader(args[0]); foreach(IRow obj in reader.Execute()) { for(int i = 0; i < obj.FieldCount; i++) { Console.Write("{0},", obj[i]); } Console.WriteLine(); } } else { Console.WriteLine("Readerをコマンドライン引数に書いてください"); } } }
見てもらうとわかるように、利用する側はReaderに関係するインターフェイスを利用しているけれど、インスタンスを参照していないので、それがどのように実装されているかわからなくても良い状況にできました。
まとめ
インターフェイスを使用することでクラス間の関わりを切り離しして実装方法に依存しないようにすることで、テストはそれぞれのクラスで行うことができるようになり保守性が上がります。また、処理を差し替えることが容易になり、機能の変更や拡張も行いやすい状態にできます。
何が言いたかったかというと「Interface segregation principle:インターフェース分離の原則」について触れておきたかったのでした。
今回の例だけではこの原則の全てを説明できていないですが、このような考えの理解に少しでも助けになればと思います。
インターフェイス その2
今回の概要
閑話休題、今回はインターフェイスとジェネリッククラスを複合してみようと思います。
前回のプログラムではReaderが取得したファイルなりテーブルのデータを改行で区切った文字列にして返却していましたが、今回はオブジェクトの繰り返しで返却するようにして、1行の内容はオブジェクトのプロパティにセットするように変えてみます。
インターフェイスの変更
前回作成したインターフェイスをまず変更します。
interface IReader { IEnumerable Execute(); } interface IReader<T> { IEnumerable<T> Execute(); }
前回のインターフェイスでは返却値がstringでしたが戻り値をIEnumerableインタフェースとしました。返却値をforeach ~ で取り扱うことができるようにすることで、より現実のプログラムに近くなるようにしておきます。
別のインターフェイスIReader<T>も定義しておきます。名前は同じように見えますが、上で定義しているインターフェイスとは別のインターフェイスです。
こちらは特定の型(T) をIEnumerableで返却するインターフェイスとしておきます。
抽象クラスの作成
DbReader、FileReaderで抽象化可能な部分を抜き出したReaderクラスを作成しておきます。
abstract class Reader<T> { }
interfaceでオブジェクトを列挙して返却するようにExecuteを定義したのでオブジェクト生成関数がほしいのでReaderクラスのコンストラクタ引数に生成関数を追加しておこうと思います。
abstract class Reader<T> { /// <summary> /// 生成関数 /// </summary> private Func<T> creator; /// <summary> /// コンストラクタ /// </summary> /// <param name="creator">オブジェクト生成関数</param> public Reader(Func<T> creator) { this.creator = creator; } /// /// オブジェクト生成 /// /// <returns></returns> protected T CreateObject() { return creator(); } }
コンストラクタの引数で渡ってきた関数をprivate変数に保持しておいて、継承したクラス内部ではCreateObjectメソッドを利用するようにしておきます。
次回
区切りが良いので今回はここまでにします。
次回はリフレクション機能を使ってオブジェクトのプロパティに値をセットするメソッドをReaderクラスに追加していきます。
閑話 準備って大切、でもいつも失敗
ちょっと立て込んでいるので与太話を。
家で工作するのがちょっと自分の中で流行っているので、休日はなんか作ってます。
近くのスーパービバホームでなんか木とか買ってきて切ったり削ったりくっつけたり。
仕事道具の手を傷つけたり汚したりしないように手袋を付けてやったりするんですけど、なんか熱中して細かい作業に入って一度手袋を取っちゃうと、もう一度付けないんですよ。
先週それで人差し指を本当にスパッとデザインナイフでやっちゃったんですよね。あっと思った時にはもう遅い、血は全く止まらないし。ヤベーイ!って小林克也さんが頭ン中で叫んでいる(古い)。
手元に瞬間接着剤があったので切り口をギュッと押さえて接着しました。見事に一発で出血が止まりましたね、押さえた指ごとくっつきましたよ。
何をやったらどんなことが起こるか、だからどんな準備するか、頭の中で分かっていても実際にそれを行うことと続けることってほんと難しいよねって話。
インターフェイス その2
インターフェイスにできて抽象クラスにできないもの
インターフェイスは共有できる機能の定義のみを行うことになっているので、1つのクラスに複数のインターフェイスを持たせることができます。抽象クラスを継承した具体クラスは複数作ることができますが、その逆に複数の抽象クラスを継承した派生クラスを作ることができません。
これがインターフェイスの大きな特徴であり、メリットです。
抽象クラスにできてインターフェイスにできないもの
抽象クラスにはメソッドの実体を記述することができますが、インターフェイスは定義なのでメソッドの実体を作成することができません。
実装のポイント
これらが抽象クラス、派生クラス、インターフェイスを設計するポイントになります。
まとめると実装の仕方の抽象クラス、派生クラスの関係の中で定義し実装し、インターフェイス定義に応じてクラス内にインターフェイスの実体を実装します。
そしてクラス利用側にはインターフェイスとインターフェイス生成ファクトリを提供し、クラスそのものは隠蔽するといった実装をすることが多いと思います。
この考え方は依存性の注入といった設計・実装の考え方につながり、テスト駆動型開発を行うとき、威力を発揮します。
インターフェイスの定義を早く決めておくことでクラスを利用する側、クラスを作成する側での依存性がインターフェイスのみになり、それぞれ独立してプログラムを作成することが可能になります。
その場合、互いのプログラマは自身のプログラムのテストを行うためのスタブ(モック)を作成しテスト可能な状態を作ります。
プログラムを作成する規模が大きくなるほど恩恵を受ける考え方なので、インターフェイスを決めることをプログラミングの癖として身に着けることが大事だと思っています。
余談ですが、仕事でのコミュニケーションなんかも同じですね、コミュニケーション手段をあらかじめ決めておくことが円滑に仕事を進めるコツだったりしますよね。
次回
インターフェイスとジェネリッククラスを使用した例を交えて利用方法を説明してみようと思います。