amphp / sql

Common interfaces for Amp based SQL drivers.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

General API design issues

DaveRandom opened this issue · comments

NOTE: I will expand on some of the issues outlined below when I have more time.

It is particularly important that an API abstraction such as this has an API design that is as close as possible to "correct" from the outset, since it defines the structure of the code that uses it, and backwards-incompatible changes to an abstract API are even more difficult to manage than at the level of a concrete implementation.

I would like to raise a few issues I have with the current API:

Naming

These issues are all essentially down to unnecessary brevity, which will result in consuming code that is that little bit harder to work with. (N.B. this is a big fat IMHO)

  • ConnectionConfig#connectionString() should be getConnectionString() - method names need a verb. I realise this is effectively a property, but as we are currently stuck with using methods to implement these, the "rules" for method naming should be followed.
  • Link#transaction() should be beginTransaction(). Same basic reasoning as above, but more important in this case - the lack of a verb makes it slightly ambiguous, it's not possible to know about how this will behave from the name alone; it could be that if I call this when there is an existing transaction it will return a reference to the existing one rather than throwing an error, or queuing the request. Adding the verb removes the ambiguity and makes it easier to read the calling code.
  • Pool#getMaxConnections() is inconsistent with it's companion methods, which both use a ...Count() suffix in the names, It should be getMaxConnectionCount().

Interface segregation

The forgotten "I" in SOLID has been forgotten again. A number of interfaces explicitly define some unrelated operations, and even some functionally identical compatible methods are defined separately with no type relationship (todo: note examples)

The Operation interface

I suspect this interface doesn't make any sense in this abstraction layer. If it does make sense then there is definitely a naming problem.

As far as I am aware, the Operation interface is actually an implementation detail, essentially used to trigger code which frees underlying resources, and thereby avoid breaking encapsulation by exposing them directly. Both amp/mysql and amp/postgres have a matching interface, but the existing usage of it is inconsistent - postgres only uses it for transactions, whereas mysql also uses it for result sets and one of the concrete statement implementations (but not as part of the statement interface). This inconsistency betrays the fact that it is an implementation detail, and not part of a generic public API such as the one defined by this repo.

If it were concluded that it does make sense to codify it as part of the public API, then the naming should be changed. Operation#onDestruct() - one of these names is wrong, either the method should be named onComplete() or the interface should be named... something else more generic. "Destruction" is a generic event that can be applied to any type of object, not just those that encapsulate a logical operation. Given the way this relates to the other API elements defined in this repo, my gut says that the method should be named onComplete().

However, the semantics of this mechanism are identical to Promise#onResolve() and I wonder whether it might make more sense to simply turn it into interface Operation extends Promise {} (or get rid of it entirely and just use Promise directly), so that one could yield $operation.

No @throws tags

The exceptions thrown by a method are part of it's public API and should be defined at the interface level, so that code working with the generic interfaces knows what it needs to handle. The exceptions defined at this level should be abstract types (abstract classes or interfaces), requiring the implementor to define a specific concrete subtype.

Operation was always meant to be an implementation detail, but might be usable/required if someone wanted to implement their own version of Pool. Thus it entered the public API, but never something that a user of the library would be concerned with. Naming certainly can be improved, absolutely open to suggestions. The semantics are not identical to Promise. It is not a placeholder, but represents resources being used that should be released when the object is destroyed or the operation completes. Moreover, the objects implementing Operation are used to resolve promises, so implementing Promise will not work. Perhaps a name such as Resource with a method onComplete()?

I agree with your points on naming, reflecting some of my own thoughts. ConnectionConfig feels incomplete and perhaps should include some of the methods that could be common, such as getHost(), getPort(), etc.

Looks like some of the @throws tags were lost in translation or were missing in the first place. Definitely should be added here. Not sure if the exception classes need to be abstract though, would there be a reason to differentiate between a QueryError from Postgres and MySQL?

A few thoughts I like to add:

  • ConnectionConfig#connectionString() should be getConnectionString() - method names need a verb. I realise this is effectively a property, but as we are currently stuck with using methods to implement these, the "rules" for method naming should be followed.

I disagree with this, the get-prefix doesn't add any meaningful value. Tell - don't ask. Or to quote Vaughn Vernon: The method names of Side-Effect-Free Functions are important. Although these methods all return Values (because they are CQS query methods) they purposely avoid the use of the get-prefix JavaBean naming convention. This simple but effective approach to object design keeps the Value Object faithful to the Ubiquitous Language. The use of getValuePercentage() is a technical computer statement, but valuePercentage() is a fluent human-readable language expression.

So I'd vote for removing all get-prefixes, however I can live with it, because it's not a real problem for me to keep them.

  • I saw this newly added method in Pool: public function resetConnections(bool $reset). I don't like this at all, because I don't know what the bool parameter is for (I actually had to look in the code on how this is used, in order to understand it). I prefer more meaningful method-names (and no params) in this case. f.e. public function enableResetConnection() and public function disableResetConnection(). BUT... see my next point.

  • Then there is this: public function setIdleTimeout(int $timeout) in the AbstractPool. This means that the pool can change its behaviour from the outside, someone can somewhere change this timeout value and hence affect other parts of the system. I would prefer to remove all setters completely and inject those value either in the constructor, or, when it's too many, use a configuration object that groups those values together (also only injected in the constructor).

  • I don't think there is any difference in the Exception (f.e. QueryError), so one class in the SQL package to rule MySQL and Postgres implementation should be good.

  • Adding those methods getHost() (or simply host()), port(), etc to the ConnectionConfig sounds reasonable.

  • About the Operation class:

Perhaps a name such as Resource with a method onComplete()?

I like this suggestions, I would also add we can add an @internal annotation above that interface, so it's clear that no userland implementation should worry about this interface and make any use of it.

  • Renaming transaction to beginTransaction - I'm okay with that.

  • Adding the missing @throws annotations sounds reasonable as well.

I agree that setting the idle timeout and reset connection flag should only be injected in the constructor. Will update.

Operation should be deleted IMO and the __destruct handling be implemented via decorators. It's a slight overhead, but I'm fine with that, because it allows a way nicer abstraction.

the get-prefix doesn't add any meaningful value.

It allows typing get and getting a list of all things you can ask for, that's at least some value people often forget.

Tell - don't ask.

That's all fine, but getters are always "ask", never "tell".

@kelunik Yep, I agree that decorators would be a much better way to implement resource freeing. I should have time tomorrow to implement this and then I'll drop Operation from this library.

Do we still miss the @throws tags? Otherwise we've addressed most points in this issue, I think.

Everything done.