joining late to the party!
I agree with you @FZE and you partially answer yourself in the question when you talk about a factory class.
What I tell my teams is "NEVER EVER do a new
in a controller or wherever EXCEPT for inside Factory classes and unit tests".
But not for going towards "named constructors". For me having "one" constructor or "many" constructors is a small problem when compared to being able to "inject" dependencies. This is the real thing that matters.
The Pizzeria
example
Say your sister buys a real pizzeria. And we model it to help in her launch.
Say we have a Pizzeria
class that needs one or more Oven
items to cook pizzas.
Imagine that by design the Pizzeria
does not "come with ovens" but all the oposite: When you purchase the pizzeria it was announced that "it has no ovens". The Pizzeria
is therefore created with zero ovens and then itself needs to increase or decrease the number of ovens as needed.
You could even say that the Oven
cannot exist by itself unless it is an ElectricalOven
or a FireOven
. We could make then Oven
an abstract class that cannot exist by itself.
You see your sister
First off your sister tells you "tomorrow I will bring in some cables, metals and buttons and I'll try to build my oven". You tell her "crazy!!". How do we instantiate the ovens? Never doing a new Oven
. In fact you do not "build" an oven in your pizzeria by yourself!!!
Next day you find your sister in the door of the pizzeria shouting out loud "hey friends!! give me an oven!!" - Wow she got mad!!! You don't have a "magic global function" out there there. You just don't go out the pizzeria and shout out loud "hey friends, give me an oven!!". That's not the way! That is what a global function would look like! Ugly.
Third day... you find your sister in front a small altar with a few candles and you hear her "praying"... "Oh please, God of Ovens, bring me an oven". - Definitvely, she is for the hospital. You tell her "there's not such a thing like God of Ovens" and she cleverly answers "I do know! I'm not silly. I know there's not a God of Ovens". So? you ask. She says "I was praying to the Concept Oven" (static methods of the class Oven - very bad thing!!).
After all this madness, you call her...
What you tell to your sister
Hey, sister... What you have do is to call your machinery provider (if you buy all your things to the same company). Or maybe (depending on the specificity or your partners), you maybe have specifically an oven provider.
More over... imagine that the pizzeria belongs to a franchise. You do not have even to worry to find "oven providers". Just the franchise network will tell you "Hey, if you need ovens, just call Helena at this number and order one".
Helena here is your oven provider.
See the similitude: The franchise network at the end is who shapes your Pizzeria by giving you "hints" on "who to talk for specific things" and you don't mind if the person changes, as the protocols have been designed well. Same way an application "configures the dependencies" of a given well-designed model dependant on interfaces only.
The good thing about this is that you don't have to care about the power in watts or if it is to be electrical or fire. You just tell Helena "hey, Helena, please serve me a small oven only for weekends, for capacity of 5 parallel pizzas" or "hey, Helena, please send me an oven for 15 parallel pizzas for daily work".
Provided that all them are "ovens" and conform to the "oven standard", Helena will get you not only an oven properly initialized, but also an oven "of the proper type".
So, in the end:
This is the only solution that makes the pizzeria work well:
- Do not try to build your own oven (better get an oven from someone that knows how to build ones. Ie. Don't do a "new".
- Do not try to call the open world "hey give me an oven" (don't call a global function)
- ** I love this one ** Do not "pray to the concept oven" to give you an oven (don't call static methods on the Oven)
- DO get an oven provider from somewhere (most probably in your constructor) and then call your oven provider to give you one.
If your provider does only do "ovens and only ovens" you might want to call it oven factory ;)
Conclusion
Most people say to change this:
use Pizza\Oven\Electrical as ElectricalOven;
class Pizzeria
{
private array $ovens = [];
public function increaseCapacity( int $numberOfPizzas, Planning $planning ) : void
{
$watts = $this->someWeirdCalculator( $numberOfPizzas, $planning );
$this->ovens[] = new ElectricalOven( $watts );
}
private function someWeirdCalculator( int $numberOfPizzas, Planning $planning ) : int
{
// do stuff
return $watts;
}
}
into this:
use Pizza\Oven\Electrical as ElectricalOven;
class Pizzeria
{
private array $ovens = [];
public function increaseCapacity( int $numberOfPizzas, Planning $planning ) : void
{
$this->ovens[] = ElectricalOven::fromPlanning( $numberOfPizzas, $planning );
}
}
But this is still heavily coupled to the ElectricalOven and this is very ugly and bad. The coupling is because to use static methods, even if you don't have to have an object you still refer to that particular class.
Factory fans that don't mind about hard or loose coupling are going to do a mistake here newing the factory. They would say to move to something like the following code. To follow the example instead of Factory I'll call it Provider. I don't mind the name for this example, choose the name that better reflects your domain. They will say "hey, now the provider can give you both ElectricalOven
s and FireOven
s.
use Pizza\OvenProvider\Helena as OvenProvider;
class Pizzeria
{
private array $ovens = [];
public function increaseCapacity( int $numberOfPizzas, Planning $planning )
{
$ovenProvider = new OvenProvider()
$this->ovens[] = $ovenProvider->fromPlanning( $numberOfPizzas, $planning );
}
}
This still has a hard problem: It you make a deal with Helena when you launch your pizzeria, it's like marrying her. You will always depend on the ovens Helena can provide you. What if 5 years later you discover there's a new provider? Should you "redesign your pizzeria and deploy it again"? no!!! You should be able to change it!!!
I always push all the new
s to factories gotten via Dependency Injection and inject an Interface not a class name. Here OvenProvider
would be an interface, not a class:
use Pizza\OvenProvider\OvenProviderInterface as OvenProvider;
class Pizzeria
{
private array $ovens;
private OvenProvider $ovenProvider;
public function __construct( OvenProvider $ovenProvider )
{
$this->ovens = [];
$this->ovenProvider = $ovenProvider;
}
public function increaseCapacity( int $numberOfPizzas, Planning $planning )
{
$this->ovens[] = $this->ovenProvider->fromPlanning( $numberOfPizzas, $planning );
}
}
At every run you can even set the dependencies depending on Env Vars. For example you could build a docker image and run this instance with provider A that gives you ovens type A1 and A2 and there run another instance of the same binary that works with provider B that gives you bigger more powerful ovens type B1, B2 and B3. No need to recompile your Pizzeria class. It just works with "zero" dependendencies provided that both A and B are implementing the interface OvenProvider
.
Coding overhead
The coding overhead is zero. See the Pizzeria class got a new line in the constructor to store the given factory or provider. Just a simple $this->myNiceService = $myNiceInjectedService
.
From the factory point of view, the only "complex thing" is to convert "number of pizzas and Planning" into the watts to create the oven and this function was already written in the first ugle example. So no "extra" coding, just moving code around.
That is...
Let the Oven
class deal with things like heatUp( int $degrees )
, coolDown( int $degrees )
and things like those.
And let Helena "calculate of what kind of oven do I need and its power and so" because she is the expert.
Never forget
And to do never forget this, let me finish with an image. You know: an image is worth a thousand words, no?
This

is not the same than this

And if they are not, they deserve different classes.
new
ing objects (I.e. new has to appear somewhere)new
ing a customer makes no sense to the business. Creating a customer from registration session does. I see the motivation there.