A typical service class often mixes read methods (e.g., getById, getLatestPosts) and write methods (e.g., create, publish, delete). This violates the Single Responsibility Principle and complicates refactoring, such as adding caching. Since caching is typically only relevant for read operations, you should separate these responsibilities into distinct classes or interfaces.
To implement this, keep your write-only service (e.g., PostService) and create a separate interface (e.g., PostQueries) for read operations. This allows you to use the Decorator pattern to add caching to the read operations without affecting the write logic.
final class PostService
{
public function create(PostCreateDto $dto){}
public function publish($postId){}
public function delete($postId){}
}
interface PostQueries
{
public function getById($id): Post;
public function getLatestPosts(): array;
public function getAuthorPosts($authorId): array;
}
final class DatabasePostQueries implements PostQueries{}
final class CachedPostQueries implements PostQueries
{
public function __construct(
private PostQueries $baseQueries,
private Cache $cache,
) {}
public function getById($id): Post
{
return $this->cache->remember('post_' . $id,
function() use($id) {
return $this->baseQueries->getById($id);
});
}
}