What design pattern to handle this?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <string>
#include <list>
#include <vector>
#include <set>
#include <array>

struct Person;

struct State {virtual ~State() = default;};

struct StateA : State {
	std::vector<Person*> enemies;
};

struct StateB : State {
	std::set<Person*> prospectiveEmployers;
};

struct StateC : State {
	std::list<Person*> secretCrushes;
};

struct Person {
	std::string name;
	std::array<State*, 3> states;  // necessary Finite State Machine in my program (here it is simplified)
	Person (const std::string& n) : name(n) {states[0] = new StateA;  states[1] = new StateB;  states[2] = new StateC;}
	void insertEnemy (Person* p) {static_cast<StateA*>(states[0])->enemies.push_back(p);}
	void insertProspectiveEmployer (Person* p) {static_cast<StateB*>(states[1])->prospectiveEmployers.insert(p);}
	void insertSecretCrush (Person* p) {static_cast<StateC*>(states[2])->secretCrushes.push_back(p);}
	void dies() {}  // What to put here?
};

int main() {
	Person *bob = new Person("Bob"), *sam = new Person("Sam"), *mary = new Person("Mary"), *kate = new Person("Kate");
	sam->insertEnemy(bob);
	mary->insertProspectiveEmployer(bob);
	kate->insertSecretCrush(bob);
	bob->dies();  // At this point sam, mary, and kate shall remove bob from their special lists.
}


Because bob dies, sam, mary, and kate must remove bob from their special lists within their states. Furthermore, at any time before bob dies, mary may stop looking for a job (i.e. her states[1] may be deleted and replaced by some other state). Similarly for sam and kate. So the removal may or may not be needed depending on what happens to sam, mary, and kate before bob dies.

What to put in the dies() function? At first, I thought of the Observer Pattern, but it doesn't help here (bob has no knowledge of sam, mary, and kate). I couldn't think of a Mediator Pattern to help either. Perhaps a unique pattern of some sort needs to be used?
Last edited on
Personally, I would use an event system. Let anyone find out about the life and death of certain kinds of objects. This requires that the object's dying breath will be a shout (e.g. calling the event handlers in the destructor). Unfortunately you can't make a base class to inherit from due to order of destruction - instead I would use reverse CRTP and make a template class that inherits the class you are interested in.

EDIT: JLBorges links to what I mean. I'm not good with terminology, by the way.
Last edited on
> At first, I thought of the Observer Pattern, but it doesn't help here (bob has no knowledge of sam, mary, and kate).

Publish and Subscribe (to me) is an extension of the Observer pattern (it is a decoupled version of Observer).
https://en.wikipedia.org/wiki/Publish/subscribe
Strive for a more flexible design. Something along these lines, perhaps:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#include <iostream>
#include <string>
#include <map>
#include <list>
#include <functional>

struct person ;

struct publisher
{
    void publish_obituary( person* deceased ) { for( auto fn : subscribers ) if(fn) fn(deceased) ; }

    using list_type = std::list< std::function< void( person* ) > > ;
    class token { list_type::iterator iter ; token( list_type::iterator iter ) : iter(iter) {} friend struct publisher ; };

    template < typename FN >
    token subscribe( FN&& fn ) { return subscribers.emplace( subscribers.end(), std::forward<FN>(fn) ) ; }

    void unsubscribe( token tok ) { subscribers.erase( tok.iter ) ; }

    private: list_type subscribers ;
};


struct person {

    enum relation_type { ENEMY, PROSPECTIVE_EMPLOYER, SECRET_CRUSH, AQUAINTANCE, MENTOR };

    explicit person( const std::string& n ) 
        : name(n), token( announcer.subscribe( std::bind( &person::handle_death, this, std::placeholders::_1 ) ) ) {}
    ~person() { announcer.unsubscribe(token) ; }

    std::multimap< person*, relation_type > related_persons ;
    void add_relation( person* who, relation_type what ) { if(who) related_persons.emplace(who,what) ; }

    void handle_death( person* who ) {

        if( who != this ) {
    	    const auto n = related_persons.erase(who) ;
    	    if(n) {
                std::clog << name << ": removed '" << who->name << "' from " ;
    	        if(n>1) std::clog << n << " relations\n" ;
    	        else std::clog << "one relation\n" ;
    	    }
        }
    }

    void dies() { dead = true ; announcer.publish_obituary(this) ; }

    static publisher announcer ;

