15

I have a module, say 'M', which has a few clients, say 'C1', 'C2', 'C3'. I want to apportion the module M's namespace, i.e. the declarations of the APIs and data it exposes, into header file(s) in such a way that -

  1. for any client only the data and APIs that it requires is visible; rest of the module's namespace is hidden from the client i.e. adhere to Interface Segregation principle.
  2. a declaration is not repeated in multiple header files i.e. not violate DRY.
  3. module M doesn't have any dependencies on its clients.
  4. a client is unaffected by the changes made in parts of the module M that is not used by it.
  5. existent clients are unaffected by the addition (or deletion) of more clients.

Currently I deal with this by dividing the module's namespace depending on the requirements of its clients. For example, in the image below the different parts of the module's namespace required by its 3 clients is shown. Client requirements have overlap. The module's namespace is divided into 4 separate header files - 'a', '1', '2' and '3'.

Module namespace partitioning

However, this violates some of the aforementioned requirements i.e. R3 and R5. Requirement 3 is violated because this partitioning depends on the nature of clients; also on the addition of a new client this partitioning changes and violates requirement 5. As can be seen in the right-side of the above image, with the addition of a new client the module's namespace is now divided into 7 header files - 'a', 'b', 'c', '1', '2*', '3*' and '4'. The header files meant for 2 of the older clients changes, thereby triggering their rebuild.

Is there a way to achieve Interface Segregation in C in a non-contrived manner?
If yes, how would you deal with the above example?

An unreal hypothetical solution I imagine would be -
The module has 1 fat header file covering its whole namespace. This header file is divided into addressable sections and subsections like a Wikipedia page. Each client then has a specific header file tailored for it. The client-specific header files are just a list of hyperlinks to the sections/subsections of the fat header file. And the build system must recognize a client-specific header file as 'modified' if any of the sections it points to in the Module's header is modified.

  • 1
    Why is this problem specific to C? Is it because C does not have inheritance? – Robert Harvey Dec 28 '16 at 16:56
  • Also, does violating ISP make your design work better? – Robert Harvey Dec 28 '16 at 17:06
  • 2
    C doesn't really intrinsically support OOP concepts (such as interfaces or inheritance). We make do with crude (but creative) hacks. Looking for a hack to simulate Interfaces. Typically, the whole header file is the interface to a module. – work.bin Dec 28 '16 at 17:06
  • 1
    struct is what you use in C when you want an interface. Granted, methods are a bit difficult. You might find this interesting: cs.rit.edu/~ats/books/ooc.pdf – Robert Harvey Dec 28 '16 at 17:06
  • I couldn't come up with an interface equivalent using struct and function pointers. – work.bin Dec 28 '16 at 17:08
5
+50

Interface Segregation, in general, should not be based on client requirements. You should change the whole approach to achieve it. I would say, modularize the interface by grouping the features into coherent groups. That is grouping is based on the coherence of features themselves, not the client requirements. In that case, you will have a set of interfaces, I1, I2, ... etc. Client C1 may use I2 alone. Client C2 may use I1 and I5 etc. Note that, if a client uses more than one Ii, is not a problem. If you have decomposed the interface into coherent modules, that is where the heart of the matter is.

Again, ISP is not client-based. It is about decomposing interface into smaller modules. If it this is done properly, it will also ensure that clients are exposed to as few features as they need.

With this approach, your clients can increase to any number but you M is unaffected. Each client will use one or some combination of the interfaces based on their need. Will there be cases that a client, C, need to include say I1 and I3, but not use all the features of these interfaces? Yes, that is not a problem. It just uses the least number of interfaces.

  • You surely meant disjoint or non-overlapping groups, I assume? – Doc Brown Jan 9 '17 at 12:08
  • Yes, disjoint and non-overlapping. – Nazar Merza Jan 9 '17 at 18:35
3

The Interface Segregation Principle says:

No client should be forced to depend on methods it does not use. ISP splits interfaces that are very large into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them.

There are a few unanswered questions here. One is:

How small?

You say:

Currently I deal with this by dividing the module's namespace depending on the requirements of its clients.

