Quels sont les exemples courants et réels d'utilisation du modèle de générateur? Qu'est-ce que ça t'achète? Pourquoi ne pas simplement utiliser un motif d'usine?
Quels sont les exemples courants et réels d'utilisation du modèle de générateur? Qu'est-ce que ça t'achète? Pourquoi ne pas simplement utiliser un motif d'usine?
Réponses:
La principale différence entre un constructeur et une IMHO d'usine, c'est qu'un constructeur est utile lorsque vous devez faire beaucoup de choses pour construire un objet. Imaginez par exemple un DOM. Vous devez créer de nombreux nœuds et attributs pour obtenir votre objet final. Une fabrique est utilisée lorsque la fabrique peut facilement créer l'objet entier dans un appel de méthode.
Un exemple d'utilisation d'un générateur est la construction d'un document XML, j'ai utilisé ce modèle lors de la création de fragments HTML par exemple, je pourrais avoir un générateur pour construire un type spécifique de table et il pourrait avoir les méthodes suivantes (les paramètres ne sont pas affichés) :
BuildOrderHeaderRow()
BuildLineItemSubHeaderRow()
BuildOrderRow()
BuildLineItemSubRow()
Ce constructeur cracherait alors le HTML pour moi. C'est beaucoup plus facile à lire que de parcourir une grande méthode procédurale.
Découvrez Builder Pattern sur Wikipedia .
Voici quelques raisons plaidant en faveur de l'utilisation du modèle et de l'exemple de code en Java, mais il s'agit d'une implémentation du modèle de générateur couvert par le gang des quatre dans les modèles de conception . Les raisons pour lesquelles vous l'utiliseriez en Java s'appliquent également à d'autres langages de programmation.
Comme l'indique Joshua Bloch dans Effective Java, 2nd Edition :
Le modèle de générateur est un bon choix lors de la conception de classes dont les constructeurs ou les usines statiques auraient plus d'une poignée de paramètres.
Nous avons tous à un moment donné rencontré une classe avec une liste de constructeurs où chaque ajout ajoute un nouveau paramètre d'option:
Pizza(int size) { ... }
Pizza(int size, boolean cheese) { ... }
Pizza(int size, boolean cheese, boolean pepperoni) { ... }
Pizza(int size, boolean cheese, boolean pepperoni, boolean bacon) { ... }
C'est ce qu'on appelle le modèle de constructeur télescopique. Le problème avec ce modèle est qu'une fois que les constructeurs ont 4 ou 5 paramètres, il devient difficile de se souvenir de l' ordre requis des paramètres ainsi que du constructeur particulier que vous pourriez souhaiter dans une situation donnée.
Une alternative que vous avez au modèle de constructeur télescopique est le modèle JavaBean où vous appelez un constructeur avec les paramètres obligatoires, puis appelez tous les paramètres facultatifs après:
Pizza pizza = new Pizza(12);
pizza.setCheese(true);
pizza.setPepperoni(true);
pizza.setBacon(true);
Le problème ici est que, parce que l'objet est créé sur plusieurs appels, il peut être dans un état incohérent au cours de sa construction. Cela nécessite également beaucoup d'efforts supplémentaires pour garantir la sécurité du fil.
La meilleure alternative est d'utiliser le modèle Builder.
public class Pizza {
private int size;
private boolean cheese;
private boolean pepperoni;
private boolean bacon;
public static class Builder {
//required
private final int size;
//optional
private boolean cheese = false;
private boolean pepperoni = false;
private boolean bacon = false;
public Builder(int size) {
this.size = size;
}
public Builder cheese(boolean value) {
cheese = value;
return this;
}
public Builder pepperoni(boolean value) {
pepperoni = value;
return this;
}
public Builder bacon(boolean value) {
bacon = value;
return this;
}
public Pizza build() {
return new Pizza(this);
}
}
private Pizza(Builder builder) {
size = builder.size;
cheese = builder.cheese;
pepperoni = builder.pepperoni;
bacon = builder.bacon;
}
}
Notez que Pizza est immuable et que les valeurs des paramètres sont toutes au même endroit . Étant donné que les méthodes de définition du générateur renvoient l'objet générateur, elles peuvent être chaînées .
Pizza pizza = new Pizza.Builder(12)
.cheese(true)
.pepperoni(true)
.bacon(true)
.build();
Il en résulte un code facile à écrire et très facile à lire et à comprendre. Dans cet exemple, la méthode de génération peut être modifiée pour vérifier les paramètres après qu'ils ont été copiés du générateur vers l'objet Pizza et lever une IllegalStateException si une valeur de paramètre non valide a été fournie. Ce modèle est flexible et il est facile d'y ajouter plus de paramètres à l'avenir. Il n'est vraiment utile que si vous allez avoir plus de 4 ou 5 paramètres pour un constructeur. Cela dit, cela pourrait être utile en premier lieu si vous pensez que vous ajouterez peut-être plus de paramètres à l'avenir.
J'ai emprunté massivement à ce sujet dans le livre Effective Java, 2nd Edition de Joshua Bloch. Pour en savoir plus sur ce modèle et d'autres pratiques Java efficaces, je le recommande vivement.
new Pizza.Builder(12).cheese().pepperoni().bacon().build();
Pizza.Builder(12).cheese().pepperoni().bacon().build();
vous auriez besoin de recompiler votre code ou d'avoir une logique inutile si vous n'avez besoin que de pepperoni pizzas. À tout le moins, vous devez également fournir des versions paramétrées comme @Kamikaze Mercenary initialement suggéré. Pizza.Builder(12).cheese(true).pepperoni(false).bacon(false).build();
. Là encore, nous ne faisons jamais de tests unitaires, n'est-ce pas?
Considérez un restaurant. La création du «repas d'aujourd'hui» est un modèle d'usine, car vous dites à la cuisine «donnez-moi le repas d'aujourd'hui» et la cuisine (l'usine) décide quel objet générer, en fonction de critères cachés.
Le constructeur apparaît si vous commandez une pizza personnalisée. Dans ce cas, le serveur dit au chef (constructeur) "J'ai besoin d'une pizza; ajoutez-y du fromage, des oignons et du bacon!" Ainsi, le générateur expose les attributs que l'objet généré doit avoir, mais cache comment les définir.
La classe .NET StringBuilder est un excellent exemple de modèle de générateur. Il est principalement utilisé pour créer une chaîne en une série d'étapes. Le résultat final que vous obtenez sur ToString () est toujours une chaîne mais la création de cette chaîne varie en fonction des fonctions de la classe StringBuilder qui ont été utilisées. Pour résumer, l'idée de base est de construire des objets complexes et de masquer les détails d'implémentation de la façon dont il est construit.
b.append(...).append(...)
avant de finalement appeler toString()
. Citation: infoq.com/articles/internal-dsls-java
Pour un problème multi-thread, nous avions besoin d'un objet complexe à construire pour chaque thread. L'objet représentait les données en cours de traitement et pouvait changer en fonction de l'entrée utilisateur.
Pourrions-nous utiliser une usine à la place? Oui
Pourquoi pas nous? Le constructeur a plus de sens, je suppose.
Les usines sont utilisées pour créer différents types d'objets qui sont du même type de base (implémenter la même interface ou classe de base).
Les constructeurs construisent le même type d'objet à plusieurs reprises, mais la construction est dynamique et peut donc être modifiée au moment de l'exécution.
Vous l'utilisez lorsque vous avez beaucoup d'options à gérer. Pensez à des choses comme jmock:
m.expects(once())
.method("testMethod")
.with(eq(1), eq(2))
.returns("someResponse");
Cela semble beaucoup plus naturel et c'est ... possible.
Il y a aussi la construction xml, la construction de chaînes et bien d'autres choses. Imaginez si vous java.util.Map
aviez mis en tant que constructeur. Vous pouvez faire des trucs comme ça:
Map<String, Integer> m = new HashMap<String, Integer>()
.put("a", 1)
.put("b", 2)
.put("c", 3);
En passant par le framework Microsoft MVC, j'ai pensé au modèle de générateur. Je suis tombé sur le modèle dans la classe ControllerBuilder. Cette classe doit renvoyer la classe d'usine du contrôleur, qui est ensuite utilisée pour construire le contrôleur concret.
L'avantage que je vois dans l'utilisation du modèle de générateur est que vous pouvez créer votre propre usine et la connecter au framework.
@Tetha, il peut y avoir un restaurant (Framework) géré par un italien, qui sert de la pizza. Afin de préparer une pizza, un gars italien (Object Builder) utilise Owen (Factory) avec une base de pizza (classe de base).
Maintenant, un Indien prend le relais d'un italien. Restaurant indien (Framework) serveurs dosa au lieu de pizza. Afin de préparer dosa Indian guy (constructeur d'objet) utilise Frying Pan (Factory) avec un Maida (classe de base)
Si vous regardez le scénario, la nourriture est différente, la façon dont la nourriture est préparée est différente, mais dans le même restaurant (dans le même cadre). Le restaurant doit être construit de manière à pouvoir prendre en charge la cuisine chinoise, mexicaine ou toute autre cuisine. Le générateur d'objets à l'intérieur du cadre facilite le plugin du type de cuisine que vous souhaitez. par exemple
class RestaurantObjectBuilder
{
IFactory _factory = new DefaultFoodFactory();
//This can be used when you want to plugin the
public void SetFoodFactory(IFactory customFactory)
{
_factory = customFactory;
}
public IFactory GetFoodFactory()
{
return _factory;
}
}
J'ai toujours détesté le modèle Builder comme quelque chose de lourd, de gênant et très souvent abusé par des programmeurs moins expérimentés. C'est un modèle qui n'a de sens que si vous devez assembler l'objet à partir de certaines données qui nécessitent une étape de post-initialisation (c'est-à-dire une fois que toutes les données sont collectées - faites quelque chose avec). Au lieu de cela, dans 99% du temps, les générateurs sont simplement utilisés pour initialiser les membres de la classe.
Dans de tels cas, il est de loin préférable de simplement déclarer withXyz(...)
les sélecteurs de type à l'intérieur de la classe et de les renvoyer une référence à lui-même.
Considère ceci:
public class Complex {
private String first;
private String second;
private String third;
public String getFirst(){
return first;
}
public void setFirst(String first){
this.first=first;
}
...
public Complex withFirst(String first){
this.first=first;
return this;
}
public Complex withSecond(String second){
this.second=second;
return this;
}
public Complex withThird(String third){
this.third=third;
return this;
}
}
Complex complex = new Complex()
.withFirst("first value")
.withSecond("second value")
.withThird("third value");
Nous avons maintenant une classe unique soignée qui gère sa propre initialisation et fait à peu près le même travail que le constructeur, sauf que c'est beaucoup plus élégant.
Un autre avantage du constructeur est que si vous avez une Factory, il y a encore du couplage dans votre code, car pour que la Factory fonctionne, elle doit connaître tous les objets qu'elle peut éventuellement créer . Si vous ajoutez un autre objet qui pourrait être créé, vous devrez modifier la classe d'usine pour l'inclure. Cela se produit également dans l'usine abstraite.
Avec le constructeur, en revanche, il vous suffit de créer un nouveau constructeur de béton pour cette nouvelle classe. La classe directrice restera la même, car elle reçoit le constructeur dans le constructeur.
En outre, il existe de nombreuses saveurs de constructeur. Kamikaze Mercenary`s en donne un autre.
/// <summary>
/// Builder
/// </summary>
public interface IWebRequestBuilder
{
IWebRequestBuilder BuildHost(string host);
IWebRequestBuilder BuildPort(int port);
IWebRequestBuilder BuildPath(string path);
IWebRequestBuilder BuildQuery(string query);
IWebRequestBuilder BuildScheme(string scheme);
IWebRequestBuilder BuildTimeout(int timeout);
WebRequest Build();
}
/// <summary>
/// ConcreteBuilder #1
/// </summary>
public class HttpWebRequestBuilder : IWebRequestBuilder
{
private string _host;
private string _path = string.Empty;
private string _query = string.Empty;
private string _scheme = "http";
private int _port = 80;
private int _timeout = -1;
public IWebRequestBuilder BuildHost(string host)
{
_host = host;
return this;
}
public IWebRequestBuilder BuildPort(int port)
{
_port = port;
return this;
}
public IWebRequestBuilder BuildPath(string path)
{
_path = path;
return this;
}
public IWebRequestBuilder BuildQuery(string query)
{
_query = query;
return this;
}
public IWebRequestBuilder BuildScheme(string scheme)
{
_scheme = scheme;
return this;
}
public IWebRequestBuilder BuildTimeout(int timeout)
{
_timeout = timeout;
return this;
}
protected virtual void BeforeBuild(HttpWebRequest httpWebRequest) {
}
public WebRequest Build()
{
var uri = _scheme + "://" + _host + ":" + _port + "/" + _path + "?" + _query;
var httpWebRequest = WebRequest.CreateHttp(uri);
httpWebRequest.Timeout = _timeout;
BeforeBuild(httpWebRequest);
return httpWebRequest;
}
}
/// <summary>
/// ConcreteBuilder #2
/// </summary>
public class ProxyHttpWebRequestBuilder : HttpWebRequestBuilder
{
private string _proxy = null;
public ProxyHttpWebRequestBuilder(string proxy)
{
_proxy = proxy;
}
protected override void BeforeBuild(HttpWebRequest httpWebRequest)
{
httpWebRequest.Proxy = new WebProxy(_proxy);
}
}
/// <summary>
/// Director
/// </summary>
public class SearchRequest
{
private IWebRequestBuilder _requestBuilder;
public SearchRequest(IWebRequestBuilder requestBuilder)
{
_requestBuilder = requestBuilder;
}
public WebRequest Construct(string searchQuery)
{
return _requestBuilder
.BuildHost("ajax.googleapis.com")
.BuildPort(80)
.BuildPath("ajax/services/search/web")
.BuildQuery("v=1.0&q=" + HttpUtility.UrlEncode(searchQuery))
.BuildScheme("http")
.BuildTimeout(-1)
.Build();
}
public string GetResults(string searchQuery) {
var request = Construct(searchQuery);
var resp = request.GetResponse();
using (StreamReader stream = new StreamReader(resp.GetResponseStream()))
{
return stream.ReadToEnd();
}
}
}
class Program
{
/// <summary>
/// Inside both requests the same SearchRequest.Construct(string) method is used.
/// But finally different HttpWebRequest objects are built.
/// </summary>
static void Main(string[] args)
{
var request1 = new SearchRequest(new HttpWebRequestBuilder());
var results1 = request1.GetResults("IBM");
Console.WriteLine(results1);
var request2 = new SearchRequest(new ProxyHttpWebRequestBuilder("localhost:80"));
var results2 = request2.GetResults("IBM");
Console.WriteLine(results2);
}
}
S'appuyant sur les réponses précédentes (jeu de mots voulu), un excellent exemple concret est le support intégré de Groovy pour Builders
.
MarkupBuilder
StreamingMarkupBuilder
SwingXBuilder
Voir Builders dans la documentation Groovy
J'ai utilisé Builder dans une bibliothèque de messagerie locale. Le noyau de la bibliothèque recevait des données du fil, les collectait avec l'instance Builder, puis, une fois que Builder avait décidé qu'il avait tout ce dont il avait besoin pour créer une instance de Message, Builder.GetMessage () construisait une instance de message en utilisant les données collectées à partir du câble.
Découvrez InnerBuilder, un plugin IntelliJ IDEA qui ajoute une action «Builder» au menu Générer (Alt + Insert) qui génère une classe de générateur interne comme décrit dans Effective Java
Quand j'ai voulu utiliser le XMLGregorianCalendar standard pour mon XML pour le marshaling d'objet de DateTime en Java, j'ai entendu beaucoup de commentaires sur le poids et la lourdeur de son utilisation. J'essayais de contrôler les champs XML dans les structures xs: datetime pour gérer le fuseau horaire, les millisecondes, etc.
J'ai donc conçu un utilitaire pour construire un calendrier XMLGregorian à partir d'un GregorianCalendar ou java.util.Date.
En raison de l'endroit où je travaille, je ne suis pas autorisé à le partager en ligne sans autorisation légale, mais voici un exemple de la façon dont un client l'utilise. Il résume les détails et filtre certaines des implémentations de XMLGregorianCalendar qui sont moins utilisées pour xs: datetime.
XMLGregorianCalendarBuilder builder = XMLGregorianCalendarBuilder.newInstance(jdkDate);
XMLGregorianCalendar xmlCalendar = builder.excludeMillis().excludeOffset().build();
Étant donné que ce modèle est plus un filtre car il définit les champs du xmlCalendar comme non définis afin qu'ils soient exclus, il le "construit" toujours. J'ai facilement ajouté d'autres options au générateur pour créer une structure xs: date et xs: time et également pour manipuler les décalages de fuseau horaire en cas de besoin.
Si vous avez déjà vu du code qui crée et utilise XMLGregorianCalendar, vous verriez comment cela a rendu la manipulation beaucoup plus facile.
Un excellent exemple dans le monde réel est à utiliser lors des tests unitaires de vos classes. Vous utilisez des générateurs sut (System Under Test).
Exemple:
Classe:
public class CustomAuthenticationService
{
private ICloudService _cloudService;
private IDatabaseService _databaseService;
public CustomAuthenticationService(ICloudService cloudService, IDatabaseService databaseService)
{
_cloudService = cloudService;
_databaseService = databaseService;
}
public bool IsAuthorized(User user)
{
//Implementation Details
return true;
}
Tester:
[Test]
public void Given_a_User_With_Permission_When_Verifying_If_Authorized_Then_Authorize_It_Returning_True()
{
CustomAuthenticationService sut = new CustomAuthenticationServiceBuilder();
User userWithAuthorization = null;
var result = sut.IsAuthorized(userWithAuthorization);
Assert.That(result, Is.True);
}
sut Builder:
public class CustomAuthenticationServiceBuilder
{
private ICloudService _cloudService;
private IDatabaseService _databaseService;
public CustomAuthenticationServiceBuilder()
{
_cloudService = new AwsService();
_databaseService = new SqlServerService();
}
public CustomAuthenticationServiceBuilder WithAzureService(AzureService azureService)
{
_cloudService = azureService;
return this;
}
public CustomAuthenticationServiceBuilder WithOracleService(OracleService oracleService)
{
_databaseService = oracleService;
return this;
}
public CustomAuthenticationService Build()
{
return new CustomAuthenticationService(_cloudService, _databaseService);
}
public static implicit operator CustomAuthenticationService (CustomAuthenticationServiceBuilder builder)
{
return builder.Build();
}
}
CustomAuthenticationService
classe?