A friend of mine who I occasionally help with development tasks came to me the other day asking about starting a project which would involve leveraging an API.
Given that I develop APIs and domain architecture, this seemed like a pretty good fit for my talents. I can produce the business logic and a simple chrome extension that talks to it and then we can explore further opportunities later, knowing that everything has been nicely abstracted.
I've coded against a few public APIs in my time for various things ranging from social media to push messaging. Although each time, I'm always left feeling like what I did was never a first-class citizen in my project. In many cases with APIs, you're only leveraging the external model, but in the case of my new project, I actually plan on "extending" it...
Read on if you're interested in any of the techniques involved, including repositories and project structure.
This package consists of models, repositories and if necessitated in any way, could probably even contain domain services. Although I'd say 99% of the time, most API access is so simple, a domain service would not be necessary. Agian though, it's not completely ruled out, so if there's additional behaviour involved with accessing your remote API, the option is there.
Given that I develop APIs and domain architecture, this seemed like a pretty good fit for my talents. I can produce the business logic and a simple chrome extension that talks to it and then we can explore further opportunities later, knowing that everything has been nicely abstracted.
I've coded against a few public APIs in my time for various things ranging from social media to push messaging. Although each time, I'm always left feeling like what I did was never a first-class citizen in my project. In many cases with APIs, you're only leveraging the external model, but in the case of my new project, I actually plan on "extending" it...
Read on if you're interested in any of the techniques involved, including repositories and project structure.
Extension Defined
When working with an external set of data, you have at least two choices. You can either harvest data from it and blend it into your own schema, or you can decide that the external representation of data is in fact part of your internal domain.
Because my entire solution targets the external platform, it made more sense to me to codify that in my abstraction. This is so that I always remain compatible and more aware of changes on their end - which I am in fact responsible for adapting to!.
This is also good when you don't necessarily want or need to take a local copy of the external data, but only maintain what you're adding to the schema. Keep in mind, there is a sliding scale here which my design can address. You don't want to be performing API requests for every small anonymous operation if that's your use case, consider caching in those scenarios.
If however, your project is mostly background work, there might be a justification for not storing the data, let alone letting it get stale. Again, it's a sliding scale which is an exercize left to the reader (for now).
Familiar Constructs
As I've been exploring non-leaky project architectures, a natural solution (that I don't take full credit for) that occured to me is to wrap the external API in a domain layer. Very similar to what you might make as a package to then depend on in your framework project, this is a repository layer that that instead of talking to MySQL, Postgres or MongoDB, talks to HTTP!This package consists of models, repositories and if necessitated in any way, could probably even contain domain services. Although I'd say 99% of the time, most API access is so simple, a domain service would not be necessary. Agian though, it's not completely ruled out, so if there's additional behaviour involved with accessing your remote API, the option is there.
- Project
- Uses services found in your domain library in controllers, filters, middleware, commands, etc...
- Depends on specific storage like MySQL, Postgres or MongoDB as a package
- Depends on your domain library as a package
- Contains repositories that interact with your storage and the api
- Implements domain library repository interfaces
- Composes the repository layer from your api/external domain library
- Domain Library
- Depends on your api/external domain library as a package
- Defines repository interfaces that extend api domain library interfaces
- Does not implement them!
- Provides domain services that perform business logic using domain library interfaces
- Instances of classes implementing domain library interfaces typically originate from the project (and usually with the help of a DI setup)
- Defines models that extend api/external domain library models
- API/External Library
- Defines a repository interface
- Implements the repository interface
- Defines models
Thinking it Through
In my situation, what I ended up doing was creating a base repository class in my api/external library that abstracts away the minutae of calling the API I'm wrapping. I made it able to return raw, array or domain objects from the API, in effect making it behave like a very primitive object-api-manager.
There will usually be specifics when it comes to implementing these kinds of things that will depend on what you're accessing. But the nice part is that there is now somewhere to hide these abstractions from the layers that only wish to consume business objects. In my case, when it's time to update how the API is accessed, or add support for a new endpoint or feature, anyone reading my code knows exactly where to go!
Some advice I like to offer when people are serious about their domain design:
- Don't use ORMs or libraries that require you to subclass a base model type
- Stay away from active-record-like systems which conflate repository and model
- Refrain from putting methods in your models that require access to global or injected infrastructure - those methods belong in a service
- Store data as attributes/properties on your model so that tools can reflect on them
Moving outwards from the external/api repositories, you reach actual project domain. This layer extends the external/api repositories and models to remain signature compatible while also adding it's own specifics.
The new interfaces will later be implemented from within the more concrete project where persistance and other niceties are established. That means there are no interface implementations in the domain library, but instead domain service implementations. This is another composable layer where you work with your models and is where you really see things pay off. It can seem a little tricky here, but if you refer to the diagram above, it's simply that your project is going to implement what your domain services depend on, that's all.
Finally, you have your project. Which after all these efforts should be implementing your final repository interfaces as mentioned above and making use of the models and services they facilitate. Which, if that's the case, congratulations! Ideally you'll also have a dependency injection container to weave it all together. Depending on what tools you're using that may require some configuration.
Don't worry, it's still possible to maintain idiomatic syntax while following these guidelines. Especially if you use an ORM like doctrine which is able to hydrate relations transparently, just pick best-of-breed tools that keep your concerns in mind.
Why?
I probably use this heading a little too often, but it's a good question to be continuously asking.
After all the toil of extracting this separation, you might be curious as to what it nets you. Well, if you review my examples thus far, you might have noticed that I use the word "package" a fair bit. For every level of separation here (3 in total), you actually have a clean break between layers of code. (In my situation, they are self-contained composer packages that can neatly track issues and changes without getting confused with irrelevant details.)
In the first package, you have your base project which will be where the rubber meets the road. Once you decide upon a framework, you should also have an idea of how you will be persisting your data and what the entry points for your functionality are. All those dependencies are maintained at the project level.
In the second and third packages, you have what defines your domain as it is known without the framework.
That's an uncommon consideration these days, as people are encouraged to put this code in their base projects in the name of rapid development. And it isn't necessarily wrong from that point of view. But the tendency has been that it ends up introducing unnecssary coupling to things like requests, sessions and error feedback. Over time, maintainability will suffer and you'll end up with the usual big ugly codebase nobody likes to work on or make sense of.
By maintaining separate domain packages, you guarantee yourself the ability to transition to new frameworks as they come along (YAGNI alert!) and more importantly - a clean separation between business logic and request handling. If you enjoy any modest level of success, you will also observe some benefits when it comes to managing scope and how hard your code is on your databases (scalability!).
As always, it's your responsibility as the project architect to look into your crystal ball and know what the future might be like. If your project is going to be more than a prototype, it's in your best interest to consider these changes early as you get started or transition from a proof of concept.
Conclusion
So, I hope you've enjoyed this quick little tour of how you can produce a domain layer that accesses an API. I encourage you to look at my new toggl sdk which employs the first part of these designs. Everything I've written here and the diagram above is more than enough to infer how to complete the rest if you're interested in trying it out.
What I've written here is not exclusive to PHP. I think after adapting to whatever constructs are available in each environment, these guidelines can be applied to many different languages.
If you're interested in knowing my specific stack, right now I'm keeping up with Laravel 5 (which is super alpha at the moment), and using my laravel-mongodb package for persistence. The rest of my project is private code, but I've shared as much as I can here with you and on github! :)
Comments
Post a Comment