I call this manual duck typing. You build interfaces that expose only what a client needs. The interface segregation principle is not simply manual duck typing.

But the ISP is also not simply a call for "coherent" role interfaces that can be reused. No "coherent" role interface design can perfectly guard against the addition of a new client with it's own role needs.

ISP is a way to isolate clients from the impact of changes to the service. It was intended to make the build go faster as you make changes. Sure it has other benefits, like not breaking clients, but that was the main point. If I'm changing the services count() function signature it's nice if clients that don't use count() don't need to be edited and recompiled.

This is WHY I care about the Interface Segregation Principle. It's not something I take on faith as important. It solves a real problem.

So the way it should be applied should solve a problem for you. There is no brain dead rote way to apply ISP that can't be defeated with just the right example of a needed change. You are supposed to look at how the system is changing and make choices that will let things quiet down. Let's explore the options.

First ask yourself: is making changes to the service interface difficult right now? If not, go outside and play until you calm down. This isn't an intellectual exercise. Please be sure the cure isn't worse than the disease.

  1. If many clients use the same subset of functions, that argues for "coherent" reusable interfaces. The subset likely focuses around one idea that we can think of as the role the service is providing to the client. It's nice when this works. This doesn't always work.

  2.  

    1. If many clients use different subsets of functions it's possible that the client is actually using the service through multiple roles. That's OK but it makes the roles hard to see. Find them and try to tease them apart. That may put us back in case 1. The client simply uses the service through more than one interface. Please don't start casting the service. If anything that would mean passing the service into the client more than once. That works but it makes me question if the service isn't a big ball of mud that needs to be broken up.

    2. If many clients use different subsets but you don't see roles even allowing that the clients might use more than one then you have nothing better than duck typing to design your interfaces around. This way of designing the interfaces ensures that the client isn't exposed to even one function it isn't using but it almost guarantees that adding a new client will always involve adding a new interface that while the service implementation doesn't need to know about it the interface that aggregates the role interfaces will. We've simply traded one pain for another.

  3. If many clients use different subsets, overlap, new clients are expected to added that will need unpredictable subsets, and you're unwilling to break up the service then consider a more functional solution. Since the first two options didn't work and you're really in a bad place where nothing is following a pattern and more changes are coming then consider providing each function it's own interface. Ending up here doesn't mean ISP has failed. If anything failed it was the object oriented paradigm. Single method interfaces follow ISP in the extreme. It's a fair bit of keyboard typing but you may find this suddenly makes the interfaces reusable. Again, be sure there isn't a simpler way to solve this problem before you do this but this is a solution that follows ISP and should isolate clients from changes to the service that they don't really care about.

So it turns out they can get very small indeed.

I've taken this question as a challenge to apply ISP in the most extreme cases. But bear in mind that extremes are best avoided. In a well thought out design that applies other SOLID principles these issues usually don't occur or matter, nearly as much.


Another unanswered question is:

Who owns these interfaces?

Over and over I see interfaces designed with what I call a "library" mentality. We all have been guilty of monkey-see-monkey-do coding where you're just doing something because that's how you saw it done. We are guilty of the same thing with interfaces.

When I look at an interface designed for a class in a library I used to think: oh, these guys are pros. This must be the right way to do an interface. What I was failing to understand is that a library boundary has it's own needs and issues. For one thing, a library is completely ignorant of the design of it's clients. Not every boundary is the same. And sometimes even the same boundary has different ways to cross it.

Here are two simple ways to look at interface design:

  • Service owned interface. Some people design every interface to expose everything a service can do. You can even find refactoring options in IDE's that will write an interface for you using whatever class you feed it.

  • Client owned interface. ISP seems to argue that this is right and service owned is wrong. You should break up every interface with the clients needs in mind. Since the client owns the interface it should define it.

So who's right?

Consider plugins:

enter image description here

Who owns the interfaces here? The clients? The services?

Turns out both.

The colors here are layers. The red layer (right) isn't supposed to know anything about the green layer (left). The green layer can be changed or replaced without touching the red layer. That way any green layer can be plugged into the red layer.

