One thing that I really like using in C++ is interfaces. I wanted to write this up to demonstrate the use of interfaces and maybe help someone figure out when they should use them.
Lets say we have the following program:
#include <iostream>
class AddSomething {
public:
AddSomething() {}
void doOp(int i) { std::cout << "10 + " << i << " = " << 10 + i << std::endl; }
};
class SubSomething {
public:
SubSomething() {}
void doOp(int i) { std::cout << "10 - " << i << " = " << 10 - i << std::endl; }
};
int main() {
AddSomething as;
as.doOp(5);
SubSomething ss;
ss.doOp(5);
return 0;
}
This program is pretty lame, but it demonstrates something that happens out in the wild. We have two objects that both take similarly typed data and do different operations on said data.
It looks to me like we could use an interface here. Lets try it.
#include <iostream>
class CoolOpIf {
public:
virtual void doOp(int i) = 0;
};
class AddSomething : public CoolOpIf {
public:
AddSomething() {}
virtual void doOp(int i) override { std::cout << "10 + " << i << " = " << 10 + i << std::endl; }
};
class SubSomething : public CoolOpIf {
public:
SubSomething() {}
virtual void doOp(int i) override { std::cout << "10 - " << i << " = " << 10 - i << std::endl; }
};
int main() {
AddSomething as;
as.doOp(5);
SubSomething ss;
ss.doOp(5);
return 0;
}
So what exactly IS an interface, and why would we type a bunch more stuff to get the same behavior? I'm glad you asked, but before we talk about what interfaces are, we should first talk about the difference between 'IS-a' and 'HAS-a' in terms of inheritance.
IS-a denotes the idea that something IS part of something else. For instance: A human IS-a mammal.
HAS-a denotes the idea that something HAS something else. For instance: A human (ideally) HAS-a brain.
Inheritance helps us define IS-a for an object. In the example above, we defined an an object called 'CoolOpIf.' CoolOpIf doesn't seem so special, but it has a method that can be inherited to make the object inheriting it a CoolOpIf.
So what makes it an interface and not just some other object? Well, the fact that we haven't defined an implementation for CoolOpIf's doOp() method makes it an interface. That means any class that wants to be a CoolOpIf gets to define its own implementation of doOp(), making it a uniquely awesome object with its own behavior.
Now that we know what interfaces ARE, we can explore why we would spend extra time to slap an interface on these objects instead of just doing it the way we did in the first example.
Notice, in the main-line we have :
AddSomething as;
as.doOp(5);
SubSomething ss;
ss.doOp(5);
You could say that the main-line HAS-a AddSomething and that the main-line HAS-a SubSomething. It is also apparent that we are calling both of them with the same data. Strange. Lets re-write the main-line to leverage the CoolOpIf and see where we end up.
int main() {
AddSomething as;
SubSomething ss;
// We need to use pointers, because the CoolOpIf doesn't have an implementation of its own!
CoolOpIf * coolOps[2];
// Assign our objects that classify as 'IS-A' CoolOpIf
coolOps[0] = &as;
coolOps[1] = &ss;
// Loop over them and give them something to do
for(int i = 0; i < 2; i++) {
coolOps[i]->doOp(5);
}
return 0;
}
We now operate on our objects as a CoolOpIf!
This is where you might be thinking "Why on earth are we still using this interface? The first example worked FINE." You're right! I think it is time to rework this whole thing into something useful, maybe a Publisher-subscriber setup?
You can go here to get the code for this.
First, lets create a SubscriberIf so we can define objects as IS-a subscriber.
// An interface for objects that want to 'be' subscribers
class SubscriberIf {
public:
// A method that allows subscriber to get some data
virtual void recvPublishedData(std::string data) = 0;
};
Since we have an interface all set, lets make some objects that qualify as 'IS-a' subscriber real quick (implementations).
class SomeSubscriber : public SubscriberIf {
public:
// Construct a subscriber and set its name
SomeSubscriber(std::string name) : name(name) {
}
// From SubscriberIf
virtual void recvPublishedData(std::string data) override {
std::cout << "Subscriber [" << name << "] received : " << data << std::endl;
}
private:
std::string name;
};
// Basically the same thing, but with a slightly different implementation of the interface for demonstration
class SpecialSubscriber : public SubscriberIf {
public:
// Construct a subscriber and set its name
SpecialSubscriber(std::string name) : name(name) {
}
// From SubscriberIf
virtual void recvPublishedData(std::string data) override {
std::cout << "Special Subscriber [" << name << "] received : " << data << std::endl;
}
private:
std::string name;
};
Next, We'll want to create a publisher object that 'HAS-a' list of subscribers.
// An object that publishes data to subscriber interfaces
class Publisher {
public:
Publisher() {
}
// Check how many subscribers the publisher has
int getSubscriberCount() const {
return subscribers.size();
}
// We don't want to allow nullptrs, so we ensure the existence of a subscriber by
// enforcing a reference.
void registerSubscriber(SubscriberIf & subscriber) {
// Because we store pointers, and have a reference, we need to make it a pointer so we add '&'
subscribers.push_back(&subscriber);
}
void publishData(std::string data) {
// Go through list of subscribers, and send them the data
for(auto &subscriber : subscribers) {
subscriber->recvPublishedData(data);
}
}
private:
// We store pointers, because we can't directly store a virtual object
std::vector<SubscriberIf*> subscribers;
};
The only thing really different between what we have now and the earlier examples is that our interface now takes a string of data and display it along with some name the're given on construction (subscriber), and we now have an object that maintains a list of interfaces that calls on them when asked to 'publish' some data (publisher).
The only thing left to do is instantiate the objects and drive the publisher.
int main() {
Publisher myPublisher;
SomeSubscriber a("Subscriber A");
SomeSubscriber b("Subscriber B");
SomeSubscriber c("Subscriber C");
SpecialSubscriber specialA("wicked");
SpecialSubscriber specialB("wild");
SpecialSubscriber specialC("far-out");
// Since all six of the objects above inherit the SubscriberIf, we can pass them to registerSubscriber
// that will allow myPublisher to execute the methods made avaiable from SubscriberIf
// Note: The publisher won't be able to access any methods on the given objects other than those inherited from
// SubscriberIf as they are 'sliced' off or 'lost' by us only passing the objects in as SubscriberIfs
myPublisher.registerSubscriber(a);
myPublisher.registerSubscriber(b);
myPublisher.registerSubscriber(c);
myPublisher.registerSubscriber(specialA);
myPublisher.registerSubscriber(specialB);
myPublisher.registerSubscriber(specialC);
std::cout << "Publisher has : " << myPublisher.getSubscriberCount() << " subscribers" << std::endl;
myPublisher.publishData("Some data that everyone cares about");
return 0;
}
Voilà !
We have now made something useful using an interface in C++!
Top comments (2)
So we basically use abstract classes as interfaces, we cant have 'pure' interfaces as in c#?
There is no concept of an 'interface' directly in the C++. I dont know anything about C# or how it handles interfaces. I would imagine however, that they would be functionally equivalent.