Design patterns are very important for every developer. it solves very common problems in every project you build.
Decorator pattern definition:
It helps you to add extra behavior on an object without affecting the other objects from the same class, will see what does that means.
Wikipedia:
the decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class
The Problem
Let's say we have a Post model
class Post extends Model
{
public function scopePublished($query) {
return $query->where('published_at', '<=', 'NOW()');
}
}
and in our PostsController we have the index method like below
class PostsController extends Controller
{
public function index() {
$posts = Post::published()->get();
return $posts;
}
}
in order to cache posts and avoid querying the database every time we need to list posts we can do the following
class PostsController extends Controller
{
public function index() {
$minutes = 1440; # 1 day
$posts = Cache::remember('posts', $minutes, function () {
return Post::published()->get();
});
return $posts;
}
}
Now we are caching posts for 1 day. but looking at the code. the controller knows too much. it knows how many days we cache, it initiates the cache object by himself.
also, imagine you are implementing the same for Tags, Categories, Archives for your HomePageController. it would be too much to read and maintain.
Repository pattern
for most of the cases, the Repository pattern is connected to the decorator pattern and you will see how.
First, let's separate how we get the posts using the Repository pattern, create app/Repositories/Posts/PostsRepositoryInterface.php
with the following content
namespace App\Repositories\Posts;
interface PostsRepositoryInterface
{
public function get();
public function find(int $id);
}
create PostsRepository
in the same path with the following content
namespace App\Repositories\Posts;
use App\Post;
class PostsRepository implements PostsRepositoryInterface
{
protected $model;
public function __construct(Post $model) {
$this->model = $model;
}
public function get() {
return $this->model->published()->get();
}
public function find(int $id) {
return $this->model->published()->find($id);
}
}
getting back to the PostsController and apply the changes to be like
namespace App\Http\Controllers;
use App\Repositories\Posts\PostsRepositoryInterface;
use Illuminate\Http\Request;
class PostsController extends Controller
{
public function index(PostsRepositoryInterface $postsRepo) {
return $postsRepo->get();
}
}
The controller became healthy and knows enough details to get the job done.
here we depend on Laravel's IOC to inject the concrete object of the Posts interface to get our posts
All we just need to do is to tell Laravel's IOC which class to create when using the interface.
In your app/Providers/AppServiceProvider.php
add the bind method
namespace App\Providers;
use App\Repositories\Posts\PostsRepositoryInterface;
use App\Repositories\Posts\PostsRepository;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(PostsRepositoryInterface::class,PostsRepository::class);
}
}
Now anytime we inject PostsRepositoryInterface
laravel will create an instance of PostsRepository
and give it back.
Implement caching through Decorator
We said at the beginning that, Decorator pattern allows behavior to be added to an individual object without affecting other objects from the same class.
Caching here is the behavior and the object/class is PostsRepository
Let's create PostsCacheRepository
in the same path app/Repositories/Posts/PostsCacheRepository.php
with the following content
namespace App\Repositories\Posts;
use App\Post;
use Illuminate\Cache\CacheManager;
class PostsCacheRepository implements PostsRepositoryInterface
{
protected $repo;
protected $cache;
const TTL = 1440; # 1 day
public function __construct(CacheManager $cache, PostsRepository $repo) {
$this->repo = $repo;
$this->cache = $cache;
}
public function get() {
return $this->cache->remember('posts', self::TTL, function () {
return $this->repo->get();
});
}
public function find(int $id) {
return $this->cache->remember('posts.'.$id, self::TTL, function () {
return $this->repo->find($id);
});
}
}
In this class, we accept the Caching object and PostsRepository object then we use the class (Decorator) to add caching behavior to the PostsReposiory instance.
we can use the same example to send HTTP requests to some service and then we fall back to the model in case of failure. I believe you got the benefit from the pattern and how it makes it easy to add behaviors.
The last thing is to modify AppServiceProvider interface binding to create PostsCacheRepository instance instead of PostsRepository
namespace App\Providers;
use App\Repositories\Posts\PostsRepositoryInterface;
use App\Repositories\Posts\PostsCacheRepository;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(PostsRepositoryInterface::class,PostsCacheRepository::class);
}
}
Check your files again now you will find it very easy to read and maintain. also, it's testable and if at some point you decided to remove the caching layer. you will just change the binding in the AppServiceProvider
and that's it. nothing extra needs to change.
Conclusions
- We learned how to cache models with the Decorator pattern
- We showed how the Repository pattern is connected to Decorator Pattern
- How DependecyInjection and Laravel IOC make our life easy
- How powerfull are laravel components
Hope you enjoyed reading the article. and it showed you the powerfull of design patterns and how it makes your project easy to maintain and manage .
Top comments (5)
Do more post like this one please.
Glad you like it and I'll do my best :)
This is not decorator. You have wrapped all the methods of the class.
This is not decoration. This is Proxy pattern.
Also I recommend looking to aspect-oriented programming for such tasks.
Awesome post!
Thanks, y Ahmed powerful explanation, waiting for more :)