I like knowing what's supposed to know about what, and what isn't supposed to know. To me, "what knows about what?", is the single most important architectural question.

Let's make some vocabulary clear:

[Client] --> [Interface] <|-- [Service]

----- Flow ----- of ----- control ---->

A client is something that uses.

A service is something that is used.

Interactor happens to be both.

ISP says break up interfaces for clients. Fine, lets apply that here:

  • Presenter (a service) shouldn't dictate to the Output Port <I> interface. The interface should be narrowed to what Interactor (here acting as a client) needs. That means the interface KNOWS about the Interactor and, to follow ISP, must change with it. And this is fine.

  • Interactor (here acting as a service) shouldn't dictate to the Input Port <I> interface. The interface should be narrowed to what Controller (a client) needs. That means the interface KNOWS about the Controller and, to follow ISP, must change with it. And this is not fine.

The second one is not fine because the red layer isn't supposed to know about the green layer. So is ISP wrong? Well kinda. No principle is absolute. This is a case where the goofs who like the interface to show everything the service can do turn out to be right.

At least, they're right if the Interactor doesn't do anything other than this use case needs. If the Interactor does things for other use cases there is no reason this Input Port <I> has to know about them. Not sure why Interactor can't just focus on one Use Case so this is a non issue, but stuff happens.

But the input port <I> interface simply can't slave itself to the Controller client and have this be a true plugin. This is a 'library' boundary. A completely different programming shop could be writing the green layer years after the red layer has been published.

If you're crossing a 'library' boundary and you feel the need to apply ISP even though you don't own the interface on the other side you're going to have to find a way to narrow the interface without changing it.

One way to pull that off is an adapter. Put it between clients like Controler and the Input Port <I> interface. The adapter accepts Interactor as an Input Port <I> and delegates it's work to it. However, it exposes only what clients like Controller need through a role interface or interfaces owned by the green layer. The adapter doesn't follow ISP it self but allows more complex class like Controller to enjoy ISP. This is useful if there are fewer adapters than clients like Controller that use them and when you're in the unusual situation where you're crossing a library boundary and, despite being published, the library won't stop changing. Looking at you Firefox. Now those changes only break your adapters.

So what does this mean? It means honestly you haven't provided enough information for me to tell you what you should do. I don't know if not following ISP is causing you a problem. I don't know if following it wouldn't end up causing you more problems.

I do know you're looking for a simple guiding principle. ISP tries to be that. But it leaves a lot unsaid. I believe in it. Yes, please don't force clients to depend on methods that they don't use, without a good reason!

