Skip to content

Commit

Permalink
✨ feature: Method to Set Instance (#45)
Browse files Browse the repository at this point in the history
* ✨ feature: Add Method to Set the Instance

* 🚨 tests: Create Mock Predefined Types

* 🚨 tests: Add Test for Set Instance

* 🧹 chore: Comment Exceptions

* 🧹 chore: Add Comments for Methods

* 🧹 chore: Add Comments to Singleton Base

* 🧹 chore: Add Comments to Singleton Factory

* 🧹 chore: Comments

* 🚨 tests: Add Additional Test
  • Loading branch information
TylerCarrol authored Oct 9, 2024
1 parent 7f489cd commit e216e9a
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 1 deletion.
43 changes: 43 additions & 0 deletions TJC.Singleton.Tests/Mocks/Valid/MockSingletonPredefinedTypes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
namespace TJC.Singleton.Tests.Mocks.Valid;

/// <summary>
/// This singleton is an example of the proper intended use of the <see cref="SingletonBase{TMyClass}"/>.
/// <para>This singleton has predefined types, similar to what may be seen within a settings singleton.</para>
/// </summary>
internal class MockSingletonPreDefinedTypes : SingletonBase<MockSingletonPreDefinedTypes>
{
#region Constructor

private MockSingletonPreDefinedTypes() { }

#endregion

#region Predefined Types

public static MockSingletonPreDefinedTypes Default { get; } = new() { Setting1 = "Setting1", Setting2 = "Setting2" };

public static MockSingletonPreDefinedTypes Empty { get; } = new() { Setting1 = string.Empty, Setting2 = string.Empty };

public static MockSingletonPreDefinedTypes Alphabet { get; } = new() { Setting1 = "ABC", Setting2 = "DEF" };

public static MockSingletonPreDefinedTypes Numbers { get; } = new() { Setting1 = "123", Setting2 = "456" };

public static MockSingletonPreDefinedTypes Symbols { get; } = new() { Setting1 = "!@#", Setting2 = "$%^" };

#endregion

#region Properties

public string Setting1 { get; private set; } = "Setting1";

public string Setting2 { get; private set; } = "Setting2";

#endregion

#region Methods

public static void SetInstance(MockSingletonPreDefinedTypes value) =>
SetBaseInstance(value);

#endregion
}
34 changes: 34 additions & 0 deletions TJC.Singleton.Tests/Tests/SetInstance/SetInstanceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace TJC.Singleton.Tests.Tests.SetInstance;

[TestClass]
public class SetInstanceTests
{
[TestMethod]
public void SetInstance_PredefinedTypes()
{
MockSingletonPreDefinedTypes.SetInstance(MockSingletonPreDefinedTypes.Default);

Assert.AreEqual("Setting1", MockSingletonPreDefinedTypes.Instance.Setting1);
Assert.AreEqual("Setting2", MockSingletonPreDefinedTypes.Instance.Setting2);

MockSingletonPreDefinedTypes.SetInstance(MockSingletonPreDefinedTypes.Empty);

Assert.AreEqual(string.Empty, MockSingletonPreDefinedTypes.Instance.Setting1);
Assert.AreEqual(string.Empty, MockSingletonPreDefinedTypes.Instance.Setting2);

MockSingletonPreDefinedTypes.SetInstance(MockSingletonPreDefinedTypes.Alphabet);

Assert.AreEqual("ABC", MockSingletonPreDefinedTypes.Instance.Setting1);
Assert.AreEqual("DEF", MockSingletonPreDefinedTypes.Instance.Setting2);

MockSingletonPreDefinedTypes.SetInstance(MockSingletonPreDefinedTypes.Numbers);

Assert.AreEqual("123", MockSingletonPreDefinedTypes.Instance.Setting1);
Assert.AreEqual("456", MockSingletonPreDefinedTypes.Instance.Setting2);

MockSingletonPreDefinedTypes.SetInstance(MockSingletonPreDefinedTypes.Symbols);

Assert.AreEqual("!@#", MockSingletonPreDefinedTypes.Instance.Setting1);
Assert.AreEqual("$%^", MockSingletonPreDefinedTypes.Instance.Setting2);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
namespace TJC.Singleton.Exceptions;

/// <summary>
/// This exception is thrown when a singleton class has an invalid constructor.
/// </summary>
/// <param name="message"></param>
public class InvalidSingletonConstructorException(string? message) : Exception(message);
4 changes: 4 additions & 0 deletions TJC.Singleton/Exceptions/SingletonInitializationException.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
namespace TJC.Singleton.Exceptions;

/// <summary>
/// This exception is thrown when a singleton fails to initialize.
/// </summary>
/// <param name="message"></param>
public class SingletonInitializationException(string? message) : Exception(message);
26 changes: 26 additions & 0 deletions TJC.Singleton/Factories/SingletonFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ private class PlaceholderSingleton : SingletonBase<PlaceholderSingleton>;

#endregion

/// <summary>
/// Instantiate all singletons in the current app domain.
/// </summary>
/// <param name="trace"></param>
/// <param name="throwIfFailed"></param>
/// <exception cref="Exception"></exception>
public static void InstantiateAll(bool trace = true,
bool throwIfFailed = false)
{
Expand All @@ -31,6 +37,10 @@ public static void InstantiateAll(bool trace = true,
throw new Exception($"{string.Join(", ", failedToInstantiate)}");
}

/// <summary>
/// Get all singleton types in the current app domain.
/// </summary>
/// <returns></returns>
public static List<Type> GetSingletonTypes()
{
var singletons = new List<Type>();
Expand All @@ -53,6 +63,22 @@ public static List<Type> GetSingletonTypes()
return singletons;
}

/// <summary>
/// Instantiates a singleton of type <seealso cref="T"/>.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="trace"></param>
/// <returns></returns>
public static bool Instantiate<T>(bool trace) =>
Instantiate(typeof(T), trace);

/// <summary>
/// Instantiates a singleton of given type.
/// </summary>
/// <param name="singleton"></param>
/// <param name="trace"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
private static bool Instantiate(this Type singleton, bool trace)
{
if (trace)
Expand Down
21 changes: 21 additions & 0 deletions TJC.Singleton/Helpers/SingletonConstructorHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,20 @@ public static class SingletonConstructorHelpers
{
#region Get Singlet Constructor

/// <summary>
/// Gets the singleton constructor for the type <seealso cref="T"/>.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static ConstructorInfo GetSingletonConstructor<T>() =>
GetSingletonConstructor(typeof(T));

/// <summary>
/// Gets the singleton constructor for the type.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
/// <exception cref="InvalidSingletonConstructorException"></exception>
public static ConstructorInfo GetSingletonConstructor(Type type)
{
// Ensure there is no public constructor
Expand All @@ -28,9 +39,19 @@ public static ConstructorInfo GetSingletonConstructor(Type type)

#region Check if Singleton has Valid Constructor

/// <summary>
/// Checks if a singleton of type <seealso cref="T"/> has a valid constructor.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static bool HasValidSingletonConstructor<T>() =>
HasValidSingletonConstructor(typeof(T));

/// <summary>
/// Checks if the singleton has a valid constructor.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public static bool HasValidSingletonConstructor(Type type)
{
try
Expand Down
6 changes: 6 additions & 0 deletions TJC.Singleton/Helpers/SingletonIdentifierHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

public static class SingletonIdentifierHelpers
{
/// <summary>
/// Determines if a type is a concrete singleton.
/// <para>I.e. If it is a <c>non-abstract class</c> that derives from <see cref="SingletonBase{TDerivedClass}"/>, or a class that does (recursively).</para>
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public static bool IsConcreteSingleton(Type type)
{
// Ensure type is a concrete class
Expand Down
29 changes: 28 additions & 1 deletion TJC.Singleton/SingletonBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,53 @@ public abstract class SingletonBase<TDerivedClass> where TDerivedClass : Singlet
{
#region Fields

private static readonly Lazy<TDerivedClass> _instance = new(CreateInstance);
/// <summary>
/// The private singleton instance.
/// </summary>
private static Lazy<TDerivedClass> _instance = new(CreateInstance);

#endregion

#region Properties

/// <summary>
/// The singleton instance.
/// </summary>
public static TDerivedClass Instance => _instance.Value;

/// <summary>
/// Check if the singleton has been instantiated.
/// </summary>
public static bool IsInstantiated => _instance.IsValueCreated;

#endregion

#region Methods

/// <summary>
/// This method is called when the singleton is first accessed.
/// <para>It is used to create the singleton instance by calling the constructor of the derived class.</para>
/// </summary>
/// <returns></returns>
/// <exception cref="SingletonInitializationException"></exception>
private static TDerivedClass CreateInstance()
{
// Use reflection to create an instance of the derived class.
var ctor = SingletonConstructorHelpers.GetSingletonConstructor<TDerivedClass>();
return (TDerivedClass)ctor.Invoke(null) ??
throw new SingletonInitializationException($"[{typeof(TDerivedClass)}] singleton failed to initialize");
}

/// <summary>
/// This method is used to set the singleton instance to a specific value.
/// <para>It is primarily used to set the instance to a predefined type.</para>
/// <para>For example, singletons are often used as settings, which have predefined types, such as <c>Default</c>, <c>Verbose</c>, <c>Silent</c>, etc.</para>
/// <para>This is set as <c>protected</c> so that it can only be accessed from within the singleton itself, since not all singletons should be able to be set.</para>
/// </summary>
/// <param name="value"></param>
protected static void SetBaseInstance(TDerivedClass value)
{
_instance = new(() => value);
}

#endregion
Expand Down

0 comments on commit e216e9a

Please sign in to comment.