1 /** 2 * This module contains instance factory facilities 3 * 4 * Authors: 5 * Mike Bierlee, m.bierlee@lostmoment.com 6 * Copyright: 2014-2025 Mike Bierlee 7 * License: 8 * This software is licensed under the terms of the MIT license. 9 * The full terms of the license can be found in the LICENSE file. 10 */ 11 12 module poodinis.factory; 13 14 import poodinis.container : DependencyContainer; 15 import poodinis.imports : createImportsString; 16 17 import std.typecons : Flag; 18 import std.exception : enforce; 19 import std.traits : Parameters, isBuiltinType, fullyQualifiedName; 20 import std.string : format; 21 22 debug { 23 import std.stdio : writeln; 24 } 25 26 alias CreatesSingleton = Flag!"CreatesSingleton"; 27 alias InstanceFactoryMethod = Object delegate(); 28 alias InstanceEventHandler = void delegate(Object instance); 29 30 class InstanceCreationException : Exception { 31 this(string message, string file = __FILE__, size_t line = __LINE__) { 32 super(message, file, line); 33 } 34 } 35 36 struct InstanceFactoryParameters { 37 TypeInfo_Class instanceType; 38 CreatesSingleton createsSingleton = CreatesSingleton.yes; 39 Object existingInstance; 40 InstanceFactoryMethod factoryMethod; 41 } 42 43 class InstanceFactory { 44 private Object instance = null; 45 private InstanceFactoryParameters _factoryParameters; 46 private InstanceEventHandler _constructionHandler; 47 48 this() { 49 factoryParameters = InstanceFactoryParameters(); 50 } 51 52 void factoryParameters(InstanceFactoryParameters factoryParameters) { 53 if (factoryParameters.factoryMethod is null) { 54 factoryParameters.factoryMethod = &this.createInstance; 55 } 56 57 if (factoryParameters.existingInstance !is null) { 58 factoryParameters.createsSingleton = CreatesSingleton.yes; 59 this.instance = factoryParameters.existingInstance; 60 } 61 62 _factoryParameters = factoryParameters; 63 } 64 65 InstanceFactoryParameters factoryParameters() { 66 return _factoryParameters; 67 } 68 69 Object getInstance() { 70 if (_factoryParameters.createsSingleton && instance !is null) { 71 debug (poodinisVerbose) { 72 printDebugUseExistingInstance(); 73 } 74 75 return instance; 76 } 77 78 debug (poodinisVerbose) { 79 printDebugCreateNewInstance(); 80 } 81 82 instance = _factoryParameters.factoryMethod(); 83 if (_constructionHandler !is null) { 84 _constructionHandler(instance); 85 } 86 87 return instance; 88 } 89 90 void onConstructed(InstanceEventHandler handler) { 91 _constructionHandler = handler; 92 } 93 94 private void printDebugUseExistingInstance() { 95 debug { 96 if (_factoryParameters.instanceType !is null) { 97 writeln(format("DEBUG: Existing instance returned of type %s", 98 _factoryParameters.instanceType.toString())); 99 } else { 100 writeln("DEBUG: Existing instance returned from custom factory method"); 101 } 102 } 103 } 104 105 private void printDebugCreateNewInstance() { 106 debug { 107 if (_factoryParameters.instanceType !is null) { 108 writeln(format("DEBUG: Creating new instance of type %s", 109 _factoryParameters.instanceType.toString())); 110 } else { 111 writeln("DEBUG: Creating new instance from custom factory method"); 112 } 113 } 114 } 115 116 protected Object createInstance() { 117 enforce!InstanceCreationException(_factoryParameters.instanceType, 118 "Instance type is not defined, cannot create instance without knowing its type."); 119 return _factoryParameters.instanceType.create(); 120 } 121 } 122 123 class ConstructorInjectingInstanceFactory(InstanceType) : InstanceFactory { 124 private shared DependencyContainer container; 125 private bool isBeingInjected = false; 126 127 this(shared DependencyContainer container) { 128 this.container = container; 129 } 130 131 private static string createArgumentList(Params...)() { 132 string argumentList = ""; 133 foreach (param; Params) { 134 if (argumentList.length > 0) { 135 argumentList ~= ","; 136 } 137 138 argumentList ~= "container.resolve!(" ~ param.stringof ~ ")"; 139 } 140 return argumentList; 141 } 142 143 private static string createImportList(Params...)() { 144 string importList = ""; 145 foreach (param; Params) { 146 importList ~= createImportsString!param; 147 } 148 return importList; 149 } 150 151 private static bool parametersAreValid(Params...)() { 152 bool isValid = true; 153 foreach (param; Params) { 154 if (isBuiltinType!param || is(param == struct)) { 155 isValid = false; 156 break; 157 } 158 } 159 160 return isValid; 161 } 162 163 protected override Object createInstance() { 164 enforce!InstanceCreationException(container, 165 "A dependency container is not defined. Cannot perform constructor injection without one."); 166 enforce!InstanceCreationException(!isBeingInjected, 167 format("%s is already being created and injected; possible circular dependencies in constructors?", 168 InstanceType.stringof)); 169 170 Object instance = null; 171 static if (__traits(compiles, __traits(getOverloads, InstanceType, `__ctor`))) { 172 foreach (ctor; __traits(getOverloads, InstanceType, `__ctor`)) { 173 static if (parametersAreValid!(Parameters!ctor)) { 174 isBeingInjected = true; 175 mixin(createImportsString!InstanceType ~ createImportList!( 176 Parameters!ctor) ~ ` 177 instance = new ` 178 ~ fullyQualifiedName!InstanceType ~ `(` ~ createArgumentList!( 179 Parameters!ctor) ~ `); 180 `); 181 isBeingInjected = false; 182 break; 183 } 184 } 185 } 186 187 if (instance is null) { 188 instance = typeid(InstanceType).create(); 189 } 190 191 enforce!InstanceCreationException(instance !is null, 192 "Unable to create instance of type" ~ InstanceType.stringof 193 ~ ", does it have injectable constructors?"); 194 195 return instance; 196 } 197 } 198 199 version (unittest) : // 200 201 import poodinis; 202 import poodinis.testclasses; 203 import std.exception; 204 205 // Test instance factory with singletons 206 unittest { 207 auto factory = new InstanceFactory(); 208 factory.factoryParameters = InstanceFactoryParameters(typeid(TestImplementation), 209 CreatesSingleton.yes); 210 auto instanceOne = factory.getInstance(); 211 auto instanceTwo = factory.getInstance(); 212 213 assert(instanceOne !is null, "Created factory instance is null"); 214 assert(instanceOne is instanceTwo, "Created factory instance is not the same"); 215 } 216 217 // Test instance factory with new instances 218 unittest { 219 auto factory = new InstanceFactory(); 220 factory.factoryParameters = InstanceFactoryParameters(typeid(TestImplementation), 221 CreatesSingleton.no); 222 auto instanceOne = factory.getInstance(); 223 auto instanceTwo = factory.getInstance(); 224 225 assert(instanceOne !is null, "Created factory instance is null"); 226 assert(instanceOne !is instanceTwo, "Created factory instance is the same"); 227 } 228 229 // Test instance factory with existing instances 230 unittest { 231 auto existingInstance = new TestImplementation(); 232 auto factory = new InstanceFactory(); 233 factory.factoryParameters = InstanceFactoryParameters(typeid(TestImplementation), 234 CreatesSingleton.yes, existingInstance); 235 auto instanceOne = factory.getInstance(); 236 auto instanceTwo = factory.getInstance(); 237 238 assert(instanceOne is existingInstance, 239 "Created factory instance is not the existing instance"); 240 assert(instanceTwo is existingInstance, 241 "Created factory instance is not the existing instance when called again"); 242 } 243 244 // Test instance factory with existing instances when setting singleton flag to "no" 245 unittest { 246 auto existingInstance = new TestImplementation(); 247 auto factory = new InstanceFactory(); 248 factory.factoryParameters = InstanceFactoryParameters(typeid(TestImplementation), 249 CreatesSingleton.no, existingInstance); 250 auto instance = factory.getInstance(); 251 252 assert(instance is existingInstance, 253 "Created factory instance is not the existing instance"); 254 } 255 256 // Test creating instance using custom factory method 257 unittest { 258 Object factoryMethod() { 259 auto instance = new TestImplementation(); 260 instance.someContent = "Ducks!"; 261 return instance; 262 } 263 264 auto factory = new InstanceFactory(); 265 factory.factoryParameters = InstanceFactoryParameters(null, 266 CreatesSingleton.yes, null, &factoryMethod); 267 auto instance = cast(TestImplementation) factory.getInstance(); 268 269 assert(instance !is null, 270 "No instance was created by factory or could not be cast to expected type"); 271 assert(instance.someContent == "Ducks!"); 272 } 273 274 // Test injecting constructor of class 275 unittest { 276 auto container = new shared DependencyContainer(); 277 container.register!TestImplementation; 278 279 auto factory = new ConstructorInjectingInstanceFactory!ClassWithConstructor(container); 280 auto instance = cast(ClassWithConstructor) factory.getInstance(); 281 282 assert(instance !is null); 283 assert(instance.testImplementation is container.resolve!TestImplementation); 284 } 285 286 // Test injecting constructor of class with multiple constructors injects the first candidate 287 unittest { 288 auto container = new shared DependencyContainer(); 289 container.register!SomeOtherClassThen; 290 container.register!TestImplementation; 291 292 auto factory = new ConstructorInjectingInstanceFactory!ClassWithMultipleConstructors( 293 container); 294 auto instance = cast(ClassWithMultipleConstructors) factory.getInstance(); 295 296 assert(instance !is null); 297 assert(instance.someOtherClassThen is container.resolve!SomeOtherClassThen); 298 assert(instance.testImplementation is null); 299 } 300 301 // Test injecting constructor of class with multiple constructor parameters 302 unittest { 303 auto container = new shared DependencyContainer(); 304 container.register!SomeOtherClassThen; 305 container.register!TestImplementation; 306 307 auto factory = new ConstructorInjectingInstanceFactory!ClassWithConstructorWithMultipleParameters( 308 container); 309 auto instance = cast(ClassWithConstructorWithMultipleParameters) factory.getInstance(); 310 311 assert(instance !is null); 312 assert(instance.someOtherClassThen is container.resolve!SomeOtherClassThen); 313 assert(instance.testImplementation is container.resolve!TestImplementation); 314 } 315 316 // Test injecting constructor of class with primitive constructor parameters 317 unittest { 318 auto container = new shared DependencyContainer(); 319 container.register!SomeOtherClassThen; 320 321 auto factory = new ConstructorInjectingInstanceFactory!ClassWithPrimitiveConstructor( 322 container); 323 auto instance = cast(ClassWithPrimitiveConstructor) factory.getInstance(); 324 325 assert(instance !is null); 326 assert(instance.someOtherClassThen is container.resolve!SomeOtherClassThen); 327 } 328 329 // Test injecting constructor of class with struct constructor parameters 330 unittest { 331 auto container = new shared DependencyContainer(); 332 container.register!SomeOtherClassThen; 333 334 auto factory = new ConstructorInjectingInstanceFactory!ClassWithStructConstructor(container); 335 auto instance = cast(ClassWithStructConstructor) factory.getInstance(); 336 337 assert(instance !is null); 338 assert(instance.someOtherClassThen is container.resolve!SomeOtherClassThen); 339 } 340 341 // Test injecting constructor of class with empty constructor will skip injection 342 unittest { 343 auto container = new shared DependencyContainer(); 344 345 auto factory = new ConstructorInjectingInstanceFactory!ClassWithEmptyConstructor(container); 346 auto instance = cast(ClassWithEmptyConstructor) factory.getInstance(); 347 348 assert(instance !is null); 349 assert(instance.someOtherClassThen is null); 350 } 351 352 // Test injecting constructor of class with no candidates fails 353 unittest { 354 auto container = new shared DependencyContainer(); 355 356 auto factory = new ConstructorInjectingInstanceFactory!ClassWithNonInjectableConstructor( 357 container); 358 359 assertThrown!InstanceCreationException(factory.getInstance()); 360 }