refactor: set global architecture and refactor code

- Create an Ioc container and set factories and services
- Adjust degrading Strategies
- Clean up and do global chores here and there and avoid Dry code
- Write unit and integration tests and ensure correct business logic
This commit is contained in:
naciriii 2022-03-30 05:34:47 +01:00
parent 181b48aff7
commit 66867fbe7a
17 changed files with 613 additions and 83 deletions

View File

@ -1,35 +1,38 @@
{
"name": "emilybache/gilded-rose-refactoring-kata",
"description": "A kata to practice refactoring, tests and polymorphism",
"require": {
"php": "^7.3 || ^8.0"
"name": "emilybache/gilded-rose-refactoring-kata",
"description": "A kata to practice refactoring, tests and polymorphism",
"require": {
"php": "^7.3 || ^8.0"
},
"autoload": {
"psr-4": {
"GildedRose\\": "src/"
},
"autoload": {
"psr-4": {
"GildedRose\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"phpstan/phpstan": "^0.12.85",
"phpstan/phpstan-phpunit": "^0.12.18",
"symplify/easy-coding-standard": "^9.3",
"symplify/phpstan-extensions": "^9.3",
"approvals/approval-tests": "^1.4"
},
"scripts": {
"checkcode": "phpcs src tests --standard=PSR12",
"fixcode": "phpcbf src tests --standard=PSR12",
"test": "phpunit",
"tests": "phpunit",
"test-coverage": "phpunit --coverage-html build/coverage",
"check-cs": "ecs check",
"fix-cs": "ecs check --fix",
"phpstan": "phpstan analyse --ansi"
"files": [
"config/ioc.php"
]
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"phpstan/phpstan": "^0.12.85",
"phpstan/phpstan-phpunit": "^0.12.18",
"symplify/easy-coding-standard": "^9.3",
"symplify/phpstan-extensions": "^9.3",
"approvals/approval-tests": "^1.4"
},
"scripts": {
"checkcode": "phpcs src tests --standard=PSR12",
"fixcode": "phpcbf src tests --standard=PSR12",
"test": "phpunit",
"tests": "phpunit",
"test-coverage": "phpunit --coverage-html build/coverage",
"check-cs": "ecs check",
"fix-cs": "ecs check --fix",
"phpstan": "phpstan analyse --ansi"
}
}

24
php/config/ioc.php Normal file
View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
use GildedRose\Services\AbstractDegradingStrategyFactory;
use GildedRose\Services\DegradingStrategyFactory;
/**
* Here We set Bindings between Interfaces and abstraction to implementation
*/
const BINDINGS =
[
AbstractDegradingStrategyFactory::class => DegradingStrategyFactory::class
];
// Helper Method to get implementation
function resolve(string $interface)
{
try {
return BINDINGS[$interface];
} catch (Throwable $e) {
throw new Exception('Cannot Find Implementation');
}
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use GildedRose\Core\Container;
use GildedRose\GildedRose;
use GildedRose\Item;
@ -22,7 +23,7 @@ $items = [
new Item('Conjured Mana Cake', 3, 6),
];
$app = new GildedRose($items);
$app = (new Container())->get(GildedRose::class, [$items]);
$days = 2;
if (count($argv) > 1) {

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace GildedRose\Core;
use Exception;
use Psr\Container\ContainerInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
class Container implements ContainerInterface
{
private $deps = [];
/**
* Get Dependency and Respect Dependency Inversion principle
* The Interface/Class Binding is gotten from ioc config file
* Where we set bindings between interfaces,custom classes,aliases.. mostly anything can be injected
*/
public function get(string $name, array $args = null): object
{
if (! isset($this->deps[$name])) {
$this->deps[$name] = $name;
}
try {
$reflection = new ReflectionClass($this->deps[$name]);
if (! $reflection->isInstantiable()) {
if (! $reflection->isInterface()) {
throw new Exception("{$reflection->getName()} is not instantiable !");
}
$reflection = new ReflectionClass(resolve($reflection->getName()));
}
} catch (ReflectionException $e) {
throw new Exception($e->getMessage());
}
$constructor = $reflection->getConstructor();
return $constructor === null ?
$reflection->newInstanceArgs() :
$reflection->newInstanceArgs($this->resolveArgs($constructor, $args));
}
public function has(string $name): bool
{
return isset($this->deps[$name]);
}
private function resolveArgs(ReflectionMethod $constructor, $args)
{
$args = $args ?? [];
$params = $constructor->getParameters();
foreach ($params as $param) {
if ($param->getType() !== null && ! $param->getType()->isBuiltin()) {
if (! array_key_exists($param->getName(), $args)) {
$args[] = $this->get(
$param->getType()->getName()
);
}
} elseif ($param->isDefaultValueAvailable()) {
$args[] = $param->getDefaultValue();
}
}
return array_values($args);
}
}

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace GildedRose;
use GildedRose\Services\AbstractDegradingStrategyFactory;
final class GildedRose
{
/**
@ -11,59 +13,31 @@ final class GildedRose
*/
private $items;
public function __construct(array $items)
/**
* Degrading Strategy Factory
* @var AbstractDegradingStrategyFactory
*/
private $degradingStrategyFactory;
public function __construct(array $items, AbstractDegradingStrategyFactory $degradingStrategyFactory)
{
$this->items = $items;
$this->degradingStrategyFactory = $degradingStrategyFactory;
}
public function __get($name)
{
return property_exists($this, $name) ? $this->{$name} : null;
}
/**
* Handle Updating quality in cleaner way respecting multiple software engineering principles
* Such as Solid/Dry etc
*/
public function updateQuality(): void
{
foreach ($this->items as $item) {
if ($item->name != 'Aged Brie' and $item->name != 'Backstage passes to a TAFKAL80ETC concert') {
if ($item->quality > 0) {
if ($item->name != 'Sulfuras, Hand of Ragnaros') {
$item->quality = $item->quality - 1;
}
}
} else {
if ($item->quality < 50) {
$item->quality = $item->quality + 1;
if ($item->name == 'Backstage passes to a TAFKAL80ETC concert') {
if ($item->sell_in < 11) {
if ($item->quality < 50) {
$item->quality = $item->quality + 1;
}
}
if ($item->sell_in < 6) {
if ($item->quality < 50) {
$item->quality = $item->quality + 1;
}
}
}
}
}
if ($item->name != 'Sulfuras, Hand of Ragnaros') {
$item->sell_in = $item->sell_in - 1;
}
if ($item->sell_in < 0) {
if ($item->name != 'Aged Brie') {
if ($item->name != 'Backstage passes to a TAFKAL80ETC concert') {
if ($item->quality > 0) {
if ($item->name != 'Sulfuras, Hand of Ragnaros') {
$item->quality = $item->quality - 1;
}
}
} else {
$item->quality = $item->quality - $item->quality;
}
} else {
if ($item->quality < 50) {
$item->quality = $item->quality + 1;
}
}
}
$this->degradingStrategyFactory->getDegradingStrategy($item)->handle();
}
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace GildedRose\Services;
use GildedRose\Item;
use GildedRose\Services\Strategies\BaseStrategy;
interface AbstractDegradingStrategyFactory
{
public const AGED = 'Aged Brie';
public const SULFURAS = 'Sulfuras, Hand of Ragnaros';
public const BACKSTAGE = 'Backstage passes to a TAFKAL80ETC concert';
public const CONJURED = 'Conjured Mana Cake';
public function getDegradingStrategy(Item $item): BaseStrategy;
}

View File

@ -0,0 +1,56 @@
<?php
namespace GildedRose\Services;
use GildedRose\Core\Container;
use GildedRose\Item;
use GildedRose\Services\Strategies\AgedBrieStrategy;
use GildedRose\Services\Strategies\BackstageStrategy;
use GildedRose\Services\Strategies\BaseStrategy;
use GildedRose\Services\Strategies\ConjuredStrategy;
use GildedRose\Services\Strategies\DefaultStrategy;
use GildedRose\Services\Strategies\SulfurasStrategy;
/**
* DegradingStrategy builder that returns strategy based on item following Factory Design pattern
*/
class DegradingStrategyFactory implements AbstractDegradingStrategyFactory
{
private $container;
public function __construct(Container $container)
{
$this->container = $container;
}
public function getDegradingStrategy(Item $item): BaseStrategy
{
switch ($item->name) {
case self::AGED:
return $this->container->get(AgedBrieStrategy::class, [
'item' => $item,
]);
break;
case self::BACKSTAGE:
return $this->container->get(BackstageStrategy::class, [
'item' => $item,
]);
break;
case self::SULFURAS:
return $this->container->get(SulfurasStrategy::class, [
'item' => $item,
]);
break;
case self::CONJURED:
return $this->container->get(ConjuredStrategy::class, [
'item' => $item,
]);
break;
default:
return $this->container->get(DefaultStrategy::class, [
'item' => $item,
]);
}
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace GildedRose\Services\Strategies;
use GildedRose\Item;
class AgedBrieStrategy extends DefaultStrategy
{
public function setSellIn(): Item
{
$this->item->sell_in--;
return $this->item;
}
public function setQuality(): Item
{
$this->item->quality = ($this->item->quality + $this->degradeRate) > self::MAX_QUALITY_VALUE ?
self::MAX_QUALITY_VALUE : $this->item->quality += $this->degradeRate;
return $this->item;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace GildedRose\Services\Strategies;
use GildedRose\Item;
class BackstageStrategy extends DefaultStrategy
{
public function setDegradeRate()
{
if ($this->item->sell_in <= 0) {
$this->degradeRate = -$this->item->quality;
} elseif ($this->item->sell_in <= 5) {
$this->degradeRate = 3;
} elseif ($this->item->sell_in <= 10) {
$this->degradeRate = 2;
}
}
public function setSellIn(): Item
{
$this->item->sell_in--;
return $this->item;
}
public function setQuality(): Item
{
$this->item->quality = ($this->item->quality + $this->degradeRate) > self::MAX_QUALITY_VALUE ?
self::MAX_QUALITY_VALUE : $this->item->quality += $this->degradeRate;
return $this->item;
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace GildedRose\Services\Strategies;
use GildedRose\Item;
interface BaseStrategy
{
public const MAX_QUALITY_VALUE = 50;
public const MIN_QUALITY_VALUE = 0;
public function __construct(Item $item);
public function setSellIn(): Item;
public function setQuality(): Item;
public function handle();
}

View File

@ -0,0 +1,21 @@
<?php
namespace GildedRose\Services\Strategies;
use GildedRose\Item;
class ConjuredStrategy extends DefaultStrategy
{
public function setSellIn(): Item
{
$this->item->sell_in--;
return $this->item;
}
public function setQuality(): Item
{
$this->item->quality = ($this->item->quality - ($this->degradeRate * 2)) < self::MIN_QUALITY_VALUE ?
self::MIN_QUALITY_VALUE : $this->item->quality -= ($this->degradeRate * 2);
return $this->item;
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace GildedRose\Services\Strategies;
use GildedRose\Item;
/**
* Behaves as a default Strategy and template for other strategies
* based on Template Method Design pattern and factory method design pattern for setDegrateRate method
* in a way to share common code and
*/
class DefaultStrategy implements BaseStrategy
{
protected $degradeRate = 1;
protected $item;
public function __construct(Item $item)
{
$this->item = $item;
}
public function setDegradeRate()
{
if ($this->item->sell_in < 0) {
$this->degradeRate *= 2;
}
}
public function setSellIn(): Item
{
$this->item->sell_in--;
return $this->item;
}
public function setQuality(): Item
{
$this->item->quality = ($this->item->quality - $this->degradeRate) < self::MIN_QUALITY_VALUE ?
self::MIN_QUALITY_VALUE : $this->item->quality -= $this->degradeRate;
return $this->item;
}
/**
* Template Method that runs steps for all strategies in order
* First we Downgrade the sellIn
* Second We set the degradeRate/riseRate
* Finally we Set the item quality based on degrate/rise Rate
*/
final public function handle()
{
$this->setSellIn();
$this->setDegradeRate();
$this->setQuality();
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace GildedRose\Services\Strategies;
use GildedRose\Item;
class SulfurasStrategy extends DefaultStrategy
{
public function setSellIn(): Item
{
return $this->item;
}
public function setQuality(): Item
{
return $this->item;
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Tests;
use GildedRose\Item;
use GildedRose\Services\AbstractDegradingStrategyFactory;
use GildedRose\Services\Strategies\ConjuredStrategy;
use GildedRose\Services\Strategies\DefaultStrategy;
use GildedRose\Services\Strategies\SulfurasStrategy;
use PHPUnit\Framework\TestCase;
class DegradingStrategyFactoryTest extends TestCase
{
use InitApp;
public $degradingStrategyFactory;
public $container;
protected function setUp(): void
{
$this->init();
$this->degradingStrategyFactory = $this->container->get(AbstractDegradingStrategyFactory::class);
}
public function testStrategyFactoryIsLoaded(): void
{
$this->assertNotNull($this->degradingStrategyFactory);
$this->assertInstanceOf(AbstractDegradingStrategyFactory::class, $this->degradingStrategyFactory);
}
public function testLoadsTheCorrectStrategy()
{
$item = $this->container->get(Item::class, ['Foo', 2, 3]);
$this->assertInstanceOf(DefaultStrategy::class, $this->degradingStrategyFactory->getDegradingStrategy($item));
$item = $this->container->get(Item::class, ['Sulfuras, Hand of Ragnaros', 0, 80]);
$this->assertInstanceOf(SulfurasStrategy::class, $this->degradingStrategyFactory->getDegradingStrategy($item));
$item = $this->container->get(Item::class, ['Conjured Mana Cake', 3, 6]);
$this->assertInstanceOf(ConjuredStrategy::class, $this->degradingStrategyFactory->getDegradingStrategy($item));
}
}

View File

@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace Tests;
use GildedRose\Item;
use GildedRose\Services\AbstractDegradingStrategyFactory;
use PHPUnit\Framework\TestCase;
class DegradingStrategyTest extends TestCase
{
use InitApp;
public $degradingStrategyFactory;
public $container;
protected function setUp(): void
{
$this->init();
$this->degradingStrategyFactory = $this->container->get(AbstractDegradingStrategyFactory::class);
}
public function testDefaultStrategy(): void
{
$item = $this->container->get(Item::class, ['Foo', 2, 3]);
$this->getStrategy($item)->handle();
$this->assertSame(2, $item->quality);
$this->assertSame(1, $item->sell_in);
$item = $this->container->get(Item::class, ['Foo', -1, 3]);
$this->getStrategy($item)->handle();
$this->assertSame(1, $item->quality);
$this->assertSame(-2, $item->sell_in);
}
public function testAgedBrieStrategy(): void
{
$item = $this->container->get(Item::class, ['Aged Brie', 2, 30]);
$this->getStrategy($item)->handle();
$this->assertSame(31, $item->quality);
$this->assertSame(1, $item->sell_in);
for ($i = 0; $i < 10; $i++) {
$this->getStrategy($item)->handle();
}
$this->assertSame(50, $item->quality);
}
/**
* "Backstage passes", like aged brie, increases in Quality as its SellIn value approaches;
* Quality increases by 2 when there are 10 days or less and by 3 when there are 5 days or less but
* Quality drops to 0 after the concert
* Yet the results are not correct under approvals/ , as the sell_in reaches 0 the quality stand still
* and only drop to 0 when sell_in < 0
* As same for increasing by 2 when sell_in reaches 10, it only increases by 2 when it's < 10
* As same also for sell_in reaching 5 it does not increase by 3 only after sell_in is lower than 5
*/
public function testBackstageStrategy(): void
{
$item = $this->container->get(Item::class, ['Backstage passes to a TAFKAL80ETC concert', 15, 20]);
$this->getStrategy($item)->handle();
$this->assertSame(21, $item->quality);
$this->assertSame(14, $item->sell_in);
$item->sell_in = 10;
$this->getStrategy($item)->handle();
$this->assertSame(23, $item->quality);
$item->sell_in = 5;
$this->getStrategy($item)->handle();
$this->assertSame(26, $item->quality);
$item->sell_in = 0;
$this->getStrategy($item)->handle();
$this->assertSame(0, $item->quality);
}
/**
* "Conjured" items degrade in Quality twice as fast as normal items
* though the results under approvals/ are not correct since it doesn't
* degrade as mentioned in requirements, it degrades same as AgedBrie
*/
public function testConjuredStrategy(): void
{
$item = $this->container->get(Item::class, ['Conjured Mana Cake', 3, 8]);
$this->getStrategy($item)->handle();
$this->assertSame(6, $item->quality);
$this->assertSame(2, $item->sell_in);
$item->sell_in = 1;
$this->getStrategy($item)->handle();
$this->assertSame(4, $item->quality);
$item->sell_in = 0;
$this->getStrategy($item)->handle();
$this->assertSame(0, $item->quality);
}
public function testSulfurasStrategy(): void
{
$item = $this->container->get(Item::class, ['Sulfuras, Hand of Ragnaros', 5, 80]);
$this->getStrategy($item)->handle();
$this->assertSame(80, $item->quality);
$this->assertSame(5, $item->sell_in);
$item->sell_in = 1;
$this->getStrategy($item)->handle();
$this->assertSame(80, $item->quality);
$this->assertSame(1, $item->sell_in);
$item->sell_in = 0;
$this->getStrategy($item)->handle();
$this->assertSame(80, $item->quality);
$this->assertSame(0, $item->sell_in);
}
private function getStrategy($item)
{
return $this->degradingStrategyFactory->getDegradingStrategy($item);
}
}

View File

@ -6,15 +6,43 @@ namespace Tests;
use GildedRose\GildedRose;
use GildedRose\Item;
use GildedRose\Services\AbstractDegradingStrategyFactory;
use PHPUnit\Framework\TestCase;
class GildedRoseTest extends TestCase
{
public function testFoo(): void
use InitApp;
private $items;
private $app;
protected function setUp(): void
{
$items = [new Item('foo', 0, 0)];
$gildedRose = new GildedRose($items);
$gildedRose->updateQuality();
$this->assertSame('fixme', $items[0]->name);
$this->init();
$this->items = [$this->container->get(Item::class, ['Aged Brie', 2, 0])];
$this->bootstrap();
}
public function testApplicationIsInstanciatedCorrectly(): void
{
$this->assertNotNull($this->app);
$this->assertInstanceOf(GildedRose::class, $this->app);
$this->assertInstanceOf(AbstractDegradingStrategyFactory::class, $this->app->degradingStrategyFactory);
}
public function testItemsAreSetAndCorrect()
{
$this->assertIsArray($this->items);
$this->assertIsArray($this->app->items);
$this->assertContainsOnlyInstancesOf(Item::class, $this->app->items);
$this->assertSame(new Item('Aged Brie', 2, 0), array_pop($this->items));
}
public function testUpdateQuality()
{
$this->app->updateQuality();
$this->assertSame(new Item('Aged Brie', 1, 1), $this->app->items[0]);
}
}

24
php/tests/InitApp.php Normal file
View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Tests;
use GildedRose\Core\Container;
use GildedRose\GildedRose;
/**
* Small Trait to speed up bootstrapping process and avoid Dry code
*/
trait InitApp
{
public function init()
{
$this->container = new Container();
}
public function bootstrap()
{
$this->app = $this->container->get(GildedRose::class, [$this->items]);
}
}