10

Providing that clients would typically consume just one method, though methods would be conceptually related, why not always apply the Interface Segregation Principle to the extreme and have [many] single-method interfaces? Is there an objective rule against this? Not something like "oh it feels wrong" or "but you will have so many types, it's hard to read and manage" but rather something logical and clear. (Still want to name contracts clearly, so functions are not a good fit?).

Example:

IGeometryManager
{
   Shape CreateTriangle();
   Shape CreateCircle();
   Shape CreateSquare();
   Shape CreateEllipse();
   Shape CreateCurve();
   Shape CreateLine();
   void RemoveAll();
}

Result:

ITriangleCreator
{
   Shape CreateTriangle();
}

ICircleCreator
{
   Shape CreateCircle();
}

ISquareCreator
{
   Shape CreateSquare();
}

IEllipseCreator
{
   Shape CreateEllipse();
}

ICurveCreator
{
   Shape CreateCurve();
}

ILineCreator
{
   Shape CreateLine();
}

IShapeRemover
{
   void RemoveAll();
}

This is a relevant blog post, but I am not 100% persuaded by the authors logic: http://blog.ploeh.dk/2014/03/10/solid-the-next-step-is-functional

Den
  • 4,827
  • 2
  • 32
  • 48
  • 10
    The only rule is - "Is this a **useful** abstraction"? Does it make things clearer? If it doesn't. Well. – Oded Jul 11 '14 at 10:34
  • 6
    `Providing that clients would typically consume just one method... ` this would be an unusual situation in software development. – rwong Jul 11 '14 at 10:34
  • 5
    If you boil an interface down to a single method, that's not really a new abstraction any more - it's a function. There's not a lot of point to `ICurveCreator`; in Java 8 I'd just use `Supplier`/`Function`/`BiFunction` and in C# I'd use `Func`. Insisting that every function under the sun be given a unique name before it can be used is incredibly counterproductive. – Doval Jul 11 '14 at 11:40
  • @Doval How do you suggest the common implementation details would be shared? Currying? – Den Jul 11 '14 at 12:17
  • @Den Can you give an example? I'm not quite sure what you mean by sharing common implementation details. – Doval Jul 11 '14 at 12:19
  • @Doval It's hard to ask this question in a generic way and come up with a good generic example. Let's pretend IGeometryManager is an interface for a physics engine. It's typical for physics engines to have a data structure holding all the shapes for collision detection. How would you share this implementation detail between functions? – Den Jul 11 '14 at 12:27
  • @Doval "Insisting that every function under the sun be given a unique name before it can be used is incredibly counterproductive. " - exactly why I said 'Not something like "oh it feels wrong" or "but you will have so many types, it's hard to read and manage"' in my question. – Den Jul 11 '14 at 12:30
  • You'll have to document it somewhere, probably in the manager interface, same with any other side effect. Does the code that calls the factories really need to know that those shapes are secretly added to some data structure as they're created though? – Doval Jul 11 '14 at 12:32
  • @Doval I have added a link that seems to be sharing your opinion. I am not going to argue against, but not sure this is for me. I will need to do further analysis. – Den Jul 11 '14 at 13:03
  • How would you define a function which invokes more than one method (e.g. both `CreateCircle` and `CreateSquare`) on the same object passed as the argument? Maybe it would make sense to use some syntax like `void createCircleAndSquare(ICircleCreator with ISquareCreator geometryCreator)`, but AFAIK most languages do not support this. – proskor Jul 11 '14 at 13:21
  • No one can really do anything with that link if you don't say what about the blog post you find questionable. – Doval Jul 11 '14 at 13:23
  • @Doval People can open and read that post and find it useful. Regardless of my opinion about it. My opinion is that stand-alone functions are not contracts. – Den Jul 11 '14 at 13:32
  • @Den You can't force implementations of an interface to be correct any more than you can force implementations of a function to be correct, so what's the difference? Rolling your own custom one-method interfaces means your `IShapeCreator` can't be as easily used with code that uses the language's de-facto function interface (e.g. `Func`, `Function`) – Doval Jul 11 '14 at 13:50
  • @Doval What about IoC containers? How would you resolve appropriate functions automatically? – Den Jul 11 '14 at 15:14
  • @Den You could always create a class that implements `Func` or the relevant interface, right? – Doval Jul 11 '14 at 15:19
  • @Doval I thought you were arguing against using such interfaces though. – Den Jul 11 '14 at 15:38
  • I'm arguing against reinventing them unnecessarily. `ISomethingFactory` is no better than `Func`, and when everyone rolls their own custom function type, it's harder to generic code that accepts or returns functions. – Doval Jul 11 '14 at 15:46
  • 1
    @Doval But isn't the fact that you can only use an instance of `IShapeCreator` where it is required and not a generic `Func` a good thing? I mean, using only generic `Func`s would make the type system useless or at least less useful. – proskor Jul 11 '14 at 15:50
  • @proskor Who's generating the "generic `Func`s" that you worry about and what's stopping him from making bad `IShapeCreator`s? I don't understand what you're trying to protect yourself against. It's not like two interfaces that both produce Shapes are fundamentally different. – Doval Jul 11 '14 at 16:00
  • 1
    @Doval True, syntactically they might be similar: both produce a shape and probably accept the same number of arguments. But they obviously specify different behavior; in this sense `ISquareCreator` and `IEllipseCreator` are indeed fundamentally different. Not only is the specification different, but there also can be multiple implementations. – proskor Jul 11 '14 at 16:39
  • @Doval "ISomethingFactory is no better than Func" - it is better _in a way_ that it allows simple injection via an IoC container. – Den Jul 11 '14 at 16:41
  • 1
    @proskor There can be multiple implementations of a `Func` as well. And if your concern is that you want to make sure that you're given a function to generate a Square and not an Ellipse, the fault is in the Shape class if it doesn't provide the means to tell them apart; that's not the factory's job. There's no guarantee that because someone gave you an IEllipseCreator it's actually creating ellipses and not squares. – Doval Jul 11 '14 at 16:47
  • 2
    @Doval. Ok, I think I understand what you are getting at. The problem is that you cannot enforce correctness formally 100%. It is actually theoretically impossible in general. But that doesn't mean that all is lost. Assuming that the implementation is correct, you still can ensure formally that at least you wont use the wrong function, if you specify the required interface correctly. On the other hand, if you just say, all `Func` are equal, you can't even do that. – proskor Jul 11 '14 at 17:02
  • 1
    @proskor I get that. What I'm saying is that you're pushing the problem to the wrong place. All `Func` are the same because all `Shape` are the same. You may have solved the problem of "how do I ensure I don't accidentally pass a factory for squares instead of ellipses", but you still have the problem of "how do I ensure I don't accidentally pass a square instead of an ellipse?". The real solution is to find a way to differentiate shapes, not to find a way to differentiate shape factories. If you fix that, you fix the "all Func are equal" issue for free. – Doval Jul 11 '14 at 17:08
  • @Doval Ok, so you're saying that the type system isn't expressive enough? Clearly, in this case the solution would be to restrict the return type to `Square` or `Circle` in `ISquareCreator` and `CircleCreator` resp., but what about functions which can return both `Square` and `Circle`? Which return type should they have? `Shape` is too general and `Square` and `Circle` is too concrete. And this is just a slight variation of the problem. – proskor Jul 11 '14 at 17:23
  • @proskor You can [define](http://programmers.stackexchange.com/questions/159804/how-do-you-encode-algebraic-data-types-in-a-c-or-java-like-language) ([or reuse](http://stackoverflow.com/questions/17254855/what-is-the-simplest-way-to-access-data-of-an-f-discriminated-union-type-in-c)) a type that can contain either a value of type A or a value of type B. You'd then have a `Func>`. – Doval Jul 11 '14 at 17:33
  • @Doval Yes, but you cannot prove everything in general. This was just a simple example, more complex one's would not even be expressible using current type systems. So, you have no choice but to specify some functions informally. – proskor Jul 11 '14 at 17:56
  • 1
    @proskor If you're going to specify it informally, what stops you from saying "The argument to this method must return a Circle or a Square"? If it's not something you can solve with the type system then rolling your own custom function type won't help you. – Doval Jul 11 '14 at 17:59
  • 1
    @Doval Because it's not black and white. In some cases you have no other choice, but it other cases the type checker can be very helpful. – proskor Jul 11 '14 at 19:00
  • 1
    @Doval I must admit, you were right about this. If you can't specify the function formally, then there is no use in creating a custom function type for it; you are going to specify it informally anyway. But, I guess, at least the name of the function/interface gives a hint about it's purpose... – proskor Jul 11 '14 at 21:21
  • 1
    @Doval Actually you have to define a custom type if you are going to provide multiple implementations of it. How else could you distinguish between unrelated functions having the same type from functions which implement the same specification? – proskor Jul 12 '14 at 07:25
  • Please take long discussions to chat. –  Jul 12 '14 at 19:46
  • One word: "protocol". Protocols support long-term assumptions. – Brian Cannard Nov 08 '20 at 05:07

4 Answers4

10

No, there isn't any objective rule.

If there were objective rules, someone could automate them and you'd be out of a job. Such decisions are always trade-offs between pressures that are pretty obvious in themselves, but have different relative strengths in different situations. So far, only (some) humans can properly judge such multidimensional optimization problems.

Kilian Foth
  • 107,706
  • 45
  • 295
  • 310
  • 2
    How about this rule: "Any interface should only have as many methods as are simultaneously consumed by a single client type/method?" – Den Jul 11 '14 at 12:20
  • 4
    That could be automated, but only if you already know all the API consumers there will ever be. Usually, publishing an API elicits new consumers doing things you hadn't expected (and typically, some of them will clamor loudly for new or extended functionality). – Kilian Foth Jul 11 '14 at 12:34
5

There is someone who has developed this principle to the extreme, and further: the german software Engineer Ralf Westphal made a complete programming model from it and called it "Event Based Components", together with a design method, called Flow Design. Actually, he does not use the "interface form", only Func or Action contracts, and he has got a lot of very good arguments why this is probably the better way to go.

He has published most articles about it in german, but here is an article in english about his approach, not by himself. Last year he published a (cheap) book about the topic.

Glorfindel
  • 3,137
  • 6
  • 25
  • 33
Doc Brown
  • 199,015
  • 33
  • 367
  • 565
  • Actually I am already using this (a component/actor model hybrid), but for higher-level composition. Integrating third-party libraries is a problem though, that's why I am asking this question (should we be passing the whole facade interface to each shape component, or just a one-method contract interface). I like dynamic and loose coupling on the top level, but prefer some solid well-defined contracts on lower level. – Den Jul 14 '14 at 10:29
  • @Den: I suggest to read Ralf Westphal's comment in the article above. – Doc Brown Jul 14 '14 at 10:56
  • Thanks, I did not notice there was a comment section. I can see even more similarities with my approach now. – Den Jul 14 '14 at 11:32
  • Scott Wlaschin talks about this in the context of Functional Programming a fair bit in this presentation. http://youtu.be/E8I19uA-wGY – RubberDuck May 17 '16 at 10:31
4

In most programming languages and frameworks, overly-segregated interfaces make it difficult to aggregate, compose, or wrap objects which share various combinations of abilities. If many types implement an interface which defines many methods, but also includes a means of asking how well particular instances can promise to implement them, then a single wrapper or aggregating class will be able to wrap or aggregate instances of all such types, and expose to the client whatever combinations of abilities are supported by the wrapped or aggregated instances.

If instead each class only implemented exactly those methods it supported, and was expected to usefully support every method it implemented, then the author of each wrapper class would need to select a fixed set of methods for it to supports. Any object which couldn't support all those methods couldn't be wrapped, and no method which wasn't in that set could be made available to clients. If a client with limited need wanted to wrap objects with limited abilities, and a client with greater needs wanted to wrap objects with greater abilities, different wrapper classes would be required for those clients. Because wrapper classes need to contain explicit logic for each wrapped method, one couldn't use a generic family of wrappers to handle the different use cases; every combination of supported methods would require a completely-separate wrapper class. If objects support many different combinations of abilities, and clients have many different combinations of requirements, the number of wrappers that are required may become totally unworkable.

While it can be useful to have the type system ensure at compile time that objects which will need to have a certain ability will, in fact, have that ability, there are many situations where trying to validate everything at compile time simply won't work usefully. If implementations of one interface would frequently be try-cast to another interface, that's a good sign that the members of both interfaces should perhaps be combined into a single interface.

supercat
  • 8,335
  • 22
  • 28
  • In PHP there are Traits that can be used to achieve horizontal composition. With those the dev can provide default implementations of each interface, that can be just `use`d in the desired class. This would allow to stick to the minimum number of aspects-combinations (base models), to still have clean code where methods are still grouped by responsibility, and still allows to override a single method in a class that needs a more specific implementation. – Kamafeather Jan 14 '20 at 00:21
3

The design of an interfaces should be based on the components that will use it (consumer), and on the components that will implement it (implementor).

Implementor side

Would you ever want to write a class that only draws a circle, but not any of the other shapes?

Maybe you have a DefaultGeometryManager with hardcoded algorithms for each shape. But the one for circle is slow or flawed. You have a dedicated highly-optimized library for circle-drawing, but you don't want your DefaultGeometryManager to depend on this library.

You could inherit from DefaultGeometryManager and override the circle method. But this approach has limitations. E.g. if you later want to do something similar for another shape.

So you could split up your interface, and let IGeometryManager inherit from the individual interfaces. Have a CompositeGeometryManager implementing IGeometryManager, which delegates each method to a dedicated implementation.

Thanks to LSP, the DefaultGeometryManager matches the requirement of each of the dependencies of CompositeGeometryManager. So:

defaultGeomegryManager = new DefaultGeometryManager();
circleCreator = new OptimizedCircleCreator();
geometryManager = new CompositeGeometryManager(defaultGeomegryManager, defaultGeomegryManager, defaultGeomegryManager, defaultGeomegryManager, defaultGeomegryManager, circleCreator);

If the method signatures for each shape were the same, you could instead have just one interface IShapeCreator. But I would say this is not the case in your example.

Decorators

A decorator class is a consumer and a provider at the same time. Usually it only cares about one of the methods. Decorators are easier to write for small interfaces.

Mocking and testing.

An interface with fewer methods is obviously easier to mock.

Consumers: Known / internal

If your interface is only meant for consumers inside your library, which you control yourself, then it is generally ok to have dedicated interfaces with only those methods that are actually needed. Or, as you suggest, one method per interface. This approach is great for internal refactoring.

Consumers: Unknown / external / 3rd party

If you want to provide a public API, then you need to design for consumers that you don't know yet. Probably you want to provide richer interfaces, which are more comfortable to use. With a "1 method per interface", 3rd party code would have a hard time delivering the exact component to each consumer.

On the other hand: If a consumer library wants to rid itself of a library it depended on, and provide the same functionality by itself, then the richer interface becomes a burden.

This, and the mocking argument, could be seen as an argument for "1 method per class" even for the public API. But I would say the overall usefulness and comfort of richer interfaces still make them the preferable choice.

Library clutter

Some developers will complain that the library becomes really big (in number of files / classes / interfaces), with all your one-off interfaces.

Also, after a lot of internal refactoring, you will see leftover interfaces and classes which you no longer use, but cannot remove for BC.

If you work in a team, this may be a turn-off for your co-developers. But while it can be irritating seeing so many interfaces and classes, it does not really give you structural problems.

I personally find this much preferable to having the same logic within one class.

Naming

If you work with a 1 method per class apoproach, you want a real simple generic naming pattern for classes and interfaces, because you will have to come up with a lot of names.

The naming pattern should prevent future name clashes and ambiguities.

You should avoid vague terms like "Manager" or "Kernel", but instead let the names somewhat reflect the name of the method.

Method names should be distinguishable between interfaces, so you can later combine interfaces via inheritance without nameclash. E.g. if you had ICurveCreator::create() and ILineCreator::create(), then you would get a nameclash in your IGeometryManager.

Generics

If your language supports generics, you need to write fewer interfaces.

Conclusion

A reasonable approach is to provide some rich, composite interfaces for your public API, and smaller interfaces as contracts between your internal components.

You could go down to 1 method per class, but it is not always necessary.

You could start all your code with a 1 method per class approach. Development can be really fast this way, because you avoid a lot of decisionmaking, and your IDE has a really easy job autocompleting. You can still scale up later, and recombine individual components.

Or you start with the bigger interfaces, and then gradually split them up as you see fit.

"the next step is Functional"

This is right, in theory. But it depends how well this is supported in your language. You might lose a lot of "strict typing" features of the language.

E.g. in PHP, the language features for interfaces and classes are richer than for functions. Modern PHP does have anonymous functions, and a "callable" type. But a parameter type hint cannot distinguish between signatures.

And even if it would: What if the required signature is just "a function that returns a string, with no parameters". This can still be quite arbitrary. A signature is a technical contract. An interface is a combination of a technical and a semantic contract.

Value objects can mitigate this problem, and make your signatures more specific.

Also, classes provide more comfortable (though more verbose) ways to organize instance variables.

donquixote
  • 857
  • 7
  • 13