Przejdź do treści

Zadanie 5. Branch By Abstraction

Nowy mikroserwis

Przełącz repozytorium na branch recommendations

git checkout recommendations

Pojawia się nowa usługa związana z rekomendacjami produktowymi.

Nowy mikroserwis nie zwraca żadnych danych.

Adres serwisu rekomendacji, wymagany do integracji znajduje się w zmiennej środowiskowej RECOMMENDATIONS_SERVICE_URL

Integracja naszej aplikacji z serwisem rekomendacji

Chcemy, aby nasz ProductLookupController zwracał w szczegółach produktu dodatkowo informacje o produktach rekomendowanych.

Response ma dodatkowo zawierać klucz recommendations, pod którym będzie lista identyfikatorów produktów rekomendowanych.

Dane te będziemy pobierać z mikroserwisu rekomendacji i odpowiednio prezentować w odpowiedzi na request do naszej aplikacji.

Te prace wymagają więcej zmian i zdecydowaliśmy, że wprowadzimy dodatkową warstwę abstrakcji.

1. Nowa flaga

Do tablicy w main/src/Flags.php dodaj klucz

<?php
return array(
    'show_recommendations_on_product_lookup' => false
);

2. Testy automatyczne

2.1. Zmień nazwę testu ProductLookupControllerTest::testControllerReturnsValidResponse na ProductLookupControllerTest::testControllerReturnsValidResponseWithRecommendationsDisabled

2.2. Dopisz kod sprawiający, by test został pominięty, gdy flaga jest włączona.

2.3. Uruchom pipeline.sh, by mieć pewność, że wszystko jest ok.

2.4. Commit.

2.5. Stwórz test ProductLookupControllerTest::testControllerReturnsValidResponseWithRecommendationsEnabled, który testuje zachowanie aplikacji, gdy flaga jest włączona. Dopisz do niego kod, który pominie test, gdy flaga jest wyłączona.

2.6. Uruchom pipeline.sh, by mieć pewność, że wszystko jest ok.

2.7. Commit.

Rozwiązanie
<?php

namespace Tbd\Main\Tests\Products;

use Tbd\Main\FeatureFlags\FeatureFlag;
use Tbd\Main\Products\Product;
use Tbd\Main\Products\ProductLookupController;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use React\Http\Message\ServerRequest;
use Tbd\Main\Products\ProductRepositoryInterface;
use Tbd\Main\Recommendations\RecommendationsServiceInterface;

class ProductLookupControllerTest extends TestCase
{
    public function testControllerReturnsValidResponseWithRecommendationsDisabled()
    {
        if(FeatureFlag::isEnabled('show_recommendations_on_product_lookup')){
            $this->markTestSkipped("Flag show_recommendations_on_product_lookup is enabled");
        }

        $request = new ServerRequest('GET', 'http://example.com/products/3');
        $request = $request->withAttribute("id", "3");

        $product = new Product(3, 'test', 'description', 100);

        $stub = $this->createMock(ProductRepositoryInterface::class);
        $stub->method('findProduct')
            ->will($this->returnValueMap([["3", $product]]));

        $controller = new ProductLookupController($stub);

        $response = $controller($request);

        $this->assertInstanceOf(ResponseInterface::class, $response);
        $this->assertEquals(200, $response->getStatusCode());
        $this->assertEquals('application/json', $response->getHeaderLine('Content-Type'));

        $output='{
    "name": "test",
    "description": "description",
    "price": 100.0
}';
        $this->assertEquals($output, (string) trim($response->getBody()));
    }

    public function testControllerReturnsValidResponseWithRecommendationsEnabled()
    {
        if(!FeatureFlag::isEnabled('show_recommendations_on_product_lookup')){
            $this->markTestSkipped("Flag show_recommendations_on_product_lookup is disabled");
        }

        $request = new ServerRequest('GET', 'http://example.com/products/3');
        $request = $request->withAttribute("id", "3");

        $product = new Product(3, 'test', 'description', 100);

        $stub = $this->createMock(ProductRepositoryInterface::class);
        $stub->method('findProduct')
            ->will($this->returnValueMap([["3", $product]]));

        $controller = new ProductLookupController($stub);

        $recoStub = $this->createMock(RecommendationsServiceInterface::class);
        $recoStub->method('getRecommendations')
            ->will($this->returnValueMap([[3, [1]]]));

        $controller->getDataProvider()->getImplementation()->setService($recoStub);

        $response = $controller($request);

        $this->assertInstanceOf(ResponseInterface::class, $response);
        $this->assertEquals(200, $response->getStatusCode());
        $this->assertEquals('application/json', $response->getHeaderLine('Content-Type'));

        $output='{
    "name": "test",
    "description": "description",
    "price": 100.0,
    "recommendations": [
        1
    ]
}';
        $this->assertEquals($output, (string) trim($response->getBody()));
    }

    public function testControllerReturns404Response()
    {
        $request = new ServerRequest('GET', 'http://example.com/products/3');
        $request = $request->withAttribute("id", "3");

        $stub = $this->createMock(ProductRepositoryInterface::class);
        $stub->method('findProduct')
            ->will($this->returnValueMap([["3", null]]));

        $controller = new ProductLookupController($stub);

        $response = $controller($request);

        $this->assertInstanceOf(ResponseInterface::class, $response);
        $this->assertEquals(404, $response->getStatusCode());
        $this->assertEquals('text/plain; charset=utf-8', $response->getHeaderLine('Content-Type'));

        $output='Product not found';
        $this->assertEquals($output, (string) trim($response->getBody()));
    }
}

