- Notifications
You must be signed in to change notification settings - Fork524
Creating interfaces to be more SOLID#529
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Conversation
willdurand commentedSep 9, 2016 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
To me, I would not add all these interfaces, because there are not different implementations for most of them (example the I get that interfaces are often good for coupling, but there I don't really see the use case honestly (and I likethe SOLID stuff). Of course, this is my very own point of view. |
unkind commentedSep 17, 2016
Usually, interfaces for VOs is a sign of bad design. Value objects are self-sufficient and very definite from behaviour point of view. There is no place for multiple behaviours here.
It makes client-code coupled with specific implementation and it defeats the original purpose of the abstraction. It makes sense to ask what exactly@egeloen wants to see in the |
Nyholm commentedOct 11, 2016
Thank you for valid input.. I've continued the work on this.. Will finish this week. |
Nyholm commentedOct 12, 2016 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
I'm done with this PR now and would like to have feedback. The major changes I've done is to introduce a I've also make sure that the I think it is a good idea to remove I decided not to add interfaces for Bounds, Coordinates, Country, AdminLevel and AdminLevelCollection. I assume (maybe falsely) that they are not subject to change. Ever... Please challenge me on this if you feel it's wrong. I would like to have much feedback on this. What do@egeloen and @geocoder-php/geocoder think? Future work:One could add vendor specific |
unkind commentedOct 12, 2016 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
I'm not sure I understand how it is supposed to work. Interface is limited to specific method list, there is no way to get an extra data. I suggested to make completely independent interfaces (or even classes), e.g. interface GoogleGeocoder{/** * @return GoogleAddress[] */publicfunctiongeocode(...);} I mean, if you really want to provide vendor-specific address objects, you would have almost same amount of work in terms of lines of code (at least, I think so), but you'd have clean API for every major providers.
Offtopic: |
unkind commentedOct 12, 2016 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
To make clear my point: finalclass GoogleGeocoder{publicfunction__construct(bool$useSsl, ...) { }publicfunctiongeocode(string$address):GoogleAddress {// ... }publicfunctiongeocodeWithin(string$address,BoundingBox$bbox):GoogleAddress {// This method is possible, because Google API allows this kind of query }}final GoogleMapsimplements Provider{publicfunction__construct(GoogleGeocoder$googleGeocoder) {// ... }publicfunctiongeocode() {// Converts GoogleAddress[] to AddressCollection }} Users will configure the |
Nyholm commentedOct 13, 2016
The idea is that you can always be sure that class GoogleGeocoderResultimplements GeocoderResult {// ... All method in the GeocoderResult interfacepublicfunctiongetQueryTime() {// Return the time it took to query the API }} And the
Yes, you would have to do interface GoogleGeocoder implements Geocoder{/** * @return GoogleGeocoderResult[] */publicfunctiongeocode(...);}interface GoogleGeocoderResult implements GeocoderResult{// ...publicfunctiongetQueryTime();} If an application author decides that he is only interested in using the Google geocoder he/she can type hint directly at the
I can rename the issue title if you want =)
Im not happy with that function either. It did already exist in the |
unkind commentedOct 13, 2016
Sure, but you open this knowledge to users like new kind of API, you legalize the magic with
I wouldn't try to make inheritance tree like this. In practice, it becomes harder to understand what's going on and reduces flexibility: you can't change contracts of different providers independently. You lock your provider with method Honestly, I'm not user of the library, so it's not fair for me to judge it. I can find reasons why it's not good in theory, but your approach may be way more practical. |
Nyholm commentedOct 13, 2016
I like having this discussion with you. Thank you for sharing your thoughts. The purpose of this library is to have a common contract for the geocoded result. If one feel that geocoder X is giving me poor results one should easily be able to switch to geocoder Y. (This is the Liskov substitution principle.) This is something we strongly believe in and should not be up for change. This contract is enforced by The reason I introduce the interfaces is that I want to open up the possibility for others to create a more specific result. If the application author feels it is important, he/she can "trade" Liskovs substitution principle to have this specific result. This will increase flexibility of the library. At the same time, he/she can still use the
The inheritance if only for interfaces. And yes, it reduces flexibility because we want to follow the Im not surewe want to introduce this extra |
unkind commentedOct 13, 2016
I think you missed my point, I never suggested to return different objects to break LSP. My point is if user wants to get more vendor-specific data/methods/abilities, he should use vendor-specific geocoder. Let's say we want to query city's coordinates by city name within specific country. Instead of finalclass UserlandService{publicfunction__construct(Geocoder$geocoder) {// ... }publicfunctionfindTheBestResult(string$cityName,string$countryCode):Coordinates {// Order is not guaranteed.foreach ($this->geocoder->geocode($cityName)as$position) {if ($positioninstanceof GooglePosition &&$position->getCoordinates()->isDefined() &&$position->getCountryCode() ===$countryCode ) {return$position->getCoordinates(); }if ($positioninstanceof OpenStreetMapPosition &&$position->getCoordinates()->isDefined() &&$position->getCountryCode() ===$countryCode) {return$position->getCoordinates(); } } }} I prefer finalclass UserlandService{publicfunction__construct(GoogleGeocoder$google,OpenStreetMapGeocoder$osm) {// ... }publicfunctionfindTheBestResult(string$cityName,string$countryCode):Coordinates {try {return$this->google->findFirstMatchWithCountry($cityName,$countryCode); }catch (NoResults$e) {foreach ($this->osm->geocode($cityName)as$address) {if ($address->getCountry()->getCode() ===$countryCode) {return$address->getCoordinates(); } } }throw NoResults::cityWithinCountry($cityName,$countryCode); }} Yes, I have 2 dependencies, but it makes code more explicit and more efficient. The providers have different abilities, different API, different methods. Google can directly execute my query, but OSM does not. Also, I can make very optimized queries for my use cases if specific provider allows it. The generic Geocoder, on other hand, run all possible providers with the same query. It makes him less efficient for this kind of task. Different cases may require different tools. Do you mind to provide the use case when the generic Geocoder would be better to work with vendor-specific stuff? |
Nyholm commentedOct 13, 2016 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
I think I did miss your point. I very much agree with your last post. That is my intention as well. That is exactly what Im trying to achieve with this PR. I do also want to allow something like this: finalclass UserlandService {// ...$googleResult =$this->google->geocode('foo');return$googleResult->getFirstResultThatAllowStreetView();} With the current code in master your preferred example and my example above is not possible. Ibelieve that this PR makes it possible with the best solution. |
nicholasruunu commentedOct 21, 2016
I like these changes! Possible improvements:
|
unkind commentedOct 21, 2016
In the radical case you'd end up with a marker interface: interface Location{}interface Geocoder{publicfunctiongeocode(...):Location;} It can be literally anything. Very flexible, so awesome. |
unkind commentedNov 7, 2016
How it is supposed to work? I mean, you can't make custom request here, because result is already here, right? Lazy loading magic? |
Nyholm commentedNov 7, 2016
By introducing interfaces weallow ourselves and third party libraries to extend the functionality whilestill keeping the contract of a provider.
class SuperCollectionimplements \Geocoder\Collection{// ... Implement methods in the Collection interfacepublicfunctiongetFirstResultThatAllowStreetView(){/** @var AwesomeLocation $item */foreach ($this->itemsas$item) {if ($item->hasStreetView()) {return$item; } }returnnull; }}class AwesomeLocationimplements \Geocoder\Location{// ... Implement methods in the Location interfacepublicfunctionhasStreetView() {// .. }} |
unkind commentedNov 7, 2016 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Yeah, I understand it. However, one of my original points with different interfaces of Geocoder was
It means that if Google, for example, provides Your value object ( |
Nyholm commentedNov 7, 2016
You could add that method, no worries. =)
The interface defines a value object. Third party implementation may do whatever as long as they follow the contract by the interface.
One does not exclude the other. My point is, and has always been, that if you introduce interfaces you will increase flexibility for the providers (open for extension). At the same time the interfaces specifies a contract which makes providers respect the Liskov substitution principle. I would be happy to discuss a PR where you introduce a But to create one interface for Google, one interface for Bing, one for ... etc is no good solution at all. Those interfaces are too specific and does not help interoperability. |
unkind commentedNov 7, 2016 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
The thing is you want to make
You want to hide same methods behind the generic interface, but when I suggest to make them explicit, you call it "not good solution at all". Do I miss something? :)
When one wants to use vendor-specific stuff, he doesn't need interoperability. You still can make generic and simple final GoogleGeocoderimplements Geocoder{publicfunctiongeocode():AddressCollection {// Usual generic contract }publicfunctiongeocodeSpecific():GoogleAddressCollection {// Same, but with extended methods }// More Google-specific methods}// orfinal GoogleGeocoder {publicfunctiongeocode(...): GoogleAddressCollection {// ... }// More Google-specific methods}final GoogleGeocoderAdapterimplements Geocoder{publicfunctiongeocode():AddressCollection {return$this->convertToGeneric($this->googleGeocoder->geocode(...)); }} When one wants to use generic geocoder, he uses |
Nyholm commentedNov 7, 2016
No I do not. You type hint for what you need. Do you need a
I put the common things in the Ie "vendor specific" are all wrong but "feature specific" are encouraged.
True and I very much agree. But in that case he doesn't need an interface either. =) |
unkind commentedNov 7, 2016 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Yes, as you can see my
Unfortunately, this class always will return the generic finalclass GeocoderThatSupportsSetBoundsimplements Geocoder{publicfunctiongeocode():CustomAddressCollection {// This implementation is broken }}
Wow, don't create 15 different providers if you don't need them. Create specific providers for most popular vendors. |
Nyholm commentedNov 7, 2016
Sure, my bad. I was thinking about actual interfaces when you said "interface". I did not consider "classes and interfaces".
That is not really how you would use it... interface GeocoderThatSupportsSetBounds {publicfunctionsetBounds(Bounds$bounds);}finalclass MyGeocoderimplements Geocoder, GeocoderThatSupportsSetBounds{publicfunctionsetBounds(Bounds$bounds) { }publicfunctiongeocode():Collection { }} You must always respect the
We currently have 19 providers in this package and 10 more in the geocoder-extra package.. |
unkind commentedNov 7, 2016
I see. I didn't suggest to make 19 specific providers. As I said, only popular ones (in other words, those providers which you want to extend with specific result objects). The rest would just simply implement the only
I wouldn't suggest to make mutable services, honestly, i.e.
And here Also, by replacing VOs with an interface, you make them harder to understand, to serialize, to replace-on-the-fly ( |
Nyholm commentedNov 7, 2016
Why? What is the win for that?
This PR does not include such change. It just allows the possibility. Feel free to argue about the implementation details of my example in when that PR comes around.
Nope. class MyGeocoderimplements Geocoder{publicfunctiongeocode():MyCollection {//.. }}class MyCollectionimplements Collection {// ..publicfunctionextraFeature();}class Consumer {publicfunction__construct(MyGeocoder$gecoder) {// }}
No no no. Do you mean that Guzzles Request object is harder to understand, serialize and "replace-on-the-fly" (?) because of PSR7? That is just ridiculous and it feels like you are reaching for arguments not to add 2 interfaces... |
unkind commentedNov 7, 2016 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Abstractions limit your models. I mean, it's OK to have more features to your generic Geocoder model, but sometimes it's OK to couple on specific provider. No limitations, precise vendor-specific API. Some providers can behave a little differently, have different arguments, return different results. For example, you can avoid nullable fields if you clearly know that the Foo provider doesn't support Abstraction has its cost and sometimes you don't want to pay it.
Because people want exactly it: they want to stick with specific provider(s) sometimes. They want to know origin of a data (they want explicitly say "first I need to check Google, then OSM"), they want to stick to google_location_id, they want to get station name from Yandex, etc.
Youcan't do that:https://3v4l.org/kv9pn
Not Guzzle Request by itself, but I think that PSR-7 is not example of great design, you are right. However, I don't think we can discuss it here. |
unkind commentedNov 10, 2016
To summarise, I see 3 options:
As I can see, there is no big difference in our solutions (especially with my second proposal). |
| use Geocoder\Model\Country; | ||
| /** | ||
| * A position is a single result from a Geocoder. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
that is not a position but a location, right?
willdurand commentedDec 2, 2016
Hey! I reviewed both the comments and the changes. I see interesting things in "both sides", but let me introduce something else: why not adding a "raw" attribute to the
Sure we don't have a solid contract but,heh, that's not the purpose of the lib anyway. 90% of the issues related to this PR are: "I'd like to get this information from Google Maps". |
Nyholm commentedDec 6, 2016
Thank you. I can agree with adding a I do aslo think I know ways of addressing the issues of 90% of those PRs. But this PR is a blocker for that. |
unkind commentedDec 6, 2016
Some interesting discussion about interfaces for value objects:https://gist.github.com/Ocramius/a9d6713867929fe23f36b35981befb0d |
willdurand commentedDec 6, 2016
If we go the "raw" way, then do we really need interfaces? Especially on VOs...? |
Nyholm commentedDec 6, 2016
Interfaces are never a bad thing. And Im not sure these are actually value objects. From Wikipedia:
This is not a "small object" nor a "simple entity". Theneed for a |
willdurand commentedDec 6, 2016
The only interface would rather be |
Nyholm commentedDec 6, 2016
I would like to argue for making RawAware a complementary interface or a part of an existing one. In other words: I do not like to see that RawAware is ouronly interface. |
unkind commentedDec 6, 2016 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
It's a strong statement. Interfaces can be sign of wrong abstraction. We don't need interface for
What exactly is not a VO?
In fact, I'm talking about 1-to-1 interfaces. I can agree if you have another methods, but I still don't see how would you use it without hacks like |
Nyholm commentedDec 7, 2016
@unkind I wrote the definition of a value object. Yes, interfacescould be a sign of wrong abstraction... But Im not inventing new interfaces, Im extracting interfaces from the models we got. I feel like you are not trying to move this PR to either be accepted or rejected. This discussion has been long as it is, there is no point of having a general (out of topic) discussion about interfaces in general. Im sorry if I have misunderstood or missinterpided your messages. About how one should use interfaces and different kinds of feature interfaces for the providers:https://3v4l.org/HdCVp I want to accept or reject this PR. I think all arguments are on the table. After this PR I want to make sure to complete all tasks before we can tag a stable release. I do not want to merge my own PR that's why I ask other maintainers to make the decision. |
unkind commentedDec 7, 2016
So, basically, you say "we have model
I'm definitely trying to show arguments against this PR. I like the idea about key-value |
willdurand commentedDec 22, 2016
Now let's continue the journey towards Geocoder 4.0 😉 |
Nyholm commentedDec 22, 2016
Thank you for merging |
Uh oh!
There was an error while loading.Please reload this page.
This PR relates to#523
To be more SOLID our value objects should have interfaces. This will allow others to write providers that may return more data than we have defined in our models.
This is WIP because Im not happy with the *Interface suffix. We need to come up with better names. Or, are we happy with names like
AddressInterface?This PR also removes the
toStringfunction in favor for__toStringTODO