    const std::string name;
    const publisher::token token ;
    private: bool dead = false ;
};

publisher person::announcer ;

int main() {
    
    const auto coroner = [] ( person* who, char begins_with, const char* name ) {
        if( !who->name.empty() && who->name[0] == begins_with )
            std::clog << "coroner '" << name << "' certifies " << who->name << "'s death.\n" ; };
                                     
    person::announcer.subscribe( std::bind( coroner, std::placeholders::_1, 'B', "Svetlana" ) ) ;
    person::announcer.subscribe( std::bind( coroner, std::placeholders::_1, 'M', "Eliyahu" ) ) ;
    person::announcer.subscribe( std::bind( coroner, std::placeholders::_1, 'S', "Eliyahu" ) ) ;

    person bob_("Bob"), sam("Sam"), mary("Mary"), kate("Kate");
    auto bob = std::addressof(bob_) ;

    sam.add_relation( bob, person::ENEMY ) ;
    mary.add_relation( bob, person::PROSPECTIVE_EMPLOYER ) ;
    kate.add_relation( bob, person::SECRET_CRUSH ) ;
    sam.add_relation( bob, person::PROSPECTIVE_EMPLOYER ) ;
    sam.add_relation( bob, person::AQUAINTANCE ) ;
    mary.add_relation( bob, person::MENTOR ) ;
    sam.add_relation( std::addressof(mary), person::AQUAINTANCE ) ;

    bob->dies() ;
    std::clog << "--------------\n" ;
    mary.dies() ;
    std::clog << "--------------\n" ;
    sam.dies() ;
}

http://coliru.stacked-crooked.com/a/1cb85ce662d4855e
JLBorges, you're very kind for not only referring me to Publish and Subscribe, but for also writing out an explicit code. Most of your solutions change my design to solve the problem, so I suppose they are also meant to teach me how to design more flexibly to begin with. So far I've been able to adapt your previous solutions to meet whatever original designs I had because my original designs were needed for other aspects of my program. I will try to do the same thing here, because the Finite State Machines in my code above (simplified of course), are absolutely necessary in my program (it solves many problems, like concurrent states). Some complications are that some of the states are deleted due to new circumstances, making the person's death irrelevant to the owner of those changed states. Furthermore, some states must use std::list while some must use std::set, etc... for the people of interest. But I will give it a try. Instead of the simple line
 
const auto n = related_persons.erase(who) ;
I need to search for 'who' among all the container data members in each state. I hope it is as straightforward as I think it is.
Last edited on
Ok, I've been able to adapt JLBorges' solution to meet my original design with the Finite State Machines. I couldn't think of a way to carry out each state with a loop, so I simply checked each state one at a time. Not pretty, but I couldn't find any better way. I also removed the Publisher::Token proxy class because my solution apparently doesn't need it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#include <iostream>
#include <string>
#include <list>
#include <vector>
#include <set>
#include <array>
#include <memory>
#include <algorithm>

struct Person;

struct State {virtual ~State() = default;};

struct StateA : State {
	std::list<Person*> enemies;
};

struct StateB : State {};

struct UnemployedState : StateB {
	std::vector<Person*> prospectiveEmployers;
};

struct EmployedState : StateB {
	Person* boss = nullptr;
	EmployedState (Person* b) : boss(b) {}
};

struct StateC : State {
	std::set<Person*> secretCrushes;
};

class Publisher {
	private: 
		using list_type = std::list<std::function<void(Person*)>>;
		list_type subscribers;
	public:
		void announceDeath (Person* deceased) {for (auto f : subscribers) if(f) f(deceased);}
		template <typename F>
		void subscribe (F&& f) {subscribers.emplace (subscribers.end(), std::forward<F>(f));}
		void unsubscribe (list_type::iterator iter) {subscribers.erase(iter);}
};

class Person {
	private:
		std::string name;
		std::array<State*, 3> states;
	public:
		static Publisher publisher;
	public:
		Person (const std::string& n) : name(n), states ({new StateA, new UnemployedState, new StateC})
			{publisher.subscribe([this](Person* p)->void {handleDeath(p);});}
		std::string getName() const {return name;}
		void insertEnemy (Person* p) {dynamic_cast<StateA*>(states[0])->enemies.push_back(p);}
		void insertProspectiveEmployer (Person* p) {dynamic_cast<UnemployedState*>(states[1])->prospectiveEmployers.push_back(p);}
		void foundJob (Person* boss) {
			delete states[1];  states[1] = new EmployedState(boss);
			std::cout << "\n" << name << " has been hired by " << boss->getName() << ".\n";
		}
		void insertSecretCrush (Person* p) {dynamic_cast<StateC*>(states[2])->secretCrushes.insert(p);}
		void dies() {publisher.announceDeath(this);}
		void displayRelations() const {  // for checking only
			std::cout << std::endl << name << "'s relations:\n";
			StateA* a = dynamic_cast<StateA*>(states[0]);
			if(a) {std::cout << "Enemies:  ";  for (const Person* x : a->enemies) std::cout << x->name << "  ";  std::cout << std::endl;}
			UnemployedState* unemployed = dynamic_cast<UnemployedState*>(states[1]);
			if (unemployed) {
				std::cout << "Prospective Employers:  ";
				for (const Person* x : unemployed->prospectiveEmployers)  std::cout << x->name << "  ";  std::cout << std::endl;
			}
			else {
				EmployedState* employed = dynamic_cast<EmployedState*>(states[1]);
				if (employed && employed->boss) std::cout << "Boss: " << employed->boss->name << std::endl;
				else if (employed && !employed->boss) std::cout << "Boss: previous boss died and will soon be replaced by a new boss.\n";
			}
			StateC* c = dynamic_cast<StateC*>(states[2]);
			if(c) {std::cout << "Secret Crushes:  ";  for (const Person* x : c->secretCrushes) std::cout << x->name << "  ";  std::cout << std::endl;}
		}
	private:
	    void handleDeath (Person* deceased) {
	        if (deceased != this) {  // Could only think of checking each state one at a time, since each state is different.
	        	StateA* a = dynamic_cast<StateA*>(states[0]);
	        	if(a) a->enemies.remove(deceased);
	        	UnemployedState* unemployed = dynamic_cast<UnemployedState*>(states[1]);
	        	if(unemployed) {
	        		std::vector<Person*>& v = unemployed->prospectiveEmployers;
				v.erase(std::remove(v.begin(), v.end(), deceased), v.end());
			}
	        	else {
	        		EmployedState* employed = dynamic_cast<EmployedState*>(states[1]);
	        		if(employed && employed->boss == deceased) employed->boss = nullptr;
	        	}
	        	StateC* c = dynamic_cast<StateC*>(states[2]);
	        	if(c) c->secretCrushes.erase(deceased);
	        }
	    }
};
Publisher Person::publisher;

int main() {
	Person::publisher.subscribe ([](Person* deceased)->void {if (!deceased->getName().empty()) std::clog << "\n" << deceased->getName() << " has died.\n\n";});
	Person *bob = new Person("Bob"), *sam = new Person("Sam"), *mary = new Person("Mary"), *kate = new Person("Kate");
	sam->insertEnemy(bob);
	sam->insertEnemy(mary);
	sam->insertProspectiveEmployer(bob);
	sam->insertProspectiveEmployer(kate);
	mary->insertProspectiveEmployer(bob);
	kate->insertProspectiveEmployer(bob);
	kate->insertSecretCrush(bob);
	Person* peopleWhoKnowBob[] = {sam, mary, kate};

	for (const Person* x : peopleWhoKnowBob) x->displayRelations();
	sam->foundJob(kate);
	mary->foundJob(bob);
	bob->dies();
	for (const Person* x : peopleWhoKnowBob) x->displayRelations();
}

Output:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Sam's relations:
Enemies:  Bob  Mary
Prospective Employers:  Bob  Kate
Secret Crushes:

Mary's relations:
Enemies:
Prospective Employers:  Bob
Secret Crushes:

Kate's relations:
Enemies:
Prospective Employers:  Bob
Secret Crushes:  Bob

Sam has been hired by Kate.  // Bob's death is now irrelevant to his prospective employers list.

Mary has been hired by Bob.

Bob has died.


Sam's relations:
Enemies:  Mary
Boss: Kate
Secret Crushes:

Mary's relations:
Enemies:
Boss: previous boss died and will soon be replaced by a new boss.
Secret Crushes:

Kate's relations:
Enemies:
Prospective Employers:
Secret Crushes: 


Edit: I realize now that Publisher::Token is needed after all for other reasons not discussed in this problem. JLBorges the genius actually looked ahead!
Last edited on
Topic archived. No new replies allowed.