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
33
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.
4
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.
2
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.
1
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.