3. Implementacja rozwiązania

3.1. Utwórz interfejs Tbd\Main\Products\ProductLookupDataProviderInterface

main/src/Products/ProductLookupDataProviderInterface.php
1
2
3
4
5
6
7
8
<?php

namespace Tbd\Main\Products;

interface ProductLookupDataProviderInterface
{
    public function getData(Product $product): array;
}

3.2. Utwórz klasę Tbd\Main\Products\ProductLookupStandardDataProvider, która implementuje powyższy interfejs i zwraca wynik zgodnie ze starą logiką.

Rozwiązanie
<?php

namespace Tbd\Main\Products;

class ProductLookupStandardDataProvider implements ProductLookupDataProviderInterface
{
    public function getData(Product $product): array
    {
        $data = [
            "name" => $product->title,
            "description" => $product->description,
            "price" => $product->price,
        ];
        return $data;
    }
}

3.3. Użyj instancji nowo utworzonej klasy w kontrolerze w miejsce starej logiki.

3.4. Uruchom pipeline.sh, by mieć pewność, że wszystko jest ok.

3.5. Commit.

3.6. Utwórz dodatkową warstwę abstrakcji - klasę Tbd\Main\Products\ProductLookupDataProviderAbstraction, która implementuje ten sam interfejs.

Niech ta klasa ma prywatną własność $implementation typu ProductLookupDataProviderInterface.

W konstruktorze, w zależności od stanu flagi show_recommendations_on_product_lookup ustaw własność $implementation. Jeśli flaga jest wyłączona, użyj tam instancji klasy ProductLookupStandardDataProvider.

Metoda getData() powinna być fasadą do metody getData() ustawionej implementacji.

Rozwiązanie
<?php

namespace Tbd\Main\Products;

use Tbd\Main\FeatureFlags\FeatureFlag;
use Tbd\Main\Recommendations\RecommendationsService;

class ProductLookupDataProviderAbstraction implements ProductLookupDataProviderInterface
{
    private ProductLookupDataProviderInterface $implementation;

    public function __construct(){
        if(FeatureFlag::isEnabled('show_recommendations_on_product_lookup')){

        }else {
            $this->implementation = new ProductLookupStandardDataProvider();
        }
    }

    public function getImplementation(): ProductLookupDataProviderInterface
    {
        return $this->implementation;
    }

    public function getData(Product $product): array
    {
        return $this->getImplementation()->getData($product);
    }
}

3.7. Użyj instancji ProductLookupDataProviderAbstraction w kontrolerze w miejsce ProductLookupStandardDataProvider.

3.8. Uruchom pipeline.sh, by mieć pewność, że wszystko jest ok.

3.9. Commit.

3.10. Utwórz klasę Tbd\Main\Products\ProductLookupWithRecommendationsDataProvider, która implementuje powyższy interfejs i zwraca wynik zgodnie z nową logiką.

Zaimplementuj konstruktor tak, by przyjmował zależność w postaci obiektu typu RecommendationsServiceInterface.

Rozwiązanie
<?php

namespace Tbd\Main\Products;

use Tbd\Main\Recommendations\RecommendationsServiceInterface;

class ProductLookupWithRecommendationsDataProvider implements ProductLookupDataProviderInterface
{
    private RecommendationsServiceInterface $service;

    public function __construct(RecommendationsServiceInterface $service){
        $this->setService($service);
    }

    public function setService(RecommendationsServiceInterface $service){
        $this->service = $service;
    }

    public function getData(Product $product): array
    {
        $recommendations = $this->service->getRecommendations($product->id);

        $data = [
            "name" => $product->title,
            "description" => $product->description,
            "price" => $product->price,
            "recommendations" => $recommendations
        ];
        return $data;
    }
}

3.11. Zmodyfikuj ProductLookupDataProviderAbstraction, tak by w przypadku włączonej flagi używać ProductLookupWithRecommendationsDataProvider jako implementacji.

Rozwiązanie
<?php

namespace Tbd\Main\Products;

use Tbd\Main\FeatureFlags\FeatureFlag;
use Tbd\Main\Recommendations\RecommendationsService;

class ProductLookupDataProviderAbstraction implements ProductLookupDataProviderInterface
{
    private ProductLookupDataProviderInterface $implementation;

    public function __construct(){
        if(FeatureFlag::isEnabled('show_recommendations_on_product_lookup')){
            $address = getenv('RECOMMENDATIONS_SERVICE_URL');
            $service = new RecommendationsService($address);
            $this->implementation = new ProductLookupWithRecommendationsDataProvider($service);
        }else {
            $this->implementation = new ProductLookupStandardDataProvider();
        }
    }

    public function getImplementation(): ProductLookupDataProviderInterface
    {
        return $this->implementation;
    }

    public function getData(Product $product): array
    {
        return $this->getImplementation()->getData($product);
    }
}

3.12. Przetestuj swoje rozwiązanie za pomocą testów automatycznych, uruchamiając je ze zmienną środowiskową, która włączy flagę

FEATURE_FLAG_SHOW_RECOMMENDATIONS_ON_PRODUCT_LOOKUP=1 composer run tests

3.13 Zmodyfikuj pipeline.sh, by uruchamiał testy drugi raz, ale z włączoną flagą

3.14. Uruchom pipeline.sh, by mieć pewność, że wszystko jest ok.

3.15. Commit.