From 7d6f6793f4d7e24397475513812535e70355b94a Mon Sep 17 00:00:00 2001 From: viveksacademia4git <45398326+viveksacademia4git@users.noreply.github.com> Date: Sun, 21 Jan 2024 15:02:56 +0100 Subject: [PATCH 1/2] Added Observer pattern #54 --- Core/Extensions/ConsoleExtensions.cs | 19 +-- .../ObserverPattern/Components/EnumTopic.cs | 11 ++ .../Components/PersonAndrewSmith.cs | 19 +++ .../Components/PersonJohnDoe.cs | 19 +++ .../ObserverPattern/ObserverPatternExample.cs | 34 ++++++ .../ObserverPatternExampleWithCategory.cs | 38 ++++++ .../StatePattern/StatePatternBulbExample.cs | 8 +- .../ExecutionHelpers/PatternOptions.cs | 13 ++ .../NoSubscriptionFoundException.cs | 6 + .../ObserverPattern/Interfaces/IPublisher.cs | 31 +++++ .../ObserverPattern/Interfaces/ISubscriber.cs | 11 ++ .../Behavioral/ObserverPattern/Publisher.cs | 68 +++++++++++ GofPatterns/GofPatterns.csproj | 15 ++- .../ObserverPattern/PublisherTests.cs | 68 +++++++++++ .../PublisherTestsWithCategory.cs | 80 +++++++++++++ .../ObserverPatternExampleTests.cs | 39 ++++++ .../ObserverPatternExampleWithCategoryTest.cs | 40 +++++++ README.md | 4 +- README/Behavioral/Observer.md | 112 ++++++++++++++++++ README/Creational/AbstractFactory.md | 10 +- README/Creational/Builder.md | 2 +- README/Creational/Factory.md | 7 +- 22 files changed, 619 insertions(+), 35 deletions(-) create mode 100644 GofConsoleApp/Examples/Behavioral/ObserverPattern/Components/EnumTopic.cs create mode 100644 GofConsoleApp/Examples/Behavioral/ObserverPattern/Components/PersonAndrewSmith.cs create mode 100644 GofConsoleApp/Examples/Behavioral/ObserverPattern/Components/PersonJohnDoe.cs create mode 100644 GofConsoleApp/Examples/Behavioral/ObserverPattern/ObserverPatternExample.cs create mode 100644 GofConsoleApp/Examples/Behavioral/ObserverPattern/ObserverPatternExampleWithCategory.cs create mode 100644 GofPatterns/Behavioral/ObserverPattern/Exceptions/NoSubscriptionFoundException.cs create mode 100644 GofPatterns/Behavioral/ObserverPattern/Interfaces/IPublisher.cs create mode 100644 GofPatterns/Behavioral/ObserverPattern/Interfaces/ISubscriber.cs create mode 100644 GofPatterns/Behavioral/ObserverPattern/Publisher.cs create mode 100644 GofPatternsTests/Behavioral/ObserverPattern/PublisherTests.cs create mode 100644 GofPatternsTests/Behavioral/ObserverPattern/PublisherTestsWithCategory.cs create mode 100644 GofPatternsTests/Examples/Behavioral/ObserverPattern/ObserverPatternExampleTests.cs create mode 100644 GofPatternsTests/Examples/Behavioral/ObserverPattern/ObserverPatternExampleWithCategoryTest.cs create mode 100644 README/Behavioral/Observer.md diff --git a/Core/Extensions/ConsoleExtensions.cs b/Core/Extensions/ConsoleExtensions.cs index 77ac66f..e660349 100644 --- a/Core/Extensions/ConsoleExtensions.cs +++ b/Core/Extensions/ConsoleExtensions.cs @@ -1,5 +1,4 @@ -using System.Text; -using Core.Console.Interfaces; +using Core.Console.Interfaces; using Microsoft.Extensions.Logging; namespace Core.Extensions; @@ -8,17 +7,6 @@ internal static class ConsoleExtensions { public static ILoggerFactory GetLoggerFactory() => LoggerFactory.Create(builder => { builder.AddConsole(); }); - public static void Log(this IConsoleLogger logger, string identifier, IEnumerable options) - { - var message = new StringBuilder($"Please enter from following {identifier}: "); - - var i = 0; - foreach (var option in options) - message.Append($"\n {++i}. {option} "); - - logger.Log(message.ToString()); - } - public static TEnum AcceptInputEnum(this IInputReader inputReader, TEnum defaultValue) { var input = inputReader.AcceptInput(); @@ -31,4 +19,9 @@ public static decimal AcceptInputDecimal(this IInputReader inputReader) var input = inputReader.AcceptInput(); return decimal.Parse(input); } + + public static void LogInfoQuit(this IConsoleLogger logger) + { + logger.Log("Please provide 'quit' input in order to quit from the example."); + } } \ No newline at end of file diff --git a/GofConsoleApp/Examples/Behavioral/ObserverPattern/Components/EnumTopic.cs b/GofConsoleApp/Examples/Behavioral/ObserverPattern/Components/EnumTopic.cs new file mode 100644 index 0000000..ba273d7 --- /dev/null +++ b/GofConsoleApp/Examples/Behavioral/ObserverPattern/Components/EnumTopic.cs @@ -0,0 +1,11 @@ +namespace GofConsoleApp.Examples.Behavioral.ObserverPattern.Components; + +internal enum EnumTopic +{ + Sports, + Politics, + Weather, + Holidays, + Invalid, + Quit +} \ No newline at end of file diff --git a/GofConsoleApp/Examples/Behavioral/ObserverPattern/Components/PersonAndrewSmith.cs b/GofConsoleApp/Examples/Behavioral/ObserverPattern/Components/PersonAndrewSmith.cs new file mode 100644 index 0000000..bdaafb9 --- /dev/null +++ b/GofConsoleApp/Examples/Behavioral/ObserverPattern/Components/PersonAndrewSmith.cs @@ -0,0 +1,19 @@ +using Core.Console.Interfaces; +using GofPatterns.Behavioral.ObserverPattern.Interfaces; + +namespace GofConsoleApp.Examples.Behavioral.ObserverPattern.Components; + +internal class PersonAndrewSmith : ISubscriber +{ + private readonly IConsoleLogger logger; + + public PersonAndrewSmith(IConsoleLogger logger) + { + this.logger = logger; + } + + public void Update(string input) + { + logger.Log($"Andrew Smith received: {input}"); + } +} \ No newline at end of file diff --git a/GofConsoleApp/Examples/Behavioral/ObserverPattern/Components/PersonJohnDoe.cs b/GofConsoleApp/Examples/Behavioral/ObserverPattern/Components/PersonJohnDoe.cs new file mode 100644 index 0000000..5accba3 --- /dev/null +++ b/GofConsoleApp/Examples/Behavioral/ObserverPattern/Components/PersonJohnDoe.cs @@ -0,0 +1,19 @@ +using Core.Console.Interfaces; +using GofPatterns.Behavioral.ObserverPattern.Interfaces; + +namespace GofConsoleApp.Examples.Behavioral.ObserverPattern.Components; + +internal class PersonJohnDoe : ISubscriber +{ + private readonly IConsoleLogger logger; + + public PersonJohnDoe(IConsoleLogger logger) + { + this.logger = logger; + } + + public void Update(string input) + { + logger.Log($"John Doe received: {input}"); + } +} \ No newline at end of file diff --git a/GofConsoleApp/Examples/Behavioral/ObserverPattern/ObserverPatternExample.cs b/GofConsoleApp/Examples/Behavioral/ObserverPattern/ObserverPatternExample.cs new file mode 100644 index 0000000..a6839c2 --- /dev/null +++ b/GofConsoleApp/Examples/Behavioral/ObserverPattern/ObserverPatternExample.cs @@ -0,0 +1,34 @@ +using Core.Extensions; +using GofConsoleApp.Examples.Behavioral.ObserverPattern.Components; +using GofPatterns.Behavioral.ObserverPattern; +using GofPatterns.Behavioral.ObserverPattern.Interfaces; + +namespace GofConsoleApp.Examples.Behavioral.ObserverPattern; + +internal class ObserverPatternExample : BaseExample +{ + private readonly IPublisher newsPublisher = new Publisher(); + + protected override bool Execute() + { + var johnDoe = new PersonJohnDoe(Logger); + var andrewSmith = new PersonAndrewSmith(Logger); + + newsPublisher.AddSubscriber(johnDoe); + newsPublisher.AddSubscriber(andrewSmith); + + while (true) + { + Logger.LogInfoQuit(); + + var newsUpdate = AcceptInputString("news update"); + + if (newsUpdate.Trim().ToLower().Equals("quit")) + break; + + newsPublisher.NotifySubscribers(newsUpdate); + } + + return true; + } +} \ No newline at end of file diff --git a/GofConsoleApp/Examples/Behavioral/ObserverPattern/ObserverPatternExampleWithCategory.cs b/GofConsoleApp/Examples/Behavioral/ObserverPattern/ObserverPatternExampleWithCategory.cs new file mode 100644 index 0000000..117618a --- /dev/null +++ b/GofConsoleApp/Examples/Behavioral/ObserverPattern/ObserverPatternExampleWithCategory.cs @@ -0,0 +1,38 @@ +using Core.Extensions; +using GofConsoleApp.Examples.Behavioral.ObserverPattern.Components; +using GofPatterns.Behavioral.ObserverPattern; +using GofPatterns.Behavioral.ObserverPattern.Interfaces; + +namespace GofConsoleApp.Examples.Behavioral.ObserverPattern; + +internal class ObserverPatternExampleWithCategory : BaseExample +{ + private readonly IPublisher newsPublisher = new Publisher(); + + protected override bool Execute() + { + var johnDoe = new PersonJohnDoe(Logger); + var andrewSmith = new PersonAndrewSmith(Logger); + + newsPublisher.AddSubscriber(johnDoe, EnumTopic.Sports); + newsPublisher.AddSubscriber(andrewSmith, EnumTopic.Sports); + newsPublisher.AddSubscriber(johnDoe, EnumTopic.Politics); + newsPublisher.AddSubscriber(andrewSmith, EnumTopic.Weather); + newsPublisher.AddSubscriber(johnDoe, EnumTopic.Holidays); + + do + { + Logger.LogInfoQuit(); + + var topic = AcceptInputEnum(EnumTopic.Invalid, nameof(EnumTopic), EnumTopic.Invalid); + + if (IsInvalidOrQuit(topic, EnumTopic.Invalid, EnumTopic.Quit, out _)) + return true; + + var newsUpdate = AcceptInputString($"{topic} news update"); + + newsPublisher.NotifySubscribers(newsUpdate, topic); + + } while (true); + } +} diff --git a/GofConsoleApp/Examples/Behavioral/StatePattern/StatePatternBulbExample.cs b/GofConsoleApp/Examples/Behavioral/StatePattern/StatePatternBulbExample.cs index f375176..0fb3587 100644 --- a/GofConsoleApp/Examples/Behavioral/StatePattern/StatePatternBulbExample.cs +++ b/GofConsoleApp/Examples/Behavioral/StatePattern/StatePatternBulbExample.cs @@ -18,18 +18,18 @@ protected override bool Execute() do { - var inputOption = AcceptInputEnum(Invalid, "state", Invalid); + var state = AcceptInputEnum(Invalid, "state", Invalid); - if (IsInvalidOrQuit(inputOption, Invalid, Quit, out var output)) + if (IsInvalidOrQuit(state, Invalid, Quit, out var output)) return output; - if (bulb.State.Name.Equals(inputOption.ToString())) + if (bulb.State.Name.Equals(state.ToString())) { Logger.Log($"Bulb already in {bulb.State.Name} state."); continue; } - switch (inputOption) + switch (state) { case On: bulb.SetState(on); diff --git a/GofConsoleApp/Examples/ExecutionHelpers/PatternOptions.cs b/GofConsoleApp/Examples/ExecutionHelpers/PatternOptions.cs index e053aa8..b12da7b 100644 --- a/GofConsoleApp/Examples/ExecutionHelpers/PatternOptions.cs +++ b/GofConsoleApp/Examples/ExecutionHelpers/PatternOptions.cs @@ -1,6 +1,7 @@ using GofConsoleApp.Examples.Behavioral.CommandPattern; using GofConsoleApp.Examples.Behavioral.CorPattern; using GofConsoleApp.Examples.Behavioral.MediatorPattern; +using GofConsoleApp.Examples.Behavioral.ObserverPattern; using GofConsoleApp.Examples.Behavioral.StatePattern; using GofConsoleApp.Examples.Behavioral.StrategyPattern; using GofConsoleApp.Examples.Creational.AbstractFactoryPattern; @@ -24,6 +25,9 @@ internal static class PatternOptions internal const string AdapterPatternOption = "13"; internal const string FlyweightPatternOption = "14"; internal const string MediatorPatternOption = "15"; + internal const string ObserverPatternOption = "16.1"; + internal const string ObserverPatternOptionWithType = "16.2"; + internal const string ChainOfResponsibilityPatternOption = "21"; internal const string ChainOfResponsibilityPatternOption2 = "21.2"; internal const string ChainOfResponsibilityPatternOption3 = "21.3"; @@ -33,6 +37,7 @@ internal static class PatternOptions internal const string StatePatternOptionDriveExample = "23.2"; internal const string StrategyPatternOptionSender = "24"; internal const string StrategyPatternOptionPayment = "24.2"; + internal const string FactoryOption = "31"; internal const string AbstractFactoryOption = "32"; internal const string BuilderPatternOption = "33"; @@ -73,6 +78,14 @@ internal static class PatternOptions MediatorPatternOption, new PatternExampleMap("Flyweight Pattern >> Drawing shapes", new MediatorPatternExample()) }, + { + ObserverPatternOption, + new PatternExampleMap("Observer Pattern >> News Publisher", new ObserverPatternExample()) + }, + { + ObserverPatternOptionWithType, + new PatternExampleMap("Observer Pattern >> News Publisher with type", new ObserverPatternExampleWithCategory()) + }, // Behavioral Patterns { diff --git a/GofPatterns/Behavioral/ObserverPattern/Exceptions/NoSubscriptionFoundException.cs b/GofPatterns/Behavioral/ObserverPattern/Exceptions/NoSubscriptionFoundException.cs new file mode 100644 index 0000000..c66882a --- /dev/null +++ b/GofPatterns/Behavioral/ObserverPattern/Exceptions/NoSubscriptionFoundException.cs @@ -0,0 +1,6 @@ +namespace GofPatterns.Behavioral.ObserverPattern.Exceptions; + +public class NoSubscriptionFoundException : Exception +{ + public NoSubscriptionFoundException(string message) : base(message) { } +} \ No newline at end of file diff --git a/GofPatterns/Behavioral/ObserverPattern/Interfaces/IPublisher.cs b/GofPatterns/Behavioral/ObserverPattern/Interfaces/IPublisher.cs new file mode 100644 index 0000000..2985b04 --- /dev/null +++ b/GofPatterns/Behavioral/ObserverPattern/Interfaces/IPublisher.cs @@ -0,0 +1,31 @@ +namespace GofPatterns.Behavioral.ObserverPattern.Interfaces; + +/// +/// Publisher (or Broadcaster) interface. +/// Publishers are responsible for managing subscribers and notifying them of events. +/// +/// +public interface IPublisher +{ + public void AddSubscriber(ISubscriber subscriber); + + public void RemoveSubscribers(); + + public void NotifySubscribers(TInput input); +} + +/// +/// Publisher (or Broadcaster) interface. +/// Publishers are responsible for managing subscribers and notifying them of events. +/// Multiple subscribers can be subscribed to a category, and there can be multiple categories. +/// +/// +/// +public interface IPublisher where TCategory : notnull +{ + public void AddSubscriber(ISubscriber subscriber, TCategory category); + + public void RemoveSubscribers(TCategory category); + + public void NotifySubscribers(TInput input, TCategory category); +} \ No newline at end of file diff --git a/GofPatterns/Behavioral/ObserverPattern/Interfaces/ISubscriber.cs b/GofPatterns/Behavioral/ObserverPattern/Interfaces/ISubscriber.cs new file mode 100644 index 0000000..d581371 --- /dev/null +++ b/GofPatterns/Behavioral/ObserverPattern/Interfaces/ISubscriber.cs @@ -0,0 +1,11 @@ +namespace GofPatterns.Behavioral.ObserverPattern.Interfaces; + +/// +/// Subscriber (or Listener) interface. +/// Subscribers are interested in certain events and implement the Update method to handle them. +/// +/// +public interface ISubscriber +{ + void Update(TInput input); +} \ No newline at end of file diff --git a/GofPatterns/Behavioral/ObserverPattern/Publisher.cs b/GofPatterns/Behavioral/ObserverPattern/Publisher.cs new file mode 100644 index 0000000..8b66dac --- /dev/null +++ b/GofPatterns/Behavioral/ObserverPattern/Publisher.cs @@ -0,0 +1,68 @@ +using GofPatterns.Behavioral.ObserverPattern.Exceptions; +using GofPatterns.Behavioral.ObserverPattern.Interfaces; + +namespace GofPatterns.Behavioral.ObserverPattern; + +/// +/// Implementation of the Publisher (or Broadcaster) interface. +/// +/// +public class Publisher : IPublisher +{ + private readonly List> subscribers = new(); + + public void AddSubscriber(ISubscriber subscriber) + { + subscribers.Add(subscriber); + } + + public void RemoveSubscribers() + { + subscribers.Clear(); + } + + public void NotifySubscribers(TInput input) + { + subscribers.ForEach(x => x.Update(input)); + } +} + +/// +/// Implementation of the Publisher (or Broadcaster) interface. +/// +/// +/// +public class Publisher : IPublisher where TCategory : notnull +{ + private readonly Dictionary> publishers = new(); + + public void AddSubscriber(ISubscriber subscriber, TCategory category) + { + IPublisher publisher; + + if (publishers.TryGetValue(category, out var outputPublisher)) + publisher = outputPublisher; + else + publishers[category] = publisher = new Publisher(); + + publisher.AddSubscriber(subscriber); + } + + public void RemoveSubscribers(TCategory category) + { + VerifySubscription(category); + publishers.Remove(category); + } + + public void NotifySubscribers(TInput input, TCategory category) + { + VerifySubscription(category); + publishers[category].NotifySubscribers(input); + } + + private void VerifySubscription(TCategory type) + { + if (!publishers.ContainsKey(type)) + throw new NoSubscriptionFoundException($"No subscription found for category: {type}"); + } +} \ No newline at end of file diff --git a/GofPatterns/GofPatterns.csproj b/GofPatterns/GofPatterns.csproj index 9f8675f..a00842d 100644 --- a/GofPatterns/GofPatterns.csproj +++ b/GofPatterns/GofPatterns.csproj @@ -2,7 +2,7 @@ net6.0 - + enable enable true @@ -10,14 +10,14 @@ True GofPatterns Gof Patterns - 1.2.7 + 1.2.8 viveksacademia vivopensource Desgin Patterns for C# (Gang of Four) - patterns; design-patterns; Gof; gang-of-four; + patterns; design-patterns; gang-of-four; Gof; behavioral-patterns; structural-patterns; creational-patterns; - chain-of-responsibility-pattern; cor; command-pattern; state-pattern; strategy-pattern; mediator-pattern; + chain-of-responsibility-pattern; cor; command-pattern; state-pattern; strategy-pattern; mediator-pattern; observer-pattern; decorator-pattern; proxy-pattern; adapter-pattern; flyweight-pattern; factory-pattern; abstract-factory-pattern; builder-pattern @@ -47,11 +47,10 @@ - + + --> diff --git a/GofPatternsTests/Behavioral/ObserverPattern/PublisherTests.cs b/GofPatternsTests/Behavioral/ObserverPattern/PublisherTests.cs new file mode 100644 index 0000000..4dd59ac --- /dev/null +++ b/GofPatternsTests/Behavioral/ObserverPattern/PublisherTests.cs @@ -0,0 +1,68 @@ +using GofConsoleApp.Examples.Behavioral.ObserverPattern.Components; +using GofPatterns.Behavioral.ObserverPattern; +using GofPatterns.Behavioral.ObserverPattern.Interfaces; +using Moq; +using NUnit.Framework; + +namespace GofPatternsTests.Behavioral.ObserverPattern; + +[TestFixture] +internal class PublisherTests : BaseTest +{ + [Test] + public void AddSubscriber_AddsGivenSubscriber() + { + // act - assert + Assert.DoesNotThrow(() => sutPublisher.AddSubscriber(new PersonJohnDoe(MockConsoleLogger.Object))); + } + + [TestCase("test")] + [TestCase("Hello")] + [TestCase("World")] + public void NotifySubscriber_NotifiesAddedSubscriber(string givenUpdate) + { + // arrange + var johnDoe = new PersonJohnDoe(MockConsoleLogger.Object); + var andrewSmith = new PersonAndrewSmith(MockConsoleLogger.Object); + + sutPublisher.AddSubscriber(johnDoe); + sutPublisher.AddSubscriber(andrewSmith); + + const int expectedLogCount = 2; + + // act + sutPublisher.NotifySubscribers(givenUpdate); + + // assert + MockConsoleLogger.Verify(x => x.Log(It.IsAny()), Times.Exactly(expectedLogCount)); + } + + [Test] + public void RemoveSubscribers_RemovesAllSubscribers() + { + // arrange + var johnDoe = new PersonJohnDoe(MockConsoleLogger.Object); + var andrewSmith = new PersonAndrewSmith(MockConsoleLogger.Object); + + sutPublisher.AddSubscriber(johnDoe); + sutPublisher.AddSubscriber(andrewSmith); + + const string givenUpdate = "test"; + var expectedLogCount = 2; + + sutPublisher.NotifySubscribers(givenUpdate); + + MockConsoleLogger.Verify(x => x.Log(It.IsAny()), Times.Exactly(expectedLogCount)); + + // act + sutPublisher.RemoveSubscribers(); + sutPublisher.NotifySubscribers(givenUpdate); + + // assert + expectedLogCount += 0; // Expected log count should not change + + MockConsoleLogger.Verify(x => x.Log(It.IsAny()), Times.Exactly(expectedLogCount)); + } + + private readonly IPublisher sutPublisher = new Publisher(); +} \ No newline at end of file diff --git a/GofPatternsTests/Behavioral/ObserverPattern/PublisherTestsWithCategory.cs b/GofPatternsTests/Behavioral/ObserverPattern/PublisherTestsWithCategory.cs new file mode 100644 index 0000000..2f6e607 --- /dev/null +++ b/GofPatternsTests/Behavioral/ObserverPattern/PublisherTestsWithCategory.cs @@ -0,0 +1,80 @@ +using GofConsoleApp.Examples.Behavioral.ObserverPattern.Components; +using GofPatterns.Behavioral.ObserverPattern; +using GofPatterns.Behavioral.ObserverPattern.Exceptions; +using GofPatterns.Behavioral.ObserverPattern.Interfaces; +using Moq; +using NUnit.Framework; +using static GofConsoleApp.Examples.Behavioral.ObserverPattern.Components.EnumTopic; + +namespace GofPatternsTests.Behavioral.ObserverPattern; + +[TestFixture] +internal class PublisherTestsWithCategory : BaseTest +{ + [Test] + public void AddSubscriber_AddsGivenSubscriber() + { + // act - assert + Assert.DoesNotThrow(() => sutPublisher.AddSubscriber(new PersonJohnDoe(MockConsoleLogger.Object), Sports)); + } + + [TestCase("Germany won Fifa world cup.", Sports, 2)] + [TestCase("Election campaign started by the candidates.", Politics, 1)] + [TestCase("Heavy snow fall expected in upcoming week.", Weather, 1)] + [TestCase("Holiday season experience flight booking at all time high.", Holidays, 1)] + public void NotifySubscriber_NotifiesAddedSubscriber(string givenUpdate, EnumTopic givenCategory, + int expectedSubscribers) + { + // arrange + var johnDoe = new PersonJohnDoe(MockConsoleLogger.Object); + var andrewSmith = new PersonAndrewSmith(MockConsoleLogger.Object); + + sutPublisher.AddSubscriber(johnDoe, Sports); + sutPublisher.AddSubscriber(andrewSmith, Sports); + sutPublisher.AddSubscriber(johnDoe, Politics); + sutPublisher.AddSubscriber(andrewSmith, Weather); + sutPublisher.AddSubscriber(johnDoe, Holidays); + + // act + sutPublisher.NotifySubscribers(givenUpdate, givenCategory); + + // assert + MockConsoleLogger.Verify(x => x.Log(It.IsAny()), Times.Exactly(expectedSubscribers)); + } + + [Test] + public void RemoveSubscribers_RemovesAllSubscribers() + { + // arrange + var johnDoe = new PersonJohnDoe(MockConsoleLogger.Object); + var andrewSmith = new PersonAndrewSmith(MockConsoleLogger.Object); + + sutPublisher.AddSubscriber(johnDoe, Sports); + sutPublisher.AddSubscriber(andrewSmith, Sports); + sutPublisher.AddSubscriber(johnDoe, Politics); + sutPublisher.AddSubscriber(andrewSmith, Weather); + sutPublisher.AddSubscriber(johnDoe, Holidays); + + const string givenUpdate = "test"; + const int expectedLogCount = 5; + + sutPublisher.NotifySubscribers(givenUpdate, Sports); + sutPublisher.NotifySubscribers(givenUpdate, Politics); + sutPublisher.NotifySubscribers(givenUpdate, Weather); + sutPublisher.NotifySubscribers(givenUpdate, Holidays); + + MockConsoleLogger.Verify(x => x.Log(It.IsAny()), Times.Exactly(expectedLogCount)); + + // act + sutPublisher.RemoveSubscribers(Sports); + sutPublisher.RemoveSubscribers(Politics); + + // assert + Assert.Throws(() => sutPublisher.NotifySubscribers(givenUpdate, Sports)); + Assert.Throws(() => sutPublisher.NotifySubscribers(givenUpdate, Politics)); + Assert.DoesNotThrow(() => sutPublisher.NotifySubscribers(givenUpdate, Weather)); + Assert.DoesNotThrow(() => sutPublisher.NotifySubscribers(givenUpdate, Holidays)); + } + + private readonly IPublisher sutPublisher = new Publisher(); +} \ No newline at end of file diff --git a/GofPatternsTests/Examples/Behavioral/ObserverPattern/ObserverPatternExampleTests.cs b/GofPatternsTests/Examples/Behavioral/ObserverPattern/ObserverPatternExampleTests.cs new file mode 100644 index 0000000..ee73a48 --- /dev/null +++ b/GofPatternsTests/Examples/Behavioral/ObserverPattern/ObserverPatternExampleTests.cs @@ -0,0 +1,39 @@ +using GofConsoleApp.Examples; +using GofConsoleApp.Examples.Behavioral.ObserverPattern; +using Moq; +using NUnit.Framework; + +namespace GofPatternsTests.Examples.Behavioral.ObserverPattern; + +[TestFixture] +[TestOf(typeof(ObserverPatternExample))] +internal class ObserverPatternExampleTests : BaseTest +{ + private const int LogCountForInput = 5; + + [Test] + public void Execute_QuitsExample_ReturnsTrue() + { + // act + var readerValues = new Queue(new[] + { + "Petrol price is at all time high.", "Germany won Fifa WorldCup.", "Freezing conditions grip Europe", "quit" + }); + + var expectedReaderCount = readerValues.Count; + var expectedLogCount = 3 + (expectedReaderCount - 1) * LogCountForInput; + + MockInputReader.Setup(x => x.AcceptInput()).Returns(readerValues.Dequeue); + + // act + var actualResult = sut.Execute(MockConsoleLogger.Object, MockInputReader.Object); + + // assert + Assert.That(actualResult, Is.True); + + MockInputReader.Verify(x => x.AcceptInput(), Times.Exactly(expectedReaderCount)); + MockConsoleLogger.Verify(x => x.Log(It.IsAny()), Times.Exactly(expectedLogCount)); + } + + private readonly BaseExample sut = new ObserverPatternExample(); +} \ No newline at end of file diff --git a/GofPatternsTests/Examples/Behavioral/ObserverPattern/ObserverPatternExampleWithCategoryTest.cs b/GofPatternsTests/Examples/Behavioral/ObserverPattern/ObserverPatternExampleWithCategoryTest.cs new file mode 100644 index 0000000..af66a2e --- /dev/null +++ b/GofPatternsTests/Examples/Behavioral/ObserverPattern/ObserverPatternExampleWithCategoryTest.cs @@ -0,0 +1,40 @@ +using GofConsoleApp.Examples.Behavioral.ObserverPattern; +using Moq; +using NUnit.Framework; +using static GofConsoleApp.Examples.Behavioral.ObserverPattern.Components.EnumTopic; + +namespace GofPatternsTests.Examples.Behavioral.ObserverPattern; + +[TestFixture] +[TestOf(typeof(ObserverPatternExampleWithCategory))] +internal class ObserverPatternExampleWithCategoryTest : BaseTest +{ + + [Test] + public void Execute_PerformsSuccessfulExampleRun_ReturnsTrue() + { + // act + var readerValues = new Queue(new[] + { + Sports.ToString(), "AC Milan has won.", + Weather.ToString(), "Freezing conditions has gripped Europe.", + Politics.ToString(), "German elections are set in next month.", + Holidays.ToString(), "German old cities are becoming famous tourist attractions.", + Quit.ToString() + }); + + var expectedReaderCount = readerValues.Count; + const int expectedLogCount = 29; + + MockInputReader.Setup(x => x.AcceptInput()).Returns(readerValues.Dequeue); + + // act + var actualResult = new ObserverPatternExampleWithCategory().Execute(MockConsoleLogger.Object, MockInputReader.Object); + + // assert + Assert.That(actualResult, Is.True); + + MockInputReader.Verify(x => x.AcceptInput(), Times.Exactly(expectedReaderCount)); + MockConsoleLogger.Verify(x => x.Log(It.IsAny()), Times.Exactly(expectedLogCount)); + } +} \ No newline at end of file diff --git a/README.md b/README.md index b6d7963..f0885f3 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,8 @@ The solution offered in the package has to be implemented in the correct way, bu ### Install -Nuget package is available at: [GofPatterns](https://www.nuget.org/packages/GofPatterns/) +- Target frameworks: NET 6 (Common soon NET 4.5) +- Nuget package is available at: [GofPatterns](https://www.nuget.org/packages/GofPatterns/). ```bash dotnet add package GofPatterns @@ -31,6 +32,7 @@ just click on the link to the design pattern and it will take you to the respect - [State](README/Behavioral/State.md) - [Strategy](README/Behavioral/Strategy.md) - [Mediator](README/Behavioral/Mediator.md) +- [Observer](README/Behavioral/Observer.md) ### Structural Patterns diff --git a/README/Behavioral/Observer.md b/README/Behavioral/Observer.md new file mode 100644 index 0000000..e4b1a6c --- /dev/null +++ b/README/Behavioral/Observer.md @@ -0,0 +1,112 @@ +# Observer Pattern + +- The observer pattern is a software design pattern in which an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods. +- The observer pattern is a design pattern that allows a subject to publish changes to its state. Other objects subscribe to be immediately notified of any changes. +- The observer pattern is mostly used to implement distributed event handling systems, in "event driven" software. Most modern languages such as C# have built in "event" constructs which implement the observer pattern components. + +## Structure + +- It consists of subject, observer, and client (client does pattern execution). +- Subject: An object that maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods. +- Observer: An interface that defines the method to be called when a subject is updated. +- Client: A client class that will use the subject and observer objects. + +## Example + +### Components + +```csharp +// Subscribers +private class PersonJohnDoe : ISubscriber +{ + public void Update(string input) => Console.WriteLine($"John Doe received: {input}"); +} + +private class PersonAndrewSmith : ISubscriber +{ + public void Update(string input) => Console.WriteLine($"Andrew Smith received: {input}"); +} +``` + +### Type 1: Execute pattern with simple implementation + +```csharp +var johnDoe = new PersonJohnDoe(); +var andrewSmith = new PersonAndrewSmith(); + +IPublisher newPublisher = new Publisher(); + +newPublisher.AddSubscriber(johnDoe); +newPublisher.AddSubscriber(andrewSmith); + +newPublisher.NotifySubscribers("Hello Subscribers!"); +newPublisher.NotifySubscribers("How are you doing?"); +``` +``` +// Output: +John Doe received: Hello Subscribers! +Andrew Smith received: Hello Subscribers! +John Doe received: How are you doing? +Andrew Smith received: How are you doing? +``` + +#### Full example + +[ObserverPatternExample](./../../GofConsoleApp/Examples/Behavioral/ObserverPattern/ObserverPatternExample.cs) + + +### Type 2: Execute pattern with category implementation + +```csharp +enum EnumTopic { Sports, Politics, Weather, Holidays } + +// Observer - Pattern execution +var johnDoe = new PersonJohnDoe(); +var andrewSmith = new PersonAndrewSmith(); + +IPublisher newPublisher = new Publisher(); + +newPublisher.AddSubscriber(johnDoe, Sports); +newPublisher.AddSubscriber(andrewSmith, Sports); +newPublisher.AddSubscriber(johnDoe, Politics); +newPublisher.AddSubscriber(andrewSmith, Weather); +newPublisher.AddSubscriber(johnDoe, Holidays); + +newPublisher.NotifySubscribers("Germany won Fifa world cup.", Sports); +newPublisher.NotifySubscribers("Election campaign started by the candidates.", Politics); +newPublisher.NotifySubscribers("Heavy snow fall expected in upcoming week.", Weather); +newPublisher.NotifySubscribers("Holiday season experience flight booking at all time high.", Holidays); +``` + +``` +// Output: +John Doe received: Germany won Fifa world cup. +Andrew Smith received: Germany won Fifa world cup. +John Doe received: Election campaign started by the candidates. +Andrew Smith received: Heavy snow fall expected in upcoming week. +John Doe received: Holiday season experience flight booking at all time high. +``` + +#### Full example + +[ObserverPatternExampleWithCategory](./../../GofConsoleApp/Examples/Behavioral/ObserverPattern/ObserverPatternExampleWithCategory.cs) + +### Classes and interfaces used in example: +- [ISubscriber](./../../GofPatterns/Behavioral/ObserverPattern/Interfaces/ISubscriber.cs) - interface for subscribers +- [IPublisher](./../../GofPatterns/Behavioral/ObserverPattern/Interfaces/IPublisher.cs) - interface for publishers +- [Publisher](./../../GofPatterns/Behavioral/ObserverPattern/Publisher.cs) - class for publishers + + +## Benefits + +- It describes the coupling between the objects and the observer. +- It provides the support for broadcast-type communication. +- It is easy to implement, change and reuse. + +## Similarity with other patterns + +- The observer pattern is also a key part in the familiar model–view–controller (MVC) architectural pattern. The observer pattern is implemented in numerous programming libraries and systems, including almost all GUI toolkits. + +## Drawbacks + +- The observer pattern can cause memory leaks, known as the lapsed listener problem, because in basic implementation it requires both explicit registration and explicit deregistration, as in the dispose pattern, because the subject holds strong references to the observers, keeping them alive. This can be prevented by the subject holding weak references to the observers. \ No newline at end of file diff --git a/README/Creational/AbstractFactory.md b/README/Creational/AbstractFactory.md index edc3c9a..dc193f2 100644 --- a/README/Creational/AbstractFactory.md +++ b/README/Creational/AbstractFactory.md @@ -62,16 +62,16 @@ Console.WriteLine($"Car: {car.Name}, Engine type: {car.EngineType}, Fuel: {car.F Car: Hutchback, Engine type: V6, Fuel: Diesel Car: Sedan, Engine type: V8, Fuel: Petrol ``` -Classes and interfaces used in code: -- [IFactoryItem](./../../GofPatterns/Creational/AbstractFactoryPattern/IFactoryItem.cs) -- [IFactory](./../../GofPatterns/Creational/AbstractFactoryPattern/IFactory.cs) -- [IBaseFactory](./../../GofPatterns/Creational/AbstractFactoryPattern/IBaseFactory.cs) -- [BaseFactory](./../../GofPatterns/Creational/AbstractFactoryPattern/BaseFactory.cs) #### Full example [AbstractFactoryPatternExample](./../../GofConsoleApp/Examples/Creational/AbstractFactoryPattern/AbstractFactoryPatternExample.cs) +### Classes and interfaces used in example: +- [IFactoryItem](./../../GofPatterns/Creational/AbstractFactoryPattern/IFactoryItem.cs) +- [IFactory](./../../GofPatterns/Creational/AbstractFactoryPattern/IFactory.cs) +- [IBaseFactory](./../../GofPatterns/Creational/AbstractFactoryPattern/IBaseFactory.cs) +- [BaseFactory](./../../GofPatterns/Creational/AbstractFactoryPattern/BaseFactory.cs) ## Benefits diff --git a/README/Creational/Builder.md b/README/Creational/Builder.md index a8f7b6b..2228098 100644 --- a/README/Creational/Builder.md +++ b/README/Creational/Builder.md @@ -34,7 +34,7 @@ Console.WriteLine($"Output: {builder.Output()}"); Output: 7.2 ``` -Classes and interfaces used in code: +### Classes and interfaces used in example: - [IBuilder](./../../GofPatterns/Creational/BuilderPattern/IBuilder.cs) - [Builder](./../../GofPatterns/Creational/BuilderPattern/Builder.cs) diff --git a/README/Creational/Factory.md b/README/Creational/Factory.md index 871b11a..016a471 100644 --- a/README/Creational/Factory.md +++ b/README/Creational/Factory.md @@ -58,14 +58,15 @@ Console.WriteLine($"Car: {sedanCar.Name}, Engine type: {sedanCar.EngineType}, Fu Car: Hatchback, Engine type: V6, Fuel: Diesel Car: Sedan, Engine type: V8, Fuel: Petrol ``` -Classes and interfaces used in code: -- [IFactoryItem](./../../GofPatterns/Creational/FactoryPattern/IFactoryItem.cs) -- [IFactory](./../../GofPatterns/Creational/FactoryPattern/IFactory.cs) #### Full example - [FactoryPatternExample](./../../GofConsoleApp/Examples/Creational/FactoryPattern/FactoryPatternExample.cs) +### Classes and interfaces used in example: +- [IFactoryItem](./../../GofPatterns/Creational/FactoryPattern/IFactoryItem.cs) +- [IFactory](./../../GofPatterns/Creational/FactoryPattern/IFactory.cs) + ## Benefits - Factory pattern provides approach to code for interface rather than implementation. From a1d17c5b1aa554ad16a9b24c4df16861f8df33c5 Mon Sep 17 00:00:00 2001 From: viveksacademia4git <45398326+viveksacademia4git@users.noreply.github.com> Date: Sun, 21 Jan 2024 15:12:28 +0100 Subject: [PATCH 2/2] Added target framework 4.5 #38 --- Directory.Packages.props | 33 +++++++++++++++++---------------- GofPatterns/GofPatterns.csproj | 9 ++++----- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f81db5c..2bb3985 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,19 +1,20 @@ - - true - NU1507 - - - - - - - - - - - - - + + true + NU1507 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GofPatterns/GofPatterns.csproj b/GofPatterns/GofPatterns.csproj index a00842d..fabd8fe 100644 --- a/GofPatterns/GofPatterns.csproj +++ b/GofPatterns/GofPatterns.csproj @@ -1,8 +1,8 @@ - net6.0 - + + net6.0;net45 enable enable true @@ -10,7 +10,7 @@ True GofPatterns Gof Patterns - 1.2.8 + 1.2.9 viveksacademia vivopensource Desgin Patterns for C# (Gang of Four) @@ -48,9 +48,8 @@ - +