はじめに
前回の続き、データ・バインディングのおはなしです。前回こしらえたCounterAppではカウンタのUIを司るMainPage(View)がカウンタの実体(Model):Counterをメンバに持ち、ボタンのイベント・ハンドラ内でCounterを直接駆動してました。
今回はこの実装をベースに、view-Model間の仲介役:ViewModelを両者間に差し込むことでViewとModelを分離します。こうすることでView/Modelそれぞれに対する拡張や変更が他方に及ぼす影響を小さく抑えることができます。画面デザイナとプログラマがそれぞれの作業を同時進行できるってわけ。
前準備:Counterの機能拡張
その前に、いま一つ面白味に欠けるCounterに少しばかりの拡張:下限値/上限値設定機能を追加しておきますか。increment/decrementし放題な従来のカウンタはそのまま残しておいて、両者を好きに差し替えられるよう手を加えます。
// CounterLib.h
#pragma once
namespace CounterLib {
// 抽象ベースクラス
class Counter {
public:
Counter(int initial) : count_(initial) {}
int get() const { return count_; }
void set(int n) { count_ = n; }
// ひとつ ふやす/へらす
void increment() { do_increment(); }
void decrement() { do_decrement(); }
// ふやせる/へらせる か?
bool can_increment() const { return do_can_increment(); }
bool can_decrement() const { return do_can_decrement(); }
protected:
virtual void do_increment() =0;
virtual void do_decrement() =0;
virtual bool do_can_increment() const =0;
virtual bool do_can_decrement() const =0;
private:
int count_;
};
// フツーのカウンタ
class DefaultCounter : public Counter {
public:
DefaultCounter(int initial) : Counter(initial) {}
protected:
virtual void do_increment();
virtual void do_decrement();
virtual bool do_can_increment() const;
virtual bool do_can_decrement() const;
};
// 下限値/上限値つきカウンタ
class BoundedCounter : public Counter {
public:
BoundedCounter(int lo, int hi, int initial);
BoundedCounter(int lo, int hi);
protected:
virtual void do_increment();
virtual void do_decrement();
virtual bool do_can_increment() const;
virtual bool do_can_decrement() const;
private:
int lo_;
int hi_;
};
}
// CoutnerLib.cpp
#include "pch.h"
#include <cassert>
#include "CounterLib.h"
namespace CounterLib {
// フツーのカウンタ
void DefaultCounter::do_increment() { set(get()+1); }
void DefaultCounter::do_decrement() { set(get()-1); }
bool DefaultCounter::do_can_increment() const { return true; }
bool DefaultCounter::do_can_decrement() const { return true; }
// 下限値/上限値つきカウンタ
BoundedCounter::BoundedCounter(int lo, int hi, int initial) : Counter(initial), lo_(lo), hi_(hi) {
assert( lo <= initial );
assert( initial <= hi );
}
BoundedCounter::BoundedCounter(int lo, int hi) : Counter((lo+hi)/2), lo_(lo), hi_(hi) {
assert( lo <= hi );
}
// ふやせるならふやす
void BoundedCounter::do_increment() {
if ( can_increment() ) {
set(get()+1);
}
}
// へらせるならへらす
void BoundedCounter::do_decrement() {
if ( can_decrement() ) {
set(get()-1);
}
}
// 上限値に達していたらfalse
bool BoundedCounter::do_can_increment() const {
return get() < hi_;
}
// 下限値に達していたらfalse
bool BoundedCounter::do_can_decrement() const {
return lo_ < get();
}
}
DefaultCoutner/BoundedCounterそれぞれのテストを用意し、動作確認しておきます。
// DefaultCounterTest.cpp
#include "pch.h"
#include "CppUnitTest.h"
#include "CounterLib/CounterLib.h" // 追加
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
namespace DefaultCounterTest {
TEST_CLASS(DefaultCounterTest) {
public:
// 各テストの開始直前に行う
TEST_METHOD_INITIALIZE(method_setup) {
counter_ = new CounterLib::DefaultCounter(0);
Assert::IsNotNull(counter_);
}
// 各テストの終了直後に行う
TEST_METHOD_CLEANUP(method_teardown) {
delete counter_;
}
// 初期状態でのカウント値は0
TEST_METHOD(constructor) {
Assert::AreEqual(0, counter_->get());
}
// incrementのたびにカウント値が+1される
TEST_METHOD(increment) {
for ( int i = 0; i < 10; ++i ) {
Assert::AreEqual(i, counter_->get());
counter_->increment();
}
}
// decrementのたびにカウント値が-1される
TEST_METHOD(decrement) {
for ( int i = 0; i < 10; ++i ) {
Assert::AreEqual(-i, counter_->get());
counter_->decrement();
}
}
private:
CounterLib::Counter* counter_;
};
}
