I am challenging my OO design skills and started an ambitious project that is supposed to be highly reusable and extensible. It is supposed to be kind of a framework for evolutionary algorithms. Maybe there is somthing out there on GitHub, but my research didn't come up with anything I'm looking for and anyway, I'd like to see if I could develop it myself and improve my skills.
The very problem I'm facing is the multitude of potential interfaces I can imagine I or some potential user might need, because implementations of the objects are so different they even have different interfaces. That might sound strange or like bad design, but I think it's due to the meta level I'm thinking at. Let me give you an abstract example, maybe you spot the right pattern I'm missing. The current pattern is Strategy Pattern, but the interface will vary according to different DataInterfaces:
class Client {
/** EvaluatorInterface */
private $evaluatorStrategy;
public function getData(): DataInterface {
//return data...
}
public function doEvaluation()
{
$value = $this->evaluatorStrategy->evaluate($this->getData());
//...
}
}
The problem is, that in this client code, I do not exactly know, what the DataInterface and thus the EvaluatorInterface must look like, since I want to be able to both change the type the data is stored in and the strategy the evaluator works with. What I do know, is that the both need to match. E.g. When I decide to add a new fancy data structure with a new interface, I need to adjust the injected EvaluationStrategy accordingly.
So, since this is PHP I could just don't specify any types and just hope we have data and evaluator strategies that can work together, or otherwise we get a runtime error but that's not what I want. The design should be clean, and I think it must work somehow.
Another thing I tried was Visitor Pattern. At first it sounded promising, but then I realized, that the EvaluatorInterface would need grow each time I'd add a new data type, something a user of my package couldn't even do.
Do you have any ideas where I'm barking up the wrong tree? Or what pattern could help me?
3 Answers 3
With @caleth pointing me in the right direction and some more research, I think I can answer my own question like this:
I think in general, the problem seems to be made for generics. Since PHP doesn't support generics, I'll need to leave out real generic TypeHints. Instead I'll document the right types using the @template T (with @var T and @return T and so on...) annotations. Moreover I'm considering integrating some kind of static analysis tool like PHPStan to provide some ability to check the correct types as in JAVA a compiler would do.
So, my code looks like this now:
/**
* @template T of DataInterface
*/
class Client {
/** @var EvaluatorInterface<T> */
private $evaluatorStrategy;
/** @return T */
public function getData() {
//return data...
}
public function doEvaluation()
{
$value = $this->evaluatorStrategy->evaluate($this->getData());
//...
}
}
-
Generics are well supported in PHP static analysers PHPStan and Psalm, and somewhat supported in PhpStorm. If you can make sure your code is never run in production without having passed static analysis check first you may find that is enough and you don't need generics support in the actual language.bdsl– bdsl01/28/2022 13:26:46Commented Jan 28, 2022 at 13:26
-
There's even github.com/Roave/you-are-using-it-wrong in case you want to make sure that downstream consumers of your library respect your generic types.bdsl– bdsl01/28/2022 13:27:26Commented Jan 28, 2022 at 13:27
The whole idea of an Interface is that it is a contract which tells the caller what is expected. Allowing any interface is usually consider a bad idea, because:
- If any object type to allowed to be passed in, reflection is required to find the methods and properties, but even if that was done, foreknowledge of how to use them is required.
- Weakly typed languages like JavaScript, do this all the time. If they want to append something to an object they just do it. We are able to use this pattern in Strongly typed languages but it does not diminish the foreknowledge factor.
- The newer patterns for Strongly typed languages bring the Generic factor.
// Allows any type to be passed in as a parm
public method<ofType>(ofType parm1){
//still requires knowledge of how to use.
}
- The traditional pattern is to protect our code as follows. In this pattern we are saying if you want to use this method there's only these interfaces that we support.
//Only allow known interface implementations
public method(InnterfaceTypeA NameA){ //we know this interface }
public method(InnterfaceTypeB NameB){ //we know this interface }
public method(InnterfaceTypeC NameC){ //we know this interface }
- This pattern uses the keyword Dynamic (which C# has)
//Still requires discovery of the type and how to use it.
public method(Dynamic Parma){}
-
I agree with you in terms of "the implementation of the method must know the interface", but what your examples do not consider, is that I have TWO interfaces. When these two match in terms of "know each other" or, concretely, in my example EvaluatorInterface<SomeDataInterface> knows how to deal with SomeDataInterface, there is enough knowledge for it to work. I think Caleths comment(s) to my question hit(s) the point.S. Parton– S. Parton08/28/2021 10:03:31Commented Aug 28, 2021 at 10:03
I am a bit out-of-touch with PHP, so I'll give it for java. It is reminiscent of OpenGL/DirectX discovery of capabilities; which was not done systematically.
My Solution
What you want, is the detection of (yet unknown) capabilities.
Every capability is an interface.
public class Client {
public <T> Optional<T> lookup(Class<T> capability) { ... }
}
public interface Flying {
/* @return average speed */
int flyDistance(int distance);
}
public interface Swimming {
/* @return average speed */
double swimDistance(double weight);
}
Client client = new BirdClient();
client.lookup(Flying.class)
.ifPresent(flying -> System.out.printf("Avg speed: %f%n",
flying.flyDistance(50)));
In PHP instead of Optional
null etcetera.
Reason
The evaluation needs structured data, specific to the evaluation class. The classes are open ended.
If you would have the evaluation class in the client as a very general class, you still would not have the data interface appliable on the world data.
So you must start with the specific class (Flying/Swimming), and then ask the client whether it can do that.
You might also be more open, have 2 evaluation strategies for a client, old and new, or on for other aspects, time and money.
And the other reason: this is evaluation centric; you start with the processing: the evaluation: as interface. And then you tailor your client as such. It is much less obvious if you have a "data object" and therein have to store the data object for this evaluation strategy, and so on, swimming in muddy waters.
EvaluatorInterface
andDataInterface
should have one single data type that they operate on, and those should be the same type, i.e. you would prefer if php had generics?Client<SpecialDataInterface><SpecialEvaluatorInterface>
. But I thought there might be a way without "fancy" language features and just simple, well considered OO Design ;)