One thing I’ve been recently wondering about, is how to implement client-specific interfaces for a class. By client-specific, I mean that a client of class A
would be granted access only to a subset of A
‘s public interface. This could be useful when different entities are to be admitted different access rights to the services provided by A
.
This isn’t in fact a new idea. It’s just a concrete implementation for the Interface Segregation Principle, one of the five SOLID principles of object-oriented design.
An example could be a NewsServer
which has two types of clients, an Editor
and a Reader
. An Editor
is allowed to submitNews()
to the NewsServer
, while a Reader
can only readNews()
from the NewsServer
:
class NewsServer
{
public:
News getNews() const;
void addNews(const News& news);
private:
List<News> serverNews_;
};
class Editor
{
public:
void submitNews() { pServer_->addNews(news_); }
private:
NewsServer* pServer_;
News news_;
};
class Reader
{
public:
void readNews() { news_ = pServer_->getNews(); }
private:
NewsServer* pServer_;
News news_;
};
However, since the NewsServer
public interface is visible to all clients, nothing prevents a Reader
from calling addNews()
to submit news to the NewsServer
.
One possible solution to prevent a Reader
client from calling addNews()
on a NewsServer
is to prevent any editing to NewsServer
from inside a Reader
object:
class Reader
{
public:
void readNews() { news_ = pServer_->getNews(); }
private:
const NewsServer* pServer_;
News news_;
};
By letting pServer_
be a const NewsServer*
, a Reader
cannot addNews()
to a NewsServer
. This however restricts any write operations to NewsServer
from a Reader
. Suppose for example that we want to extend the NewsServer
to allow a Reader
to addComments()
on submitted news. This extension would break this solution because a Reader
would have to be able to edit a NewsServer
.
A better way to approach this problem is similar, in some sense, to an SQL VIEW
of a TABLE
. The idea is basically to hide the raw interface of a NewsServer, and expose a “view” of it that is customized per client. So a Reader
client would have to deal with a NewsServerReaderView
and an Editor
would have to deal with a NewsServerEditorView
. A view is simply a new interface for a NewsServer
that only exposes the services relevant to a specific client. This is also a good place for using aggregation through private inheritance:
class NewsServer
{
protected:
News rawGetNews() const;
void rawAddNews(const News& news);
void rawAddComments(const Comments& comments);
private:
List<News> serverNews_;
List<Comments> serverComments_;
};
class NewsServerEditorView: private NewsServer
{
public:
void addNews(const News& news) { rawAddNews(news); }
};
class NewsServerReaderView: private NewsServer
{
public:
News getNews() const { return rawGetNews(); }
void addComments(const Comments& comments) { rawAddComments(comments); }
};
class Editor
{
public:
void submitNews() { pServer_->addNews(news_); }
private:
NewsServerEditorView* pServer_;
News news_;
};
class Reader
{
public:
void readNews() { news_ = pServer_->getNews(); }
void commentOnNews(const Comments& comments) { pServer_->addComments(comments); }
private:
NewsServerReaderView* pServer_;
News news_;
Comments comments_;
};
This way, read and write access are both preserved for all clients and every client is only granted access to services relevant to itself. Notice that NewsServer
‘s old public
interface is now protected
. This prevents any client from accessing the raw NewsServer
interface and forces it to use one of the available views (or possibly add a new view).