If you have a good reason, such as your designing something to accept plugins, then be aware of the problems not following ISP causes (it's hard to change without breaking clients), and ways to mitigate them (keep Interactor or at least Input Port <I> focused on one stable use case).

  • Thanks for the input. I have a service-providing module which has multiple clients. Its namespace has logically coherent boundaries but client needs cut across these logical boundaries. Thereby divvying up the namespace on the basis of logical boundaries doesn't help with ISP. I therefore have divided the namespace on the basis of client needs as shown in the diagram in the question. But this makes it dependent on clients and a poor way of coupling clients to the service, since clients could be added/removed relatively frequently, but the changes in the service will be minimal. – work.bin Jan 11 '17 at 12:39
  • I am now leaning towards the service providing a fat interface, as in its full namespace and it is upto the client to access these services via client specific adapters. In C terms that would be a file of function wrappers owned by the client. Changes in the service would force recompilation of the adapter but not necessarily the client. ..<contd> – work.bin Jan 11 '17 at 12:50
  • <contd>.. This will definitely keep build times minimal and keep the coupling between the client and service 'loose' at the cost of runtime (calling an intermediary wrapper function), increases code space, it increases the stack usage and probably more mindspace (programmer's) in maintaining the adapters. – work.bin Jan 11 '17 at 12:50
  • My current solution satisfies my needs now, the new approach will take more effort and may well be violating YAGNI. I will have to weigh the pros and cons of each method and decide which way to go here. – work.bin Jan 11 '17 at 12:52
1

So this point:

existent clients are unaffected by the addition (or deletion) of more clients.

Gives up that you are violating another important principle which is YAGNI. I would care about it when I have hundreds of clients. Thinking about something up front and then it will turn out that you don't have any additional clients for this code beats the purpose.

Second

 partitioning depends on the nature of clients

Why is your code not using DI, dependency inversion, nothing, nothing in your library should be depending on your client nature.

Eventually it looks like you need additional layer under your code to serve needs for overlapping stuff (DI so your front facing code depends only on this additional layer, and your clients depend only on your front facing interface) this way you beat the DRY.
This you would od it for real. So you make same stuff that you use in yor module layer below another module. This way having layer below you achieve:

for any client only the data and APIs that it requires is visible; rest of the module's namespace is hidden from the client i.e. adhere to Interface Segregation principle.

yes

a declaration is not repeated in multiple header files i.e. not violate DRY. module M doesn't have any dependencies on its clients.

yes

a client is unaffected by the changes made in parts of the module M that is not used by it.

yes

existent clients are unaffected by the addition (or deletion) of more clients.

yes

1

The same information as provided in declaration is always repeated in the definition. It's just the way this language works. Also, repeating a declaration in multiple header files does not violate DRY. It's a rather commonly used technique (in the standard library at least).

Repeating the documentation or the implementation would violate DRY.

I wouldn't bother myself with this unless the client code isn't written by me.

0

I disclaim my confusion. However your practical example draws a solution in my head. If I can put into my own words: all partitions in module M have a many to many exclusive relationship with any and all clients.

Sample Structure

M.h      // fat header
 - P1    // Partition 1
 - P2    // ... 2
   - P21 // ... 2 section 1
 - P3    // ... 3
C1.c     // Client 1 (Needs to include P1, P3)
C2.c     // ... 2 (Needs to include P2)
C3.c     // ... 3 (Needs to include P1, P21, P3)

M.h

#ifdef P1
#define _PREF_ P1_             // Define Prefix ("PREF") = P1_
 void _PREF_init();            // Some partition specific function
#endif /* P1 */

#ifdef P2
#define _PREF_ P2_
 void _PREF_init();
#endif /* P2 */

#if defined(P21) || defined (P2) // Part 2.1
#define _PREF_ P2_1_
 void _PREF_oddone();
#endif /* P21 */

#ifdef P3
#define _PREF_ P3_
 void _PREF_init();
#endif /* P3 */

M.c

In the M.c file, you would not actually have to use the #ifdefs because what you put in the .c file does not effect the client files so long as the functions the client files use are defined.

#include "M.h"
#define _PREF_ P1_        
void _PREF_init() { ... };

#define _PREF_ P2_
void _PREF_init() { ... }

#define _PREF_ P2_1_
void _PREF_oddone() { ... }

#define _PREF_ P3_
void _PREF_init() { ... }

C1.c

#define P1     // "invite" P1
#define P3     // "invite" P3
#include "M.h" // Open the door, but only the invited come in.

void main()
{
    P1_init();
    //P2_init();
    //P2_1_oddone();
    P3_init();
}

C2.c

#define P2
#include "M.h

void main()
{
    //P1_init();
    P2_init();
    P2_1_oddone();
    //P3_init();
}

C3.c

#define P1
#define P21
#define P3  
#include "M.h" 

void main()
{
    P1_init();
    //P2_init();
    P2_1_oddone();
    P3_init();
}

Again, I'm not sure if this is what you're asking. So take it with a grain of salt.

  • What does M.c look like? Do you define P1_init() and P2_init()? – work.bin Jan 11 '17 at 12:58
  • @work.bin I presume M.c would look like a simple .c file with the exception of defining the namespace between functions. – Sanchke Dellowar Jan 11 '17 at 15:09
  • Assuming both C1 and C2 exist - what does P1_init() and P2_init() link to? – work.bin Jan 11 '17 at 16:07
  • In the M.h/M.c file, The preprocessor will replace _PREF_ with whatever it was last defined to. So _PREF_init() will be P1_init() because of the last #define statement. Then the next define statement will set PREF equal to P2_, thus generating P2_init(). – Sanchke Dellowar Jan 11 '17 at 18:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.