commit 50ffd34e072eda3913768cd36d5b674b18dcf0be Author: Thibaud Date: Sun Apr 12 01:36:20 2020 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c4d9393 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +*.swp +*.*~ +project.lock.json +.DS_Store +*.pyc +nupkg/ + +# Visual Studio Code +.vscode + +# Rider +.idea + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +msbuild.log +msbuild.err +msbuild.wrn + +# Visual Studio 2015 +.vs/ + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..25c1c3c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,3 @@ +FROM mcr.microsoft.com/dotnet/core/sdk:3.1 +COPY app/ app/ +ENTRYPOINT ["dotnet", "test", "app/"] diff --git a/Dockerfile.debug b/Dockerfile.debug new file mode 100644 index 0000000..b434b5c --- /dev/null +++ b/Dockerfile.debug @@ -0,0 +1,3 @@ +FROM mcr.microsoft.com/dotnet/core/sdk:3.1 +COPY app/ app/ +ENTRYPOINT ["dotnet", "vstest", "--logger:trx", "app/VegetableShop.*Tests*"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b61ef6c --- /dev/null +++ b/Makefile @@ -0,0 +1,41 @@ +DOTNET="/usr/bin/dotnet" +SLN="./src/DDDDemo.sln" +TARGET="linux-x64" + +all: clean restore publish publish-release + +clean: + $(DOTNET) clean --verbosity=quiet $(SLN) + rm -rf release-$(TARGET) debug-$(TARGET) + +mrproper: clean + find src/ \( -name "bin" -o -name "obj" \) -exec rm -rf {} + + +restore: + $(DOTNET) restore --verbosity=quiet --runtime $(TARGET) $(SLN) + +build: + $(DOTNET) build $(SLN) + +publish: + $(DOTNET) restore --verbosity=quiet --runtime $(TARGET) $(SLN) + $(DOTNET) publish $(SLN) --no-restore \ + --verbosity=quiet \ + -c Debug \ + --output debug-$(TARGET) + +publish-release: + $(DOTNET) restore --verbosity=quiet --runtime $(TARGET) $(SLN) + $(DOTNET) publish $(SLN) --no-restore \ + --verbosity=quiet \ + -c Release \ + --output release-$(TARGET) + +run: + $(DOTNET) run $(SLN) + +list-tests: restore + $(DOTNET) test --no-restore --verbosity=quiet --list-tests $(SLN) + +tests: restore + $(DOTNET) test --logger=trx --no-restore --verbosity=quiet $(SLN) diff --git a/README.md b/README.md new file mode 100644 index 0000000..551b4e2 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# Domain Driven Design : des armes pour affronter la complexité + +[](https://blog.octo.com/domain-driven-design-des-armes-pour-affronter-la-complexite/) + + +« La complexité, c’est comme le cholestérol. Il faut surtout se débarasser du mauvais. » (Proverbe gascon-malgache) + + +DDD est l’acronyme de Domain Driven Design. Ce n’est ni un framework, ni une +méthodologie, mais plutôt une approche décrite dans l’ouvrage du même nom +d’Eric Evans. Un de ses objectifs est de définir une vision et un langage +partagés par toutes les personnes impliquées dans la construction d’une +application, afin de mieux en appréhender la complexité. Nous ne souhaitons pas +faire ici une présentation de DDD (voir plutôt ici pour une introduction). Nous +voulons montrer comment DDD peut adresser certaines problématiques évoquées +dans l’article “J’ai mal à mon application ! Ca se soigne ?” au travers d’un +exemple d’application (“je veux vendre et acheter des légumes sur internet”), +tout en s’inscrivant dans une démarche de développement Agile. + diff --git a/src/DDDDemo.sln b/src/DDDDemo.sln new file mode 100644 index 0000000..d749436 --- /dev/null +++ b/src/DDDDemo.sln @@ -0,0 +1,45 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VegetableShop.Domain", "VegetableShop.Domain\VegetableShop.Domain.csproj", "{9EE77ACA-9351-46CF-B55B-CE76EFBCDB04}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VegetableShop.API.IntegrationTests", "VegetableShop.API.IntegrationTests\VegetableShop.API.IntegrationTests.csproj", "{7D802ED7-C73C-437B-A91E-BA013939AF7C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VegetableShop.Infrastructure", "VegetableShop.Infrastructure\VegetableShop.Infrastructure.csproj", "{CCD37EDB-52CE-4C5B-B413-9A90A30E4F29}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VegetableShop.API", "VegetableShop.API\VegetableShop.API.csproj", "{AA3D52BE-056A-40CD-8F3D-82B9053E04D6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VegetableShop.Domain.AcceptanceTests", "VegetableShop.Domain.AcceptanceTests\VegetableShop.Domain.AcceptanceTests.csproj", "{BADB4EB0-3817-4114-B256-8162B76C86A3}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9EE77ACA-9351-46CF-B55B-CE76EFBCDB04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9EE77ACA-9351-46CF-B55B-CE76EFBCDB04}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9EE77ACA-9351-46CF-B55B-CE76EFBCDB04}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9EE77ACA-9351-46CF-B55B-CE76EFBCDB04}.Release|Any CPU.Build.0 = Release|Any CPU + {7D802ED7-C73C-437B-A91E-BA013939AF7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7D802ED7-C73C-437B-A91E-BA013939AF7C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D802ED7-C73C-437B-A91E-BA013939AF7C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7D802ED7-C73C-437B-A91E-BA013939AF7C}.Release|Any CPU.Build.0 = Release|Any CPU + {1838A5EB-E5F8-4CAF-BA53-EBF5ACD6954A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1838A5EB-E5F8-4CAF-BA53-EBF5ACD6954A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1838A5EB-E5F8-4CAF-BA53-EBF5ACD6954A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1838A5EB-E5F8-4CAF-BA53-EBF5ACD6954A}.Release|Any CPU.Build.0 = Release|Any CPU + {CCD37EDB-52CE-4C5B-B413-9A90A30E4F29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CCD37EDB-52CE-4C5B-B413-9A90A30E4F29}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CCD37EDB-52CE-4C5B-B413-9A90A30E4F29}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CCD37EDB-52CE-4C5B-B413-9A90A30E4F29}.Release|Any CPU.Build.0 = Release|Any CPU + {AA3D52BE-056A-40CD-8F3D-82B9053E04D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA3D52BE-056A-40CD-8F3D-82B9053E04D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA3D52BE-056A-40CD-8F3D-82B9053E04D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA3D52BE-056A-40CD-8F3D-82B9053E04D6}.Release|Any CPU.Build.0 = Release|Any CPU + {BADB4EB0-3817-4114-B256-8162B76C86A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BADB4EB0-3817-4114-B256-8162B76C86A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BADB4EB0-3817-4114-B256-8162B76C86A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BADB4EB0-3817-4114-B256-8162B76C86A3}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/src/VegetableShop.API.IntegrationTests/ConsumerServicesTests.cs b/src/VegetableShop.API.IntegrationTests/ConsumerServicesTests.cs new file mode 100644 index 0000000..426da3e --- /dev/null +++ b/src/VegetableShop.API.IntegrationTests/ConsumerServicesTests.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.Logging.Abstractions; +using VegetableShop.API.Services; +using VegetableShop.Infrastructure.Persistence; +using Xunit; + +namespace VegetableShop.API.IntegrationTests +{ + public class ConsumersServicesTests + { + private readonly ConsumerServices _consumerServices; + + public ConsumersServicesTests() + { + var loggerFactory = new NullLoggerFactory(); + var consumerRepository = new ConsumerRepositoryImpl(loggerFactory); + var farmerRepository = new FarmerRepositoryImpl(loggerFactory); + var vegetableRepositoryImpl = new VegetableRepositoryImpl(); + + _consumerServices = new ConsumerServices(loggerFactory, + consumerRepository, + farmerRepository, + vegetableRepositoryImpl); + } + + [Fact] + public void ShouldBuyVegetables() + { + _consumerServices.BuyVegetables(1L, 1L, 1L, 10); + } + } +} \ No newline at end of file diff --git a/src/VegetableShop.API.IntegrationTests/FarmerServicesTests.cs b/src/VegetableShop.API.IntegrationTests/FarmerServicesTests.cs new file mode 100644 index 0000000..4aec597 --- /dev/null +++ b/src/VegetableShop.API.IntegrationTests/FarmerServicesTests.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Logging.Abstractions; +using VegetableShop.API.Services; +using VegetableShop.Domain.Model; +using VegetableShop.Infrastructure.Persistence; +using Xunit; + +namespace VegetableShop.API.IntegrationTests +{ + public class FarmerServicesTests + { + private readonly FarmerServices _farmerServices; + private readonly FarmerRepositoryImpl _farmerRepository; + + public FarmerServicesTests() + { + var loggerFactory = new NullLoggerFactory(); + var vegetableRepository = new VegetableRepositoryImpl(); + _farmerRepository = new FarmerRepositoryImpl(loggerFactory); + _farmerServices = new FarmerServices(loggerFactory, _farmerRepository, vegetableRepository); + } + + [Fact] + public void ShouldPutVegetableOnSale() + { + var price = new Price(1, "EUR"); + _farmerServices.PutOnSale(1L, 1L, price); + + var vegetable = _farmerRepository.GetById(1L); + Assert.NotNull(vegetable); + } + } +} \ No newline at end of file diff --git a/src/VegetableShop.API.IntegrationTests/VegetableShop.API.IntegrationTests.csproj b/src/VegetableShop.API.IntegrationTests/VegetableShop.API.IntegrationTests.csproj new file mode 100644 index 0000000..e499371 --- /dev/null +++ b/src/VegetableShop.API.IntegrationTests/VegetableShop.API.IntegrationTests.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + + diff --git a/src/VegetableShop.API/Program.cs b/src/VegetableShop.API/Program.cs new file mode 100644 index 0000000..528a8d0 --- /dev/null +++ b/src/VegetableShop.API/Program.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.Logging; +using VegetableShop.API.Services; +using VegetableShop.Domain.Events; +using VegetableShop.Domain.Model; +using VegetableShop.Domain.Repositories; +using VegetableShop.Infrastructure.Persistence; + +namespace VegetableShop.API +{ + public class Program + { + private readonly ConsumerServices _consumerServices; + private readonly FarmerServices _farmerServices; + + private ILogger _logger; + private ILoggerFactory _loggerFactory; + + private ILoggerFactory SetupLogging() + { + _loggerFactory = LoggerFactory.Create(builder => + { + builder + .AddFilter("Microsoft", LogLevel.Warning) + .AddFilter("System", LogLevel.Warning) + .AddFilter("LoggingConsoleApp.Program", LogLevel.Debug) + .AddConsole(); + }); + _logger = _loggerFactory.CreateLogger(); + return _loggerFactory; + } + + private Program() + { + var loggerFactory = SetupLogging(); + + ConsumerRepository consumerRepository = new ConsumerRepositoryImpl(loggerFactory); + FarmerRepository farmerRepository = new FarmerRepositoryImpl(loggerFactory); + VegetableRepository vegetableRepository = new VegetableRepositoryImpl(); + + _consumerServices = new ConsumerServices(loggerFactory, + consumerRepository, + farmerRepository, + vegetableRepository); + + _farmerServices = new FarmerServices(loggerFactory, + farmerRepository, + vegetableRepository); + + DomainEvents.AddObserver(new LogItemOnFarmerPutOnSaleEvent(_loggerFactory)); + } + + private void Run() + { + _logger.LogInformation("Calling _consumerServices.BuyVegetables"); + _farmerServices.PutOnSale(1, 1, new Price(1, "EUR")); + _consumerServices.BuyVegetables(1, 1, 1, 10); + } + + public static void Main(string[] args) + { + new Program().Run(); + } + } +} \ No newline at end of file diff --git a/src/VegetableShop.API/Services/ConsumerServices.cs b/src/VegetableShop.API/Services/ConsumerServices.cs new file mode 100644 index 0000000..cfe58b6 --- /dev/null +++ b/src/VegetableShop.API/Services/ConsumerServices.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Logging; +using VegetableShop.Domain.Repositories; + +namespace VegetableShop.API.Services +{ + public class ConsumerServices + { + private readonly ILogger _logger; + private readonly ConsumerRepository _consumerRepository; + private readonly FarmerRepository _farmerRepository; + private readonly VegetableRepository _vegetableRepository; + + public ConsumerServices(ILoggerFactory loggerFactory, + ConsumerRepository consumerRepository, + FarmerRepository farmerRepository, + VegetableRepository vegetableRepository) + { + _logger = loggerFactory.CreateLogger(); + _consumerRepository = consumerRepository; + _farmerRepository = farmerRepository; + _vegetableRepository = vegetableRepository; + } + + public void BuyVegetables(long consumerId, long farmerId, long vegetableId, uint quantity) + { + var consumer = _consumerRepository.GetById(consumerId); + var farmer = _farmerRepository.GetById(farmerId); + var vegetable = _vegetableRepository.GetById(vegetableId); + _logger.LogDebug($"{consumer}, {farmer}, {vegetable}"); + consumer.Buy(quantity, vegetable, farmer); + } + } +} \ No newline at end of file diff --git a/src/VegetableShop.API/Services/FarmerServices.cs b/src/VegetableShop.API/Services/FarmerServices.cs new file mode 100644 index 0000000..008c485 --- /dev/null +++ b/src/VegetableShop.API/Services/FarmerServices.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Logging; +using VegetableShop.Domain.Model; +using VegetableShop.Domain.Repositories; + +namespace VegetableShop.API.Services +{ + public class FarmerServices + { + private readonly ILogger _logger; + private readonly FarmerRepository _farmerRepository; + private readonly VegetableRepository _vegetableRepository; + + public FarmerServices(ILoggerFactory loggerFactory, + FarmerRepository farmerRepository, + VegetableRepository vegetableRepository) + { + _logger = loggerFactory.CreateLogger(); + _farmerRepository = farmerRepository; + _vegetableRepository = vegetableRepository; + } + + public void PutOnSale(long farmerId, long vegetableId, Price price) + { + var farmer = _farmerRepository.GetById(farmerId); + var vegetable = _vegetableRepository.GetById(vegetableId); + _logger.LogDebug($"{farmer}, {vegetable}"); + farmer.PutOnSale(vegetable, price); + } + } +} \ No newline at end of file diff --git a/src/VegetableShop.API/VegetableShop.API.csproj b/src/VegetableShop.API/VegetableShop.API.csproj new file mode 100644 index 0000000..8469074 --- /dev/null +++ b/src/VegetableShop.API/VegetableShop.API.csproj @@ -0,0 +1,16 @@ + + + + Exe + netcoreapp3.1 + VegetableShop.API + + + + + + + + + + diff --git a/src/VegetableShop.Domain.AcceptanceTests/AddTwoNumbers.cs b/src/VegetableShop.Domain.AcceptanceTests/AddTwoNumbers.cs new file mode 100644 index 0000000..78d84b5 --- /dev/null +++ b/src/VegetableShop.Domain.AcceptanceTests/AddTwoNumbers.cs @@ -0,0 +1,37 @@ +using Xunit; +using Xunit.Gherkin.Quick; + +namespace VegetableShop.Domain.AcceptanceTests +{ + [FeatureFile("./Features/AddTwoNumbers.feature")] + public sealed class AddTwoNumbers : Feature + { + private readonly Calculator _calculator = new Calculator(); + + [Given(@"I chose (\d+) as first number")] + public void I_chose_first_number(int firstNumber) + { + _calculator.SetFirstNumber(firstNumber); + } + + [And(@"I chose (\d+) as second number")] + public void I_chose_second_number(int secondNumber) + { + _calculator.SetSecondNumber(secondNumber); + } + + [When(@"I press add")] + public void I_press_add() + { + _calculator.AddNumbers(); + } + + [Then(@"the result should be (\d+) on the screen")] + public void The_result_should_be_z_on_the_screen(int expectedResult) + { + var actualResult = _calculator.Result; + + Assert.Equal(expectedResult, actualResult); + } + } +} \ No newline at end of file diff --git a/src/VegetableShop.Domain.AcceptanceTests/Calculator.cs b/src/VegetableShop.Domain.AcceptanceTests/Calculator.cs new file mode 100644 index 0000000..eac6ced --- /dev/null +++ b/src/VegetableShop.Domain.AcceptanceTests/Calculator.cs @@ -0,0 +1,15 @@ +namespace VegetableShop.Domain.AcceptanceTests +{ + public class Calculator + { + private int _firstNumber; + private int _secondNumber; + + public void SetFirstNumber(in int firstNumber) => _firstNumber = firstNumber; + + public void SetSecondNumber(in int secondNumber) => _secondNumber = secondNumber; + + public void AddNumbers() => Result = _firstNumber + _secondNumber; + public int Result { get; private set; } + } +} \ No newline at end of file diff --git a/src/VegetableShop.Domain.AcceptanceTests/Features/AddTwoNumbers.feature b/src/VegetableShop.Domain.AcceptanceTests/Features/AddTwoNumbers.feature new file mode 100644 index 0000000..6aefd6e --- /dev/null +++ b/src/VegetableShop.Domain.AcceptanceTests/Features/AddTwoNumbers.feature @@ -0,0 +1,10 @@ +Feature: AddTwoNumbers + In order to learn Math + As a regular human + I want to add two numbers using Calculator + + Scenario: Add two numbers + Given I chose 12 as first number + And I chose 15 as second number + When I press add + Then the result should be 27 on the screen \ No newline at end of file diff --git a/src/VegetableShop.Domain.AcceptanceTests/Features/PutOnSale.feature b/src/VegetableShop.Domain.AcceptanceTests/Features/PutOnSale.feature new file mode 100644 index 0000000..fd18e19 --- /dev/null +++ b/src/VegetableShop.Domain.AcceptanceTests/Features/PutOnSale.feature @@ -0,0 +1,7 @@ +Feature: PutOnSale + + Scenario: PutOnSale + Given A farmer + And A vegetable of name carrot + When I put the vegetable on sale for 42 EUR + Then The vegetable carrot should be on sale at price 42 EUR \ No newline at end of file diff --git a/src/VegetableShop.Domain.AcceptanceTests/PutOnSale.cs b/src/VegetableShop.Domain.AcceptanceTests/PutOnSale.cs new file mode 100644 index 0000000..ae1af23 --- /dev/null +++ b/src/VegetableShop.Domain.AcceptanceTests/PutOnSale.cs @@ -0,0 +1,48 @@ +using Microsoft.Extensions.Logging.Abstractions; +using VegetableShop.Domain.Model; +using Xunit; +using Xunit.Gherkin.Quick; + +namespace VegetableShop.Domain.AcceptanceTests +{ + [FeatureFile("./Features/PutOnSale.feature")] + public sealed class PutOnSale : Feature + { + private static readonly NullLoggerFactory LoggerFactory = new NullLoggerFactory(); + + private Farmer _farmer; + private Vegetable _vegetable; + + [Given(@"A farmer")] + public void A_Farmer() + { + _farmer = new Farmer(LoggerFactory); + Assert.NotNull(_farmer); + } + + [And(@"A vegetable of name (\w+)")] + public void A_Vegetable(string name) + { + _vegetable = new Vegetable(name); + } + + [When(@"I put the vegetable on sale for (\d+) (\w+)")] + public void I_Put_On_Sale(decimal value, string currency) + { + var price = new Price(value, currency); + _farmer.PutOnSale(_vegetable, price); + Assert.Contains(_farmer.VegetablesForSale, pair => pair.Key == _vegetable); + } + + [Then(@"The vegetable (\w+) should be on sale at price (\d+) (\w+)")] + public void The_vegetable_should_be_on_sale_at_price_x(string name, + decimal value, + string currency) + { + Assert.Equal(_vegetable.Name, name); + var price = _farmer.VegetablesForSale[_vegetable]; + Assert.Equal(price.Value, value); + Assert.Equal(price.Currency, currency); + } + } +} \ No newline at end of file diff --git a/src/VegetableShop.Domain.AcceptanceTests/VegetableShop.Domain.AcceptanceTests.csproj b/src/VegetableShop.Domain.AcceptanceTests/VegetableShop.Domain.AcceptanceTests.csproj new file mode 100644 index 0000000..9028a19 --- /dev/null +++ b/src/VegetableShop.Domain.AcceptanceTests/VegetableShop.Domain.AcceptanceTests.csproj @@ -0,0 +1,39 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/src/VegetableShop.Domain/Errors/UnauthorizedSaleException.cs b/src/VegetableShop.Domain/Errors/UnauthorizedSaleException.cs new file mode 100644 index 0000000..d989a04 --- /dev/null +++ b/src/VegetableShop.Domain/Errors/UnauthorizedSaleException.cs @@ -0,0 +1,13 @@ +using System; +using VegetableShop.Domain.Model; + +namespace VegetableShop.Domain.Errors +{ + public class UnauthorizedSaleException : Exception + { + public UnauthorizedSaleException(Farmer farmer, Vegetable vegetable) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/VegetableShop.Domain/Events/DomainEvents.cs b/src/VegetableShop.Domain/Events/DomainEvents.cs new file mode 100644 index 0000000..55a10a3 --- /dev/null +++ b/src/VegetableShop.Domain/Events/DomainEvents.cs @@ -0,0 +1,21 @@ +using System.Collections; +using System.Collections.Generic; + +namespace VegetableShop.Domain.Events +{ + public static class DomainEvents + { + private static readonly ICollection Observers = new List(); + public static void Notify(Event @event) + { + foreach (var obs in Observers) + { + obs.Handle(@event); + } + } + + public static void AddObserver(EventHandler obs) => Observers.Add(obs); + + public static bool RemoveObserver(EventHandler obs) => Observers.Remove(obs); + } +} \ No newline at end of file diff --git a/src/VegetableShop.Domain/Events/Event.cs b/src/VegetableShop.Domain/Events/Event.cs new file mode 100644 index 0000000..14bc966 --- /dev/null +++ b/src/VegetableShop.Domain/Events/Event.cs @@ -0,0 +1,6 @@ +namespace VegetableShop.Domain.Events +{ + public interface Event + { + } +} \ No newline at end of file diff --git a/src/VegetableShop.Domain/Events/EventHandler.cs b/src/VegetableShop.Domain/Events/EventHandler.cs new file mode 100644 index 0000000..6f3c217 --- /dev/null +++ b/src/VegetableShop.Domain/Events/EventHandler.cs @@ -0,0 +1,7 @@ +namespace VegetableShop.Domain.Events +{ + public interface EventHandler + { + void Handle(Event @event); + } +} \ No newline at end of file diff --git a/src/VegetableShop.Domain/Events/FarmerPutOnSaleEvent.cs b/src/VegetableShop.Domain/Events/FarmerPutOnSaleEvent.cs new file mode 100644 index 0000000..79c5f11 --- /dev/null +++ b/src/VegetableShop.Domain/Events/FarmerPutOnSaleEvent.cs @@ -0,0 +1,23 @@ +using VegetableShop.Domain.Model; + +namespace VegetableShop.Domain.Events +{ + public class FarmerPutOnSaleEvent : Event + { + public long Id { get; } + public Vegetable Vegetable { get; } + public Price Price { get; } + + public FarmerPutOnSaleEvent(in long id, Vegetable vegetable, Price price) + { + Id = id; + Vegetable = vegetable; + Price = price; + } + + public override string ToString() + { + return $"{nameof(Id)}: {Id}, {nameof(Vegetable)}: {Vegetable}, {nameof(Price)}: {Price}"; + } + } +} \ No newline at end of file diff --git a/src/VegetableShop.Domain/Events/LogItemOnFarmerPutOnSaleEvent.cs b/src/VegetableShop.Domain/Events/LogItemOnFarmerPutOnSaleEvent.cs new file mode 100644 index 0000000..34add29 --- /dev/null +++ b/src/VegetableShop.Domain/Events/LogItemOnFarmerPutOnSaleEvent.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Logging; + +namespace VegetableShop.Domain.Events +{ + public class LogItemOnFarmerPutOnSaleEvent: EventHandler + { + private readonly ILogger _logger; + + public LogItemOnFarmerPutOnSaleEvent(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + private void Handle(FarmerPutOnSaleEvent @event) + => _logger.LogInformation($"Received {nameof(@event)}, {@event}"); + + public void Handle(Event @event) => Handle(@event as FarmerPutOnSaleEvent); + } +} \ No newline at end of file diff --git a/src/VegetableShop.Domain/Model/Consumer.cs b/src/VegetableShop.Domain/Model/Consumer.cs new file mode 100644 index 0000000..80d34a0 --- /dev/null +++ b/src/VegetableShop.Domain/Model/Consumer.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Logging; + +namespace VegetableShop.Domain.Model +{ + public class Consumer + { + private readonly ILogger _logger; + + public Consumer(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + public void Buy(uint quantity, Vegetable vegetable, Farmer farmer) + { + _logger.LogInformation($"Buying {quantity} {vegetable} from {farmer}"); + } + + public void SubscribeToPriceDrop(Farmer farmer) + { + _logger.LogInformation($"Subscribing to price drop from {farmer}"); + } + } +} \ No newline at end of file diff --git a/src/VegetableShop.Domain/Model/Farmer.cs b/src/VegetableShop.Domain/Model/Farmer.cs new file mode 100644 index 0000000..e75c0ae --- /dev/null +++ b/src/VegetableShop.Domain/Model/Farmer.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using VegetableShop.Domain.Errors; +using VegetableShop.Domain.Events; +using VegetableShop.Domain.Specifications; + +namespace VegetableShop.Domain.Model +{ + public class Farmer + { + private readonly ILogger _logger; + public Dictionary VegetablesForSale { get; } = new Dictionary(); + private static readonly long Id = new Random().Next(int.MaxValue); + + public Farmer(ILoggerFactory loggerFactory) => _logger = loggerFactory.CreateLogger(); + public void PutOnSale(Vegetable vegetable, Price price) + { + if (AuthorizedSaleSpecification.IsSatisfiedBy(this, vegetable, price)) + { + VegetablesForSale.Add(vegetable, price); + _logger.LogInformation($"Add {vegetable} for price {price}"); + DomainEvents.Notify(new FarmerPutOnSaleEvent(Id, vegetable, price)); + } + else + { + throw new UnauthorizedSaleException(this, vegetable); + } + } + + public void WithdrawFromSale() => throw new NotImplementedException(); + + public void ChangePrice() => throw new NotImplementedException(); + + public override string ToString() => $"Farmer[id={Id}]"; + } +} \ No newline at end of file diff --git a/src/VegetableShop.Domain/Model/Price.cs b/src/VegetableShop.Domain/Model/Price.cs new file mode 100644 index 0000000..bd79a71 --- /dev/null +++ b/src/VegetableShop.Domain/Model/Price.cs @@ -0,0 +1,17 @@ +namespace VegetableShop.Domain.Model +{ + public readonly struct Price + { + public decimal Value { get; } + + public string Currency { get; } + + public Price(decimal value, string currency = "EUR") + { + Value = value; + Currency = currency; + } + + public override string ToString() => $"{Value} {Currency}"; + } +} \ No newline at end of file diff --git a/src/VegetableShop.Domain/Model/Vegetable.cs b/src/VegetableShop.Domain/Model/Vegetable.cs new file mode 100644 index 0000000..99c460a --- /dev/null +++ b/src/VegetableShop.Domain/Model/Vegetable.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +namespace VegetableShop.Domain.Model +{ + public class Vegetable + { + private readonly long _id; + public string Name { get; } + + public Vegetable(string name) + { + _id = new Random().Next(int.MaxValue); + Name = name; + } + + public override string ToString() => $"Vegetable[id={_id}, name={Name}"; + } +} \ No newline at end of file diff --git a/src/VegetableShop.Domain/Repositories/ConsumerRepository.cs b/src/VegetableShop.Domain/Repositories/ConsumerRepository.cs new file mode 100644 index 0000000..4dcedf0 --- /dev/null +++ b/src/VegetableShop.Domain/Repositories/ConsumerRepository.cs @@ -0,0 +1,9 @@ +using VegetableShop.Domain.Model; + +namespace VegetableShop.Domain.Repositories +{ + public interface ConsumerRepository + { + Consumer GetById(in long consumerId); + } +} \ No newline at end of file diff --git a/src/VegetableShop.Domain/Repositories/FarmerRepository.cs b/src/VegetableShop.Domain/Repositories/FarmerRepository.cs new file mode 100644 index 0000000..2c94603 --- /dev/null +++ b/src/VegetableShop.Domain/Repositories/FarmerRepository.cs @@ -0,0 +1,9 @@ +using VegetableShop.Domain.Model; + +namespace VegetableShop.Domain.Repositories +{ + public interface FarmerRepository + { + Farmer GetById(in long farmerId); + } +} \ No newline at end of file diff --git a/src/VegetableShop.Domain/Repositories/VegetableRepository.cs b/src/VegetableShop.Domain/Repositories/VegetableRepository.cs new file mode 100644 index 0000000..efbd0ad --- /dev/null +++ b/src/VegetableShop.Domain/Repositories/VegetableRepository.cs @@ -0,0 +1,9 @@ +using VegetableShop.Domain.Model; + +namespace VegetableShop.Domain.Repositories +{ + public interface VegetableRepository + { + Vegetable GetById(in long vegetableId); + } +} \ No newline at end of file diff --git a/src/VegetableShop.Domain/Specifications/AuthorizedSaleSpecification.cs b/src/VegetableShop.Domain/Specifications/AuthorizedSaleSpecification.cs new file mode 100644 index 0000000..30d12ad --- /dev/null +++ b/src/VegetableShop.Domain/Specifications/AuthorizedSaleSpecification.cs @@ -0,0 +1,9 @@ +using VegetableShop.Domain.Model; + +namespace VegetableShop.Domain.Specifications +{ + public static class AuthorizedSaleSpecification + { + public static bool IsSatisfiedBy(Farmer farmer, Vegetable vegetable, Price price) => true; + } +} \ No newline at end of file diff --git a/src/VegetableShop.Domain/VegetableShop.Domain.csproj b/src/VegetableShop.Domain/VegetableShop.Domain.csproj new file mode 100644 index 0000000..165d533 --- /dev/null +++ b/src/VegetableShop.Domain/VegetableShop.Domain.csproj @@ -0,0 +1,10 @@ + + + + netcoreapp3.1 + + + + + + diff --git a/src/VegetableShop.Infrastructure/Persistence/ConsumerRepositoryImpl.cs b/src/VegetableShop.Infrastructure/Persistence/ConsumerRepositoryImpl.cs new file mode 100644 index 0000000..a352487 --- /dev/null +++ b/src/VegetableShop.Infrastructure/Persistence/ConsumerRepositoryImpl.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using VegetableShop.Domain.Model; +using VegetableShop.Domain.Repositories; + +namespace VegetableShop.Infrastructure.Persistence +{ + public class ConsumerRepositoryImpl: ConsumerRepository + { + private readonly Dictionary _consumers; + + public ConsumerRepositoryImpl(ILoggerFactory loggerFactory) + { + _consumers = new Dictionary + { + { 1L, new Consumer(loggerFactory) } + }; + } + + public Consumer GetById(in long consumerId) => _consumers[consumerId]; + } +} \ No newline at end of file diff --git a/src/VegetableShop.Infrastructure/Persistence/FarmerRepositoryImpl.cs b/src/VegetableShop.Infrastructure/Persistence/FarmerRepositoryImpl.cs new file mode 100644 index 0000000..11ba44c --- /dev/null +++ b/src/VegetableShop.Infrastructure/Persistence/FarmerRepositoryImpl.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using VegetableShop.Domain.Model; +using VegetableShop.Domain.Repositories; + +namespace VegetableShop.Infrastructure.Persistence +{ + public class FarmerRepositoryImpl: FarmerRepository + { + private readonly ILoggerFactory _loggerFactory; + + public FarmerRepositoryImpl(ILoggerFactory loggerFactory) + { + _loggerFactory = loggerFactory; + _farmers = new Dictionary + { + { 1L, new Farmer(_loggerFactory) } + }; + } + + private readonly Dictionary _farmers; + + public Farmer GetById(in long farmerId) => _farmers[farmerId]; + } +} \ No newline at end of file diff --git a/src/VegetableShop.Infrastructure/Persistence/VegetableRepositoryImpl.cs b/src/VegetableShop.Infrastructure/Persistence/VegetableRepositoryImpl.cs new file mode 100644 index 0000000..ef1db8d --- /dev/null +++ b/src/VegetableShop.Infrastructure/Persistence/VegetableRepositoryImpl.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using VegetableShop.Domain.Model; +using VegetableShop.Domain.Repositories; + +namespace VegetableShop.Infrastructure.Persistence +{ + public class VegetableRepositoryImpl: VegetableRepository + { + private readonly Dictionary _vegetables = new Dictionary + { + { 1L, new Vegetable("carrot") } + }; + public Vegetable GetById(in long vegetableId) => _vegetables[vegetableId]; + } +} \ No newline at end of file diff --git a/src/VegetableShop.Infrastructure/VegetableShop.Infrastructure.csproj b/src/VegetableShop.Infrastructure/VegetableShop.Infrastructure.csproj new file mode 100644 index 0000000..1df5869 --- /dev/null +++ b/src/VegetableShop.Infrastructure/VegetableShop.Infrastructure.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.1 + + + + + + +