- 投稿日:2020-09-15T15:30:11+09:00
オブジェクト指向歴25年のオブジェクト指向おじさんが語るオブジェクト指向設計の処方箋
この記事のターゲット
この記事は以下の人々を対象としています。
- オブジェクト指向を一通りわかっている人。
- オブジェクト指向の設計力を高めたい人。
- オブジェクト指向を使っているのに、設計が綺麗にならず悩んでいる人。
- プログラムが大きくなるとオブジェクト指向設計が破綻する人。
- オブジェクト指向に限界を感じている人。
- 共同開発メンバーの設計力に差があって困っている人。
以下の人は対象外です。
- オブジェクト指向が何なのかわからない人。
- オブジェクト指向を極めている人。
オブジェクトは責任ベースで考える
オブジェクト指向といえば、やれインターフェイスだメッセージだ隠蔽だカプセル化だ、みたいな用語がたくさん出て来て、どれも関連があるようでないようで意味が分からないですよね。気取ったこと言ってんじゃねぇよと。
日本で社会人経験があれば、こんなものは一言あれば一発で理解できます。それは責任です。
仕事をするとき、責任者だとか担当者だとかを職場で設定しますよね。こうした責任者というのは、責任の範囲が決まっています。任されたことについてはしっかり責任を持って対応する。そして他の人が担当している職域には踏み込まない。「その件に関しましては私の方では対応いたしかねますが、鈴木の方が担当となっておりますのでご紹介いたしましょうか」的な奴です。
各オブジェクトには責任の範囲というものがあります。クラスの設計が美しくないなと思ったら、そのクラスの責任範囲が適切かどうかを考えるのが基本です。本当にその仕事はそのクラスが担当するべきなのか。他のクラスが担当するはずじゃなかったのか。または別途新しいクラスに担当させるべきか。クラスを職種に、オブジェクトを個人に当てはめ、擬人化して考えると上手くいきます。頭の中でオブジェクトが「その件に関しては対応しかねますので…」と言い始めたらヤバいです。
人間もやることが増えすぎると仕事が手に負えなくなるように、オブジェクトも責任を抱え込みすぎると手に負えなくなります。
責任を越える仕事は、やらない
営業担当の一職員である男性が、ロビーが汚いからといって掃除を始めてしまえば、外回りに行く時間が遅れるかもしれませんし、背広が汚れて業務に支障が出るかもしれません。もしすでに総務が清掃員を午後に手配済みであれば、その作業は無駄になります。
このくらいの掃除なら自分でできるなと思っても、そこはあえて手を出さず、総務に連絡して清掃員を手配してもらいます。総務は清掃員の手配状況について知っているでしょうから、無駄な重複作業がないよううまくやってくれるでしょう。
これはロビーというグローバル変数に営業クラスの男性オブジェクトがアクセスしてしまったことによるミスです。
責任とは何か
ここまでで言いたいことを改めて要約すると、オブジェクト指向設計を考えるときに、オブジェクトを擬人化して、そいつに責任について語らせると、結構うまくハマる、ということになります。
なおメンバ関数やメンバ変数が増えてきたら、そのクラスは責任過多になっている可能性が高いです。逆に少なすぎる場合は仕事をサボっている不要なオブジェクトかもしれません。適度な粒度に保ちましょう。
オブジェクト間の関連を設計する
よし、オブジェクトの責任範囲を適切に保てば上手くいくんだな。なんかうまく作れそうな気がしてきた…。と思ったのもつかの間、プログラムが大きくなってくるとそれでもグチャグチャになってきます。なぜだ、これがオブジェクト指向の限界なのか…。
いえいえ、オブジェクトの責任範囲を適切な粒度に保つ、というのはオブジェクト指向の基本に過ぎません。そこから先は「関連」が大切になってきます。関連とはオブジェクトとオブジェクトの間の繋がりのことです。会社で言えば組織図とか連絡窓口とかです。
デザインパターン
オブジェクト指向で言う所の関連はどのように考えればよいのか。方法の1つとしてデザインパターンを参照できます。デザインパターンとはオブジェクト指向で作られたソフトウェア設計のカタログです。ググれば色々出て来るでしょうから詳細は割愛します。
このデザインパターン、GoFってやつが有名ですが、はっきり言ってそんなに綺麗に体系化されていません。頻繁に使われる奴もあれば、こんなの使わねーよって奴もあります。またGoF以外にも様々なパターンが当然考えられるわけですが、業界全体で研究してまとめて体系化しようという流れにはなっていません。
それでもGoFを一通り見てみることで、美しいオブジェクト指向設計とは何かということが掴めるようになります。
UML
もう1つの方法は図示化することです。UMLでなくても何でもよいと言えばよいのですが、いちおう規格化されているのでUMLを使いましょう。色々な図がありますが、ことオブジェクト指向設計においてはクラス図が重要です。
ざっくりいえば、設計が美しいとクラス図も美しくなり、設計が汚いとクラス図も汚くなります。関連線がぐちゃぐちゃになったら、それを綺麗に直していくだけで、設計も半自動的に美しくなります。また図にするとレビュー(他の人からのアドバイス)を受けやすくなります。
UMLの作画は専用ツールもありますが、私はVisioか手書きです。ソフトの詳細設計をVisioで書こうとすると死ねますが、それはUMLの使い方がそもそも間違っています。図を書くときは、検討したいポイントに絞って書くべきです。ソフト全体ではなく、ライブラリだけとか、特定の機能だけとか。また造りが難しくなっちゃった所だけとか。stringクラスへの関連線を全クラスから引くとか愚の骨頂です。
ポイントを絞って図を描けば、設計を考えやすくなりますし、レビューもしやすくなりますし、ドキュメントも読みやすくなります。VisioでA4を超えるような図を描いているうちは、適度な粒度感覚が身についているとは言えません。
継承とか多態とか
継承・多態が分からないとオブジェクト指向が分かったとは言えません。しかし継承や多態とは、オブジェクト間の関連の一種に過ぎません。この記事を読んでいる皆さんは優秀な人でしょうから、適当なオブジェクト指向言語をしばらくいじっていれば理解できるでしょう。というわけで説明は省略。
関連とは何か
オブジェクトの責任範囲という基本を踏まえたうえで、各オブジェクト間の関連を考えていると、結局のところオブジェクト指向設計とは、各オブジェクト間の関連を設計するということに他ならないということが解ってくるはずです。
オブジェクトの責任を超える裁量を認める
よし、オブジェクトの責任範囲をコンパクトに保ち、デザインパターンやUMLなんかを使って各オブジェクト間の関連をよく考えれば、綺麗なプログラムを組めるのだな…。
ところがプログラムがいよいよ巨大になってくると、なかなかそうもいきません。一番よくある失敗は伝言ゲームというメッセージのリレーです。
伝言ゲーム
「御社のパーツを購入したいのですが、お見積りをいただけますでしょうか」「少々お待ちください。」「A社があのパーツを買いたいと言ってますが」「B社から卸してもらわないと売れないじゃないか、B社に確認してみろ」「御社のパーツを購入したいのですが、お見積りをいただけますでしょうか」…。
こんな伝言ゲームの登場人物が2~3人までだったらまだ許されますが、4人5人と階層が深くなるようであれば要注意です。特にデータを取得するためだけに2度3度と呼ぶのはヤバいです。
伝言板
この問題を解決するにはデータベースを置きます。データベースとは伝言板のようなものです。B社のウェブページに価格が掲載されていれば、A社に問い合わせなくてもよかったわけです。
ここで言うデータベースとは、単なる共通データ置き場という広い意味で使っています。本当のリレーショナルDBやインメモリDBでもよいですし、ファイルでもよいですし、グローバルにアクセスできるkey-valueペアのメモリストレージでもよいです。
データベースがプロセスのメモリ外にあれば、プロセスを分割することも可能になります。小さなプロセスに分割して全体のアプリケーションを実装することが可能であれば、各プロセスの粒度を押さえられます。
グローバル変数の逆襲
このデータベースとは、グローバル変数そのものです。せっかくオブジェクト指向で責任範囲を区別したのに、また全ての責任を抱え込んでどうするのか。
データベースはグローバル変数なので、やはり導入しない方が望ましいです。小さなプログラムは極力オブジェクト指向の範囲内でなんとかなるはずです。プログラムが巨大になり、伝言ゲームが過ぎると感じ始めたら、そこで初めてデータベースの導入を考えてみましょう。
結局銀の弾丸はない?
グローバル変数はしばしば悲惨な結果を招きます。ちょっとでもコード量が増えると簡単に破綻します。オブジェクト指向を導入すれば、かなりコード量が増えても耐えられます。しかしプログラムが極端に大きくなりすぎると、伝言ゲームの嵐になって別の破滅を招きます。
スキルの低いプログラマが扱うデータベースは悲惨なことになります。しかし、オブジェクト指向をある程度まで極めた猛者たちが集まり、各クラスの責任範囲を逸脱しない範囲でデータベースにアクセスするのであれば、オブジェクト指向とデータベースの高度なレベルでの両立は可能でしょう。そのバランスのとり方は、高い志を持って頑張るしかないです。
- 投稿日:2020-09-15T15:01:20+09:00
[.NET] バイトオーダーを指定した整数・バイナリの相互変換
Question
ある整数型をリトルエンディアン(あるいはビッグエンディアン)で
byte
配列に書き込みたい。
または、byte
配列に書き込まれているリトルエンディアン(あるいはビッグエンディアン)の整数型を復元したい。
どうすればいいか。
BitConverter
クラスのGetBytes
やToInt32
にはバイトオーダーを指定する方法はない。Answer
BinaryPrimitives
クラスを使用する。
ReadOnlySpan<byte>
から整数型(int
)を復元するにはReadInt32BigEndian
/ReadInt32LittleEndian
- 整数型(
int
)の内容をSpan<byte>
に書き出すにはWriteInt32BigEndian
/WriteInt32LittleEndian
メソッドを使用する。
int i1 = 12345678; var buffer = new byte[255]; BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(0, 4), i1); int i2 = BinaryPrimitives.ReadInt32LittleEndian(buffer.AsSpan(0, 4));
int
型だけでなく、short
,ushort
からlong
,ulong
型までサポートされている。このクラスは
System.Buffers.Binary
名前空間にあり、.NET Core 2.1から使用できる。
.NET Frameworkを利用している場合、System.Memory
をNuGetで入手すれば使用できるようだ。
(System.Buffers
ではない。)
- 投稿日:2020-09-15T15:01:20+09:00
[.NET] バイトオーダーを指定して整数型とバイナリとを変換するには
Question
ある整数型をリトルエンディアン(あるいはビッグエンディアン)で
byte
配列に書き込みたい。
または、byte
配列に書き込まれているリトルエンディアン(あるいはビッグエンディアン)の整数型を復元したい。
どうすればいいか。
BitConverter
クラスのGetBytes
やToInt32
にはバイトオーダーを指定する方法はない。Answer
BinaryPrimitives
クラスを使用する。
ReadOnlySpan<byte>
から整数型(int
)を復元するにはReadInt32BigEndian
/ReadInt32LittleEndian
- 整数型(
int
)の内容をSpan<byte>
に書き出すにはWriteInt32BigEndian
/WriteInt32LittleEndian
メソッドを使用する。
int i1 = 12345678; var buffer = new byte[255]; BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(0, 4), i1); int i2 = BinaryPrimitives.ReadInt32LittleEndian(buffer.AsSpan(0, 4));
int
型だけでなく、short
,ushort
からlong
,ulong
型までサポートされている。このクラスは
System.Buffers.Binary
名前空間にあり、.NET Core 2.1から使用できる。
.NET Frameworkを利用している場合、System.Memory
をNuGetで入手すれば使用できるようだ。
(System.Buffers
ではない。)
- 投稿日:2020-09-15T15:01:20+09:00
[.NET] バイトオーダーを指定して整数とバイナリとを変換するには
Question
ある整数型をリトルエンディアン(あるいはビッグエンディアン)で
byte
配列に書き込みたい。
または、byte
配列に書き込まれているリトルエンディアン(あるいはビッグエンディアン)の整数型を復元したい。
どうすればいいか。
BitConverter
クラスのGetBytes
やToInt32
にはバイトオーダーを指定する方法はない。Answer
BinaryPrimitives
クラスを使用する。
ReadOnlySpan<byte>
から整数型(int
)を復元するにはReadInt32BigEndian
/ReadInt32LittleEndian
- 整数型(
int
)の内容をSpan<byte>
に書き出すにはWriteInt32BigEndian
/WriteInt32LittleEndian
メソッドを使用する。
int i1 = 12345678; var buffer = new byte[255]; BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(0, 4), i1); int i2 = BinaryPrimitives.ReadInt32LittleEndian(buffer.AsSpan(0, 4));
int
型だけでなく、short
,ushort
からlong
,ulong
型までサポートされている。このクラスは
System.Buffers.Binary
名前空間にあり、.NET Core 2.1から使用できる。
.NET Frameworkを利用している場合、System.Memory
をNuGetで入手すれば使用できるようだ。
(System.Buffers
ではない。)
- 投稿日:2020-09-15T14:00:56+09:00
【C#】アクセシビリティーに一貫性がありません でちょっと詰まった話
C#でリソースを管理するクラスを作った時に
『CS0053 アクセシビリティに一貫性がありません。
プロパティ型 'Resources' のアクセシビリティはプロパティ 'ResourceManager.Resources' よりも低く設定されています。』
というエラーが出た。コードとしては以下のような感じ。
リソース管理クラスpublic class ResourceManager : INotifyPropertyChanged { /// <summary> /// インスタンス /// </summary> public static ResourceManager Current { get; } = new ResourceManager(); /// <summary> /// 多言語化されたリソース /// </summary> public Resources Resources { get; } = new Resources(); /// ここから下は関係ないので省略 }エラーを見た時はResourceManagerクラスもResourcesプロパティも
『public』なので問題無いのに何故?と思ってたけどよくよくエラーを見ると
プロパティの方ではなくでResourcesクラスがダメだって書いてる。リソースのアクセス修飾子が『internal』になってる。
それを『public』として公開したのでエラーになっていたというオチ。
- 投稿日:2020-09-15T13:52:04+09:00
WPFのListViewで選択時のスタイルを変更する
はじめに
ListViewで表示しているテンプレートの選択時のスタイルを変更したかったけど、意外と記事が少ないので残しておくことにしました。
環境
Windows 10
Visual Studio 2017完成系
完成系といったけど、関係するところだけ抜粋。
<Window.Resources> <ResourceDictionary> <Style x:Key="listBoxItemStyle" TargetType="{x:Type ListBoxItem}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ContentControl}"> <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> <ContentPresenter /> </Border> </ControlTemplate> </Setter.Value> </Setter> <Style.Triggers> <Trigger Property="IsSelected" Value="True"> <Setter Property="Background" Value="Transparent" /> <Setter Property="BorderBrush" Value="Silver" /> <Setter Property="BorderThickness" Value="10" /> </Trigger> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="Background" Value="Transparent" /> <Setter Property="Opacity" Value="0.5" /> </Trigger> </Style.Triggers> </Style> </ResourceDictionary> </Window.Resources><ListView> <ListView.ItemContainerStyle> <Style BasedOn="{StaticResource listBoxItemStyle}" TargetType="ListBoxItem" /> </ListView.ItemContainerStyle> </ListView>テンプレートのスタイルを適用
Borderの背景、ブラシ、線の太さはテンプレートの設定に準拠しますという宣言。
<Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ContentControl}"> <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> <ContentPresenter /> </Border> </ControlTemplate> </Setter.Value> </Setter>実際に選択時のスタイルを決める
IsSelected
が選択時のスタイル。
IsMouseOver
がマウスオーバー時のスタイル。この指定だと、マウスオーバー時はちょっと透明に、選択時はグレーの枠線が表示されます。
<Style.Triggers> <Trigger Property="IsSelected" Value="True"> <Setter Property="Background" Value="Transparent" /> <Setter Property="BorderBrush" Value="Silver" /> <Setter Property="BorderThickness" Value="10" /> </Trigger> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="Background" Value="Transparent" /> <Setter Property="Opacity" Value="0.5" /> </Trigger> </Style.Triggers>リストビューのスタイルに設定
BasedOnでStaticResourceを設定してOK。
<ListView> <ListView.ItemContainerStyle> <Style BasedOn="{StaticResource listBoxItemStyle}" TargetType="ListBoxItem" /> </ListView.ItemContainerStyle> </ListView>
- 投稿日:2020-09-15T13:10:03+09:00
WPFでImageのSourceにパスを指定していたら削除できなかった話
TL;DR
- ListViewでとあるフォルダ内の画像一覧を表示しようとした
- WPFのImageのSourceにBindingで画像そのもののパスを与えた
- 表示を終わった時点で使った画像を削除したかったけど、削除できなかった
- 画像のパスじゃなくて、BitmapImageを与えるようにしたら解決
環境
Windows 10
Visual Studio 2017最初
MainWindow.xaml<Image Margin="10,20,10,20" Source="{Binding ImgPath}" />MainWindowViewModel.cspublic class ThumbnailImage : BindableBase, IThumbnailImage { private string_imgPath; public string ImgPath { get { return this._imgPath; } set { SetProperty(ref this._imgPath, value); } } public ThumbnailImage(string filePath) { this.ImgPath = filePath; } }最終
MainWindow2.xaml<Image Margin="10,20,10,20" Source="{Binding ImgPath}" />cspublic class ThumbnailImage : BindableBase, IThumbnailImage { private BitmapImage _imgPath; public BitmapImage ImgPath { get { return this._imgPath; } set { SetProperty(ref this._imgPath, value); } } public ThumbnailImage(string filePath) { this.ImgPath = CreateBitmapImage(filePath); } public static BitmapImage CreateBitmapImage(string filePath) { BitmapImage img = new BitmapImage(); img.BeginInit(); img.CacheOption = BitmapCacheOption.OnLoad; // ←ここが重要 img.UriSource = new System.Uri(filePath); img.EndInit(); return img; } }
CacheOption
をBitmapCacheOption.OnLoad
にすると、読み込み後に画像ファイルを占有しなくなるそうです。
- 投稿日:2020-09-15T00:47:39+09:00
DateTime要素を持つC#のリストで、最新の計測タイムより〇秒前の時刻を持つ要素を全て削除する
背景
List<Class>
型のコレクションの要素にDateTime型があり、
”最新の計測タイムより〇秒前の時刻を持つ要素を全て削除する”
がしたかったのでそのメモ。
WPFで書いてるから余計な部分も少しあるけどご愛敬。準備
ExampleList.csclass ExampleList { public int Number { get; set; } public DateTime TimeStamp { get; set; } }リストに使うクラスを作成
MainWindow.csprivate List<ExampleList> _list = new List<ExampleList>(); private DateTime Dt; //最新の時刻が入る public MainWindow() { InitializeComponent(); ListAdd(); Console.WriteLine("削除前"); ListCheck(); Console.WriteLine("Dt(最新の時刻):" + Dt); } private void ListAdd() { DateTime dt = DateTime.Now; for (int i = 1; i <= 10; i++) { DateTime dt2 = dt.AddSeconds(i); //dt + i 秒 _list.Add(new ExampleList() { Number = i, TimeStamp = dt2 }); Dt = dt2; } } private void ListCheck() { foreach (ExampleList el in _list) Console.WriteLine("ナンバー:{0} 時刻{1}", el.Number, el.TimeStamp); }
ListAdd
メソッドでlistに項目を追加します。
ListCheck
メソッドでlistの中身を確認します。
実行すると削除前 ナンバー:1 時刻2020/09/15 0:25:16 ナンバー:2 時刻2020/09/15 0:25:17 ナンバー:3 時刻2020/09/15 0:25:18 ナンバー:4 時刻2020/09/15 0:25:19 ナンバー:5 時刻2020/09/15 0:25:20 ナンバー:6 時刻2020/09/15 0:25:21 ナンバー:7 時刻2020/09/15 0:25:22 ナンバー:8 時刻2020/09/15 0:25:23 ナンバー:9 時刻2020/09/15 0:25:24 ナンバー:10 時刻2020/09/15 0:25:25 Dt(最新の時刻):2020/09/15 0:25:25こんな感じで10個のリスト項目ができました。
指定した秒数より前の要素を削除する
本題です。
指定した秒数より前の時刻を持つリスト要素を削除するメソッドRemove
を追加します。MainWindow.csprivate void Remove() { int s = -5; //秒数の指定 DateTime ago = Dt.AddSeconds(s); //最新の計測からs秒前の時刻 int result = _list.FindLastIndex(t => t.TimeStamp <= ago); //{リストの中で指定秒数前の値を持つ項目}のうち最も大きいインデックス番号を検索 _list.RemoveRange(0, result); //リストのインデックス 0~result を削除 }
MainWindowメソッドから実行しますMainWindow.cspublic MainWindow() { InitializeComponent(); ListAdd(); Console.WriteLine("削除前"); ListCheck(); Console.WriteLine("Dt(最新の時刻):" + Dt); Remove(); Console.WriteLine("削除後"); ListCheck(); }
実行結果削除前 ナンバー:1 時刻2020/09/15 0:25:16 ナンバー:2 時刻2020/09/15 0:25:17 ナンバー:3 時刻2020/09/15 0:25:18 ナンバー:4 時刻2020/09/15 0:25:19 ナンバー:5 時刻2020/09/15 0:25:20 ナンバー:6 時刻2020/09/15 0:25:21 ナンバー:7 時刻2020/09/15 0:25:22 ナンバー:8 時刻2020/09/15 0:25:23 ナンバー:9 時刻2020/09/15 0:25:24 ナンバー:10 時刻2020/09/15 0:25:25 Dt(最新の時刻):2020/09/15 0:25:25 削除後 ナンバー:5 時刻2020/09/15 0:25:20 ナンバー:6 時刻2020/09/15 0:25:21 ナンバー:7 時刻2020/09/15 0:25:22 ナンバー:8 時刻2020/09/15 0:25:23 ナンバー:9 時刻2020/09/15 0:25:24 ナンバー:10 時刻2020/09/15 0:25:25指定した秒数より前の時刻を持つリスト要素を削除することができました。
最終的なコード全体
class ExampleList
は最初から変わっていないので省きます。MainWindow.cspublic partial class MainWindow : Window { private List<ExampleList> _list = new List<ExampleList>(); private DateTime Dt; //最新の時刻が入る public MainWindow() { InitializeComponent(); ListAdd(); Console.WriteLine("削除前"); ListCheck(); Console.WriteLine("Dt(最新の時刻):" + Dt); Remove(); Console.WriteLine("削除後"); ListCheck(); } private void ListAdd() { DateTime dt = DateTime.Now; for (int i = 1; i <= 10; i++) { DateTime dt2 = dt.AddSeconds(i); //dt + i 秒 _list.Add(new ExampleList() { Number = i, TimeStamp = dt2 }); Dt = dt2; } } private void Remove() { int s = -5; //秒数の指定 DateTime ago = Dt.AddSeconds(s); //最新の計測からs秒前の時刻 int result = _list.FindLastIndex(t => t.TimeStamp <= ago); //{リストの中で指定秒数前の値を持つ項目}のうち最も大きいインデックス番号を検索 _list.RemoveRange(0, result); //リストのインデックス 0~result を削除 } private void ListCheck() { foreach (ExampleList el in _list) Console.WriteLine("ナンバー:{0} 時刻{1}", el.Number, el.TimeStamp); } }