#進捗表示とビジネスロジックを分離する
重い処理に対して、どのくらいまで処理を行ったかを知りたい場合がある。ある人にバッチでコンソールに進捗表示をさせる処理の作成を頼んだ結果、サンプルコードのようなコードを作成していた。話を聞いてみると、オブザーバーパターンで実装したとのこと。実装はともかく考え方は参考になったので、サンプルコードで考察してみる。
##サンプルコード
Somethingクラス内に進捗表示をするため、progressCounterを渡している。
進捗表示:ProgressCounter(ConsoleTestプロジェクト)
ビジネスロジック:Something.Do(Qiitaプロジェクト)
※ConsoleTestからQiitaプロジェクトを参照している。
void Main()
{
var something = new Something();
var progressCounter = new ProgressCounter();
something.Do(progressCounter);
}
public class Something
{
public void Do(IProgressCounter progressCounter)
{
int maxCount = 10;
for(int i = 0; i < maxCount; i++)
{
// なんらかの処理
// 進捗表示
if (progressCounter != null)
{
progressCounter.Inc();
progressCounter.Display();
}
}
}
}
class ProgressCounter : IProgressCounter
{
private int _count = 0;
public void Display()
{
Console.WriteLine(_count);
}
public void Inc()
{
_count++;
}
}
public interface IProgressCounter
{
void Display();
void Inc();
}
##考察
このコードはビジネスロジック側に暗黙的に影響を与えている。
ProgressCounerが処理件数を持っており、Something.Doと処理件数が一致していなければいけない。一ループ、一件の前提が必要でありcontiuneを入れてスキップをいれただけで件数が一致しなくなる。
処理件数はDoから渡すだけでよく、2重持ちをする必要はない。
一方、インターフェースにしたことで、進捗表示はコンソール以外でも対応可能である。Windows FormのProgressBarを使用しても、ビジネスロジックには何の影響もない。この点は評価に値する。
IProgressCounterを関数の引数にしているが、ビジネスロジックで使用する引数のみにするべきである。依存性の注入のほうが良い。
##リファクタリング後のコード
明示的な開始と終了処理が必要だと思うので、Start,End関数を追加している。また、NullObjectパターンを前提にしているので、Nullチェックはしない。
コーディングの都合上、改良後のコードは別フォルダに配置しているので、Modfy.ProgressCounterやQiita.CounterOK.Somethingという名前空間にしている。
尚、マルチスレッド化は考えず、元のコードを活かすようにした。
void ModifiedMain()
{
// 改良後
var progressCounter = new Modify.ProgressCounter();
var something = new Qiita.CounterOK.Something(progressCounter);
something.Do();
}
public class Something
{
private readonly IProgressCounter _progressCounter = null;
public Something(IProgressCounter progressCounter)
{
_progressCounter = progressCounter;
}
public void Do()
{
int maxCount = 10;
// nullの代わりにNullObjectを渡してくる前提のため、nullチェックはしない。
_progressCounter.MaxCount = maxCount;
_progressCounter.Start();
for (int i = 0; i < maxCount; i++)
{
// なんらかの処理
// 進捗表示
_progressCounter.Update(i + 1);
}
_progressCounter.End();
}
}
public interface IProgressCounter
{
int MaxCount { get; set; }
void Start();
void Update(int count);
void End();
}
public class ProgressCounter : IProgressCounter
{
public int MaxCount { get; set; }
public void Start()
{
System.Console.WriteLine("処理開始");
}
public void Update(int count)
{
Console.WriteLine(count + "/" + MaxCount);
}
public void End()
{
System.Console.WriteLine("処理完了");
}
}
##まとめ
リファクタリングすることで、改良前よりはビジネスロジックへの影響は少なくなった。ビジネスロジック内に進捗表示処理が残ってしまうのは仕方ない。
本来は自由につけはずしができるようなクラス設計が望ましい。
つけはずしであれば、デコレーターでDecorator.DoをSomething.Doに被せることは可能だが、シグネチャを合わせないといけないので現実的ではない。
##サンプルコード Java 2017/3/24 追記
void main() {
Something something = new Something();
ProgressCounter progressCounter = new ProgressCounter();
something.Do(progressCounter);
}
public class Something {
public void Do(IProgressCounter progressCounter) {
int maxCount = 10;
for (int i = 0; i < maxCount; i++) {
// なんらかの処理
// 進捗表示
if (progressCounter != null) {
progressCounter.inc();
progressCounter.display();
}
}
}
}
public interface IProgressCounter {
void display();
void inc();
}
public class ProgressCounter implements IProgressCounter {
private int count = 0;
@Override
public void display() {
System.out.println(count);
}
@Override
public void inc() {
count++;
}
}
##リファクタリング後 Java
void main() {
ProgressCounter progressCounter = new ProgressCounter();
Something something = new Something(new ProgressCounter());
something.Do();
}
public class Something {
private final IProgressCounter progressCounter;
public Something(IProgressCounter progressCounter) {
this.progressCounter = progressCounter;
}
public void Do() {
int maxCount = 10;
// nullの代わりにNullObjectを渡してくる前提のため、nullチェックはしない。
progressCounter.setMaxCount(maxCount);
progressCounter.start();
for (int i = 0; i < maxCount; i++) {
// なんらかの処理
// 進捗表示
progressCounter.update(i + 1);
}
progressCounter.end();
}
}
public interface IProgressCounter {
int getMaxCount();
void setMaxCount(int maxCount);
void start();
void update(int count);
void end();
}
public class ProgressCounter implements IProgressCounter {
private int maxCount = 0;
@Override
public int getMaxCount() {
return maxCount;
}
@Override
public void setMaxCount(int maxCount) {
this.maxCount = maxCount;
}
@Override
public void start() {
System.out.println("処理開始");
}
@Override
public void update(int count) {
System.out.println(count + "/" + maxCount);
}
@Override
public void end() {
System.out.println("処理完了");
}
}