add Observer interface

This commit is contained in:
falsycat 2023-07-09 16:10:48 +09:00
parent 99379b19f9
commit b93fcb6ac7
4 changed files with 221 additions and 0 deletions

View File

@ -9,5 +9,21 @@ target_sources(nf7_iface
PRIVATE
version.cc
PUBLIC
observer.hh
version.hh
)
add_executable(nf7_iface_test)
target_sources(nf7_iface_test
PRIVATE
observer_test.hh
observer_test.cc
)
target_link_libraries(nf7_iface_test
PRIVATE
nf7_iface
GTest::gmock_main
GTest::gtest_main
)
include(GoogleTest)
gtest_discover_tests(nf7_iface_test)

99
iface/observer.hh Normal file
View File

@ -0,0 +1,99 @@
// No copyright
// ---- this header provides a way to implement Observer pattern easily
#pragma once
#include <cassert>
#include <unordered_set>
#include <utility>
namespace nf7::iface {
// T is notified to Observer<T> by Observer<T>::Target.
// All observers should be based on this.
template <typename T>
class Observer {
public:
class Target;
inline explicit Observer(Target&);
inline virtual ~Observer() noexcept;
protected:
virtual void Notify(const T&) {}
virtual void NotifyWithMove(T&& v) { Notify(v); }
virtual void NotifyDestruction(const T* = nullptr) {}
private:
Target& target_;
};
// All objects which can be observed by Observer<T> must be based on this.
template <typename T>
class Observer<T>::Target {
public:
friend class Observer<T>;
Target() = default;
virtual ~Target() noexcept {
for (auto obs : obs_) {
obs->NotifyDestruction();
}
}
Target(const Target&) = delete;
Target(Target&&) = delete;
Target& operator=(const Target&) = delete;
Target& operator=(Target&&) = delete;
protected:
void Notify(T&& v) noexcept {
assert(!calling_observer_ && "do not call Notify from observer callback");
if (1 == obs_.size()) {
auto first = *obs_.begin();
calling_observer_ = true;
first->NotifyWithMove(std::move(v));
calling_observer_ = false;
return;
}
calling_observer_ = true;
for (auto obs : obs_) {
obs->Notify(v);
}
calling_observer_ = false;
}
bool observed(const T* = nullptr) const noexcept { return !obs_.empty(); }
private:
void Register(Observer<T>& obs) {
assert(!calling_observer_ &&
"do not register any observer from observer callback");
obs_.insert(&obs);
}
void Unregister(const Observer<T>& obs) noexcept {
assert(!calling_observer_ &&
"do not unregister any observer from observer callback");
obs_.erase(&const_cast<Observer<T>&>(obs));
}
private:
std::unordered_set<Observer<T>*> obs_;
bool calling_observer_ = false;
};
template <typename T>
Observer<T>::Observer(Target& target) : target_(target) {
target_.Register(*this);
}
template <typename T>
Observer<T>::~Observer() noexcept {
target_.Unregister(*this);
}
} // namespace nf7::iface

77
iface/observer_test.cc Normal file
View File

@ -0,0 +1,77 @@
// No copyright
#include "iface/observer.hh"
#include "iface/observer_test.hh"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <cstdint>
#include <optional>
using namespace nf7::iface;
using namespace nf7::iface::test;
TEST(Observer, NotifyWithMove) {
ObserverTargetMock<int32_t> target;
ObserverMock<int32_t> sut {target};
EXPECT_CALL(sut, NotifyWithMove(111)).Times(1);
target.Notify(int32_t {111});
}
TEST(Observer, NotifyWithRef) {
ObserverTargetMock<int32_t> target;
ObserverMock<int32_t> sut1 {target};
ObserverMock<int32_t> sut2 {target};
EXPECT_CALL(sut1, Notify(111)).Times(1);
EXPECT_CALL(sut2, Notify(111)).Times(1);
target.Notify(int32_t {111});
}
TEST(Observer, NotifyDestruction) {
std::optional<ObserverTargetMock<int32_t>> target;
target.emplace();
ObserverMock<int32_t> sut {*target};
EXPECT_CALL(sut, NotifyDestruction(testing::_)).Times(1);
target = std::nullopt;
}
#if !defined(NDEBUG)
TEST(Observer, DeathByRegisterInCallback) {
nf7::test::ObserverTargetMock<int32_t> target;
nf7::test::ObserverMock<int32_t> sut {target};
ON_CALL(sut, NotifyWithMove(testing::_)).WillByDefault([&](auto&&) {
nf7::test::ObserverMock<int32_t> sut2 {target};
(void) sut2;
});
ASSERT_DEATH_IF_SUPPORTED(target.Notify(111), "");
}
TEST(Observer, DeathByUnregisterInCallback) {
nf7::test::ObserverTargetMock<int32_t> target;
std::optional<nf7::test::ObserverMock<int32_t>> sut;
sut.emplace(target);
ON_CALL(*sut, NotifyWithMove(testing::_)).WillByDefault([&](auto&&) {
sut = std::nullopt;
});
ASSERT_DEATH_IF_SUPPORTED(target.Notify(111), "");
}
TEST(Observer, DeathByNotifyInCallback) {
nf7::test::ObserverTargetMock<int32_t> target;
nf7::test::ObserverMock<int32_t> sut {{target}};
ON_CALL(sut, NotifyWithMove(testing::_)).WillByDefault([&](auto&&) {
target.Notify(111);
});
ASSERT_DEATH_IF_SUPPORTED(target.Notify(111), "");
}
#endif

29
iface/observer_test.hh Normal file
View File

@ -0,0 +1,29 @@
// No copyright
#pragma once
#include "iface/observer.hh"
#include <gmock/gmock.h>
namespace nf7::iface::test {
template <typename T>
class ObserverMock : public Observer<T> {
public:
explicit ObserverMock(Observer<T>::Target& target) : Observer<T>(target) {
}
MOCK_METHOD1(Notify, void(const T&));
MOCK_METHOD1(NotifyWithMove, void(T&&));
MOCK_METHOD1(NotifyDestruction, void(const T*));
};
template <typename T>
class ObserverTargetMock : public Observer<T>::Target {
public:
using Observer<T>::Target::Target;
using Observer<T>::Target::Notify;
};
} // namespace nf7::iface::test