Lecture 15

Last Time: Shared_ptr, Decorator pattern This Time: Observer Pattern, Casting

Design Patterns: Effective solutions to common problems.

Observer pattern: Publisher / Subscriber relationship.

Publisher: Generates some data, change in state Subscriber: Can dynamically subscribe or unsubscribe for various publishers

Should react when data is changed.

Example: Spreadsheet application.

Publishers: Cells. (Subjects) Subscribers: Charts (Observers)

When a cell changes, charts that are observing that cell must re-render, display new information.

May have different types of publishers / subscribers, e.g. - different charts require different logic for re-rendering.

  • Subject has Observers
  • Observer has a reference to subject

Steps:

  1. ConcreteSubject has its state updated
  2. notifyObservers is called - either by ConcreteSubject or some controller (like the main function)
  3. notify is called on each observer in Subject’s observer list
  4. This calls ConcreteSubject::notify, by the assumption it is pure virtual
  5. ConcreteObserver calls getState on ConcreteSubject (notify observers), uses info as necessary

Example: Twitter clone. Tweeters are subjects. Followers which are observers, can only follow one Tweeter.

class Subject {
	vector<Observer*> observers;
	public:
		void attach(Observer* o){observers.push_back(o);}
		void detach(Observer* o){//remove from observers;}
		void notifyObservers() {
			for(observer* o: observers) o->update();
		}
		virtual ~Subject() = 0; // need to provide impl. outside of the class
};
subject::~Subject(){}
 
class Observer {
	public:
		virtual void update()=0;
		virtual ~Observer(){}
};
 
class Tweeter: public Subject {
	ifstream in;
	string tweet;
	public:
		Tweeter(const string& fileName): in{fileName}{}
		bool tweet() {
			getline(in tweet);
			return in.good();
		}
		string getState(){return tweet;}
};
 
class Follower: public Observer {
	Tweeter* iFollow;
	string myName;
	public:
		Follower(Tweeter* t, string s):iFollow{t}, myName{s} {
			iFollow->attach(this); // put in the Observers in the vector?
		}
		~Follower(){
			iFollow->detach(this);
		}
		void update(){
			string tweet = iFollow->getState();
			if(tweet.find(myName)!= string::npos){
				cout << "They said my name!"
			}else {
				cout << ":(" << endl;
			}
		}
};
 
int main() {
	Tweeter elon{"elon.txt"};
	Follow joe{&elon, "joe"};
	Follower jane{&elon, "jane"};
	
	while(elon.tweet()){
		elon.notifyObservers();
	}
	
}

Note: this only works because destruction happens in reverse order!

Also Note: Design patterns we demonstrate - simplistic implementations. Only get value at scale - multiple types of subjects/observers. Other variants also exists - for example: passing a Subject& via notify method. (LOOK INTO THAT)

That’s all you need to complete assignment 3. Review assignment 3 question 3, with the song library. Implemented with Observer pattern.


Casting

Casting allows us to take one type, treat as another.

In C:

Node n;
int* ip = (int*) &n;
  • (int*) is a casting operator - treat this as an int*

This is dangerous., generally avoid casting unless necessary, subverts type system.

If necessary, use one of the 4 C++ casts instead of C-style casting.

  1. static_cast - “well-defined” conversions between two types e.g:
void g(float f);
void g(int n);
float f;
g(static_cast<int>(f));
  • g calls int version of g

Another example:

Book* b = ...;
Text* t = static_cast<Text*>(b);
  • Trust me bro, I know it’s a Text.
  • Undefined behaviour if it’s not actually pointing at a Text
  1. reinterpret_cast - allows for poorly-defined, implementation dependent casts. Most uses of reinterpret_cast are undefined behaviour
Rational r;
Node* n = reinterpret_cast<Node*> (&r);
  1. const_cast

  2. dynamic_cast

Next: CS247 Lecture 16