LoginSignup
10
13

More than 5 years have passed since last update.

ビジネスロジックに影響を与えない進捗表示 - C#,Java

Last updated at Posted at 2017-03-20

進捗表示とビジネスロジックを分離する

重い処理に対して、どのくらいまで処理を行ったかを知りたい場合がある。ある人にバッチでコンソールに進捗表示をさせる処理の作成を頼んだ結果、サンプルコードのようなコードを作成していた。話を聞いてみると、オブザーバーパターンで実装したとのこと。実装はともかく考え方は参考になったので、サンプルコードで考察してみる。

サンプルコード

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("処理完了");
    }

}

目次

10
13
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
13