Design patterns
Fabriek
Zoeken…
Opmerkingen
Bieden een interface voor het maken van families van gerelateerde of afhankelijke objecten zonder hun concrete klassen op te geven.
- GOF 1994
Eenvoudige fabriek (Java)
Een fabriek verkleint de koppeling tussen code die objecten moet maken van objectcreatiecode. Het maken van objecten wordt niet expliciet gemaakt door een klassenbouwer aan te roepen, maar door een functie aan te roepen die het object namens de beller maakt. Een eenvoudig Java-voorbeeld is het volgende:
interface Car {
}
public class CarFactory{
static public Car create(String s) {
switch (s) {
default:
case "us":
case "american": return new Chrysler();
case "de":
case "german": return new Mercedes();
case "jp":
case "japanese": return new Mazda();
}
}
}
class Chrysler implements Car {
public String toString() { return "Chrysler"; }
}
class Mazda implements Car {
public String toString() { return "Mazda"; }
}
class Mercedes implements Car {
public String toString() { return "Mercedes"; }
}
public class CarEx {
public static void main(String args[]) {
Car car = CarFactory.create("us");
System.out.println(car);
}
}
In dit voorbeeld geeft de gebruiker slechts een hint over wat hij nodig heeft en staat de fabriek vrij om iets geschikts te bouwen. Het is een afhankelijkheidsinversie : de uitvoerder van het Car
concept is vrij om een gepaste betonnen Car
te retourneren op verzoek van de gebruiker, die op zijn beurt de details van het gebouwde concrete object niet kent.
Dit is een eenvoudig voorbeeld van hoe de fabriek werkt, in dit voorbeeld is het natuurlijk altijd mogelijk om concrete klassen te instantiëren; maar men kan dit voorkomen door concrete klassen in een pakket te verbergen, zodat de gebruiker de fabriek moet gebruiken.
.Net Fiddle voor bovenstaand voorbeeld.
Abstracte fabriek (C ++)
Abstract fabriekspatroon biedt een manier om een samenhangende verzameling objecten te verkrijgen door een verzameling fabriekenfuncties. Zoals voor elk patroon, wordt de koppeling verminderd door de manier waarop een set objecten wordt gemaakt, samen te vatten, zodat de gebruikerscode zich niet bewust is van de vele details van de objecten die hij nodig heeft.
Het volgende C ++ -voorbeeld illustreert hoe u verschillende soorten objecten van dezelfde (hypothetische) GUI-familie kunt verkrijgen:
#include <iostream>
/* Abstract definitions */
class GUIComponent {
public:
virtual ~GUIComponent() = default;
virtual void draw() const = 0;
};
class Frame : public GUIComponent {};
class Button : public GUIComponent {};
class Label : public GUIComponent {};
class GUIFactory {
public:
virtual ~GUIFactory() = default;
virtual std::unique_ptr<Frame> createFrame() = 0;
virtual std::unique_ptr<Button> createButton() = 0;
virtual std::unique_ptr<Label> createLabel() = 0;
static std::unique_ptr<GUIFactory> create(const std::string& type);
};
/* Windows support */
class WindowsFactory : public GUIFactory {
private:
class WindowsFrame : public Frame {
public:
void draw() const override { std::cout << "I'm a Windows-like frame" << std::endl; }
};
class WindowsButton : public Button {
public:
void draw() const override { std::cout << "I'm a Windows-like button" << std::endl; }
};
class WindowsLabel : public Label {
public:
void draw() const override { std::cout << "I'm a Windows-like label" << std::endl; }
};
public:
std::unique_ptr<Frame> createFrame() override { return std::make_unique<WindowsFrame>(); }
std::unique_ptr<Button> createButton() override { return std::make_unique<WindowsButton>(); }
std::unique_ptr<Label> createLabel() override { return std::make_unique<WindowsLabel>(); }
};
/* Linux support */
class LinuxFactory : public GUIFactory {
private:
class LinuxFrame : public Frame {
public:
void draw() const override { std::cout << "I'm a Linux-like frame" << std::endl; }
};
class LinuxButton : public Button {
public:
void draw() const override { std::cout << "I'm a Linux-like button" << std::endl; }
};
class LinuxLabel : public Label {
public:
void draw() const override { std::cout << "I'm a Linux-like label" << std::endl; }
};
public:
std::unique_ptr<Frame> createFrame() override { return std::make_unique<LinuxFrame>(); }
std::unique_ptr<Button> createButton() override { return std::make_unique<LinuxButton>(); }
std::unique_ptr<Label> createLabel() override { return std::make_unique<LinuxLabel>(); }
};
std::unique_ptr<GUIFactory> GUIFactory::create(const string& type) {
if (type == "windows") return std::make_unique<WindowsFactory>();
return std::make_unique<LinuxFactory>();
}
/* User code */
void buildInterface(GUIFactory& factory) {
auto frame = factory.createFrame();
auto button = factory.createButton();
auto label = factory.createLabel();
frame->draw();
button->draw();
label->draw();
}
int main(int argc, char *argv[]) {
if (argc < 2) return 1;
auto guiFactory = GUIFactory::create(argv[1]);
buildInterface(*guiFactory);
}
Als het gegenereerde uitvoerbare bestand abstractfactory
wordt genoemd, kan de uitvoer het volgende opleveren:
$ ./abstractfactory windows
I'm a Windows-like frame
I'm a Windows-like button
I'm a Windows-like label
$ ./abstractfactory linux
I'm a Linux-like frame
I'm a Linux-like button
I'm a Linux-like label
Eenvoudig voorbeeld van Factory die een IoC (C #) gebruikt
Fabrieken kunnen ook worden gebruikt in combinatie met Inversion of Control (IoC) -bibliotheken.
- Het typische gebruik van een dergelijke fabriek is wanneer we een object willen maken op basis van parameters die niet bekend zijn tot het moment van uitvoering (zoals de huidige gebruiker).
- In deze gevallen kan het soms moeilijk (zo niet onmogelijk) zijn om de IoC-bibliotheek alleen te configureren om dit soort runtime contextuele informatie te verwerken, zodat we het in een fabriek kunnen verpakken.
Voorbeeld
- Stel dat we een
User
, waarvan de kenmerken (ID, beveiligingsniveau, etc.) onbekend zijn tot runtime (aangezien de huidige gebruiker iedereen kan zijn die de applicatie gebruikt). - We moeten de huidige gebruiker nemen en een
ISecurityToken
voor hen verkrijgen, die vervolgens kan worden gebruikt om te controleren of de gebruiker bepaalde acties mag uitvoeren of niet. - De implementatie van ISecurityToken zal variëren afhankelijk van het niveau van de Gebruiker - met andere woorden, ISecurityToken maakt gebruik van polymorfisme .
In dit geval hebben we twee implementaties, die ook Marker Interfaces gebruiken om het gemakkelijker te maken ze te identificeren in de IoC-bibliotheek; de IoC-bibliotheek is in dit geval gewoon samengesteld en geïdentificeerd door de abstractie IContainer
.
Merk ook op dat veel moderne IoC-fabrieken eigen mogelijkheden of plug-ins hebben die het automatisch maken van fabrieken mogelijk maken en de noodzaak van markeerinterfaces vermijden zoals hieronder weergegeven; Aangezien dit echter niet allemaal het geval is, is dit voorbeeld gebaseerd op een eenvoudig, laagste algemeen functioneel concept.
//describes the ability to allow or deny an action based on PerformAction.SecurityLevel
public interface ISecurityToken
{
public bool IsAllowedTo(PerformAction action);
}
//Marker interface for Basic permissions
public interface IBasicToken:ISecurityToken{};
//Marker interface for super permissions
public interface ISuperToken:ISecurityToken{};
//since IBasictoken inherits ISecurityToken, BasicToken can be treated as an ISecurityToken
public class BasicToken:IBasicToken
{
public bool IsAllowedTo(PerformAction action)
{
//Basic users can only perform basic actions
if(action.SecurityLevel!=SecurityLevel.Basic) return false;
return true;
}
}
public class SuperToken:ISuperToken
{
public bool IsAllowedTo(PerformAction action)
{
//Super users can perform all actions
return true;
}
}
Vervolgens zullen we een SecurityToken
fabriek maken, die afhankelijk is van onze IContainer
public class SecurityTokenFactory
{
readonly IContainer _container;
public SecurityTokenFactory(IContainer container)
{
if(container==null) throw new ArgumentNullException("container");
}
public ISecurityToken GetToken(User user)
{
if (user==null) throw new ArgumentNullException("user);
//depending on the user security level, we return a different type; however all types implement ISecurityToken so the factory can produce them.
switch user.SecurityLevel
{
case Basic:
return _container.GetInstance<BasicSecurityToken>();
case SuperUser:
return _container.GetInstance<SuperUserToken>();
}
}
}
Nadat we deze bij de IContainer
hebben geregistreerd:
IContainer.For<SecurityTokenFactory>().Use<SecurityTokenFactory>().Singleton(); //we only need a single instance per app
IContainer.For<IBasicToken>().Use<BasicToken>().PerRequest(); //we need an instance per-request
IContainer.For<ISuperToken>().Use<SuperToken>().PerRequest();//we need an instance per-request
de consumerende code kan het gebruiken om het juiste token te krijgen tijdens runtime:
readonly SecurityTokenFactory _tokenFactory;
...
...
public void LogIn(User user)
{
var token = _tokenFactory.GetToken(user);
user.SetSecurityToken(token);
}
Op deze manier profiteren we van de inkapseling door de fabriek en van het levenscyclusbeheer van de IoC-bibliotheek.
Een abstracte fabriek
Het volgende ontwerppatroon is gecategoriseerd als een creatief patroon.
Een abstracte fabriek wordt gebruikt om een interface te bieden voor het maken van families van gerelateerde objecten, zonder concrete klassen op te geven en kan worden gebruikt om platformspecifieke klassen te verbergen.
interface Tool {
void use();
}
interface ToolFactory {
Tool create();
}
class GardenTool implements Tool {
@Override
public void use() {
// Do something...
}
}
class GardenToolFactory implements ToolFactory {
@Override
public Tool create() {
// Maybe additional logic to setup...
return new GardenTool();
}
}
class FarmTool implements Tool {
@Override
public void use() {
// Do something...
}
}
class FarmToolFactory implements ToolFactory {
@Override
public Tool create() {
// Maybe additional logic to setup...
return new FarmTool();
}
}
Dan zou een leverancier / producent van een soort worden gebruikt die informatie zou krijgen die het mogelijk zou maken om het juiste type fabrieksimplementatie terug te geven:
public final class FactorySupplier {
// The supported types it can give you...
public enum Type {
FARM, GARDEN
};
private FactorySupplier() throws IllegalAccessException {
throw new IllegalAccessException("Cannot be instantiated");
}
public static ToolFactory getFactory(Type type) {
ToolFactory factory = null;
switch (type) {
case FARM:
factory = new FarmToolFactory();
break;
case GARDEN:
factory = new GardenToolFactory();
break;
} // Could potentially add a default case to handle someone passing in null
return factory;
}
}
Fabrieksvoorbeeld door implementatie van Factory-methode (Java)
intent:
Definieer een interface voor het maken van een object, maar laat subklassen beslissen welke klasse ze moeten instantiëren. Met de fabrieksmethode kan een klasse instantiëring naar subklassen uitstellen.
UML-diagram:
Product: het definieert een interface van de objecten die de Factory-methode maakt.
ConcreteProduct: implementeert Productinterface
Maker: verklaart de fabrieksmethode
ConcreateCreator: implementeert de fabrieksmethode om een exemplaar van een betonproduct te retourneren
Probleemstelling: maak een Factory of Games met behulp van Factory Methods, die de spelinterface definieert.
Codefragment:
import java.util.HashMap;
/* Product interface as per UML diagram */
interface Game{
/* createGame is a complex method, which executes a sequence of game steps */
public void createGame();
}
/* ConcreteProduct implementation as per UML diagram */
class Chess implements Game{
public Chess(){
createGame();
}
public void createGame(){
System.out.println("---------------------------------------");
System.out.println("Create Chess game");
System.out.println("Opponents:2");
System.out.println("Define 64 blocks");
System.out.println("Place 16 pieces for White opponent");
System.out.println("Place 16 pieces for Black opponent");
System.out.println("Start Chess game");
System.out.println("---------------------------------------");
}
}
class Checkers implements Game{
public Checkers(){
createGame();
}
public void createGame(){
System.out.println("---------------------------------------");
System.out.println("Create Checkers game");
System.out.println("Opponents:2 or 3 or 4 or 6");
System.out.println("For each opponent, place 10 coins");
System.out.println("Start Checkers game");
System.out.println("---------------------------------------");
}
}
class Ludo implements Game{
public Ludo(){
createGame();
}
public void createGame(){
System.out.println("---------------------------------------");
System.out.println("Create Ludo game");
System.out.println("Opponents:2 or 3 or 4");
System.out.println("For each opponent, place 4 coins");
System.out.println("Create two dices with numbers from 1-6");
System.out.println("Start Ludo game");
System.out.println("---------------------------------------");
}
}
/* Creator interface as per UML diagram */
interface IGameFactory {
public Game getGame(String gameName);
}
/* ConcreteCreator implementation as per UML diagram */
class GameFactory implements IGameFactory {
HashMap<String,Game> games = new HashMap<String,Game>();
/*
Since Game Creation is complex process, we don't want to create game using new operator every time.
Instead we create Game only once and store it in Factory. When client request a specific game,
Game object is returned from Factory instead of creating new Game on the fly, which is time consuming
*/
public GameFactory(){
games.put(Chess.class.getName(),new Chess());
games.put(Checkers.class.getName(),new Checkers());
games.put(Ludo.class.getName(),new Ludo());
}
public Game getGame(String gameName){
return games.get(gameName);
}
}
public class NonStaticFactoryDemo{
public static void main(String args[]){
if ( args.length < 1){
System.out.println("Usage: java FactoryDemo gameName");
return;
}
GameFactory factory = new GameFactory();
Game game = factory.getGame(args[0]);
System.out.println("Game="+game.getClass().getName());
}
}
output:
java NonStaticFactoryDemo Chess
---------------------------------------
Create Chess game
Opponents:2
Define 64 blocks
Place 16 pieces for White opponent
Place 16 pieces for Black opponent
Start Chess game
---------------------------------------
---------------------------------------
Create Checkers game
Opponents:2 or 3 or 4 or 6
For each opponent, place 10 coins
Start Checkers game
---------------------------------------
---------------------------------------
Create Ludo game
Opponents:2 or 3 or 4
For each opponent, place 4 coins
Create two dices with numbers from 1-6
Start Ludo game
---------------------------------------
Game=Chess
Dit voorbeeld toont een Factory
klasse door een FactoryMethod
implementeren.
Game
is de interface voor alle soorten games. Het definieert complexe methode:createGame()
Chess, Ludo, Checkers
zijn verschillende varianten van spellen, die implementatie bieden omcreateGame()
public Game getGame(String gameName)
isFactoryMethod
in de klasseIGameFactory
GameFactory
vooraf verschillende typen games in de constructor. Het implementeertIGameFactory
fabrieksmethode.spelnaam wordt doorgegeven als opdrachtregelargument aan
NotStaticFactoryDemo
getGame
inGameFactory
accepteert een spel naam en keert terug overeenkomstigeGame
object.
Wanneer te gebruiken:
- Fabrieksinstelling : wanneer u object-instantiëringlogica niet wilt blootstellen aan de client / beller
- Abstracte fabriek : wanneer u een interface wilt bieden aan families van gerelateerde of afhankelijke objecten zonder hun concrete klassen op te geven
- Fabrieksmethode: een interface definiëren voor het maken van een object, maar de subklassen laten beslissen welke klasse ze moeten instantiëren
Vergelijking met andere creatiepatronen:
Ontwerp begint met Factory Method (minder gecompliceerd, meer aanpasbaar, subklassen groeien) en evolueren naar Abstract Factory, Prototype of Builder (flexibeler, complexer) naarmate de ontwerper ontdekt waar meer flexibiliteit nodig is
Abstracte Factory- klassen worden vaak geïmplementeerd met Factory Methods , maar ze kunnen ook worden geïmplementeerd met Prototype
Referenties voor verder lezen: Ontwerppatronen voor sourcemaking
Vlieggewichtfabriek (C #)
In eenvoudige woorden:
Een vlieggewichtfabriek die voor een gegeven, reeds bekende sleutel altijd hetzelfde object als reactie geeft. Voor nieuwe sleutels wordt de instantie gemaakt en teruggestuurd.
Gebruik van de fabriek:
ISomeFactory<string, object> factory = new FlyweightFactory<string, object>();
var result1 = factory.GetSomeItem("string 1");
var result2 = factory.GetSomeItem("string 2");
var result3 = factory.GetSomeItem("string 1");
//Objects from different keys
bool shouldBeFalse = result1.Equals(result2);
//Objects from same key
bool shouldBeTrue = result1.Equals(result3);
Implementatie:
public interface ISomeFactory<TKey,TResult> where TResult : new()
{
TResult GetSomeItem(TKey key);
}
public class FlyweightFactory<TKey, TResult> : ISomeFactory<TKey, TResult> where TResult : new()
{
public TResult GetSomeItem(TKey key)
{
TResult result;
if(!Mapping.TryGetValue(key, out result))
{
result = new TResult();
Mapping.Add(key, result);
}
return result;
}
public Dictionary<TKey, TResult> Mapping { get; set; } = new Dictionary<TKey, TResult>();
}
Extra opmerkingen
Ik zou aanraden om aan deze oplossing het gebruik van een IoC Container
toe te voegen (zoals in een ander voorbeeld hier wordt uitgelegd) in plaats van uw eigen nieuwe exemplaren te maken. Men kan dit doen door een nieuwe registratie voor de TResult
aan de container toe te voegen en er vervolgens vanaf te lossen (in plaats van het dictionary
in het voorbeeld).
Fabriek methode
Het patroon van de fabrieksmethode is een creatief patroon dat de instantiëringslogica van een object abstraheert om de clientcode ervan te ontkoppelen.
Wanneer een fabrieksmethode tot een klasse behoort die een implementatie is van een ander fabriekspatroon zoals Abstracte fabriek , is het meestal geschikter om te verwijzen naar het patroon dat door die klasse is geïmplementeerd in plaats van naar het fabrieksmethodepatroon.
Naar het patroon van de fabrieksmethode wordt vaker verwezen bij het beschrijven van een fabrieksmethode die behoort tot een klasse die niet primair een fabriek is.
Het kan bijvoorbeeld voordelig zijn om een fabrieksmethode op een object te plaatsen dat een domeinconcept weergeeft als dat object een staat omsluit die het creatieproces van een ander object zou vereenvoudigen. Een fabrieksmethode kan ook leiden tot een ontwerp dat beter is afgestemd op de alomtegenwoordige taal van een specifieke context.
Hier is een codevoorbeeld:
//Without a factory method
Comment comment = new Comment(authorId, postId, "This is a comment");
//With a factory method
Comment comment = post.comment(authorId, "This is a comment");