TL;DR
There are several possible alternatives to reducing the complexity of creating an object. First of all, it is necessary to verify that the class is well designed, respecting the principle of sole responsibility and that of high cohesion. The design patterns Façade, Strategy and State can be very useful in this task, in addition to using class specialization.
Then one can consider the use of dependency injection, Service Locator or another form of inversion of control can be an output. If this is not enough, it is convenient to adopt overloads of builders, Factory Method, Factory or Prototype. If this is not enough, you can go to the most complex design patterns Abstract Factory or Builder.
There is no cake recipe to carry out this process. Each case is a case. In certain cases, some of these processes will be certain, others will not. In other cases, it may be that the one that worked on the first one doesn’t work, but the one that was discarded, this time it’s the best. There are cases where you will have to combine several approaches to have a good result and there are cases where there are several equally satisfactory possible approaches. It is necessary a certain tact and vision of the whole to realize which of these would be the best, and a good dose of experimentation also, besides applying refactoring and changes if the project evolves in a way in which what was good before ceases to be now.
First, if you have a class with a huge amount of attributes, it’s probably violating the principle of single responsibility and has low cohesion.
The principle of sole responsibility says that the class must have one, only one, and not more than one responsibility. It should serve to model exactly one concept in the project.
Cohesion, on the other hand, is a way of assessing how much a class models its responsibility. A class has high cohesion when it has a single well-defined responsibility and models it completely. The higher the cohesion, the better.
An example where the principle of sole responsibility is violated is in that all-purpose class with a lot of methods for a thousand different purposes. It doesn’t even have to be something so blatant, because that class that models Funcionario
, but it has inside it some data of something that would be Empresa
and also has the methods to validate the CPF and the mobile number is also a violation of the principle of sole liability.
Classes that are modeled by failing to decompose properly also fail the principle of single responsibility and high cohesion. For example:
public class Funcionario {
private String nome;
private String cpf;
private LocalDate nascimento;
private String cidade;
private String estado;
private String pais;
private String endereco;
private String numeroEndereco;
private String complemento;
private int dddCelular;
private int numeroCelular;
private int dddTelefoneFixo;
private int numeroTelefoneFixo;
private int ramalTelefoneFixo;
// Um construtor gigante e uma porrada de métodos aqui.
}
Note that it is possible to decompose this class into smaller classes: Telefone
, Endereco
, Cidade
, Estado
, Pais
, something similar can be achieved:
public class Funcionario {
private String nome;
private String cpf;
private LocalDate nascimento;
private Endereco endereco;
private List<Telefone> telefones;
// Um construtor razoável com alguns métodos aqui.
}
This process has a certain similarity with the database normalization process, although it is something quite different. The idea is that when using concepts of composition, aggregation and association, it is possible to extract components related to each other in concepts, increasing cohesion and moving towards a single responsibility. Obviously, this process changes the way the class is instantiated.
However, although having a single responsibility is necessary to achieve high cohesion, it is still not enough. The class must also assume its full responsibility, to avoid these falling into other classes of others.
An example where there is a gap in cohesion is when things like this start to appear:
public class FuncionarioHelper {
public void marcarFerias(Funcionario f) {
// ...
}
public void obterSalario(Funcionario f) {
// ...
}
public void cadastrarDependente(Funcionario f, Dependente d) {
// ...
}
}
These methods there represent business class rules Funcionario
, and therefore should be there. The fact that they are not there means that the class Funcionario
probably has a low cohesion. The ideal would be to do this:
public class Funcionario {
// Tudo que já estava antes.
public void marcarFerias() {
// ...
}
public void obterSalario() {
// ...
}
public void cadastrarDependente(Dependente d) {
// ...
}
}
This also tends to improve the encapsulation, since the class (in this case Funcionario
) will have much less need to share and expose your internal data. This also has direct effect on reducing the coupling, which is a way of assessing how much one class depends on others. The smaller the coupling, the better.
In general, classes that have names containing words such as Utils
, Helper
, Manager
, among others, there are indications that there are problems in class cohesion. In some cases, this is inevitable because it is not possible to add new methods to the desired class (for example, a class StringUtils
often have a lot of methods of things that we would like you to be in class String
, but we can’t put it there because we can’t change it).
In languages such as Ruby and Javascript, where it is possible to add methods to existing classes without modifying them directly (a concept called mix-in), this problem is solved. For example:
String.prototype.reverse = function() {
let r = "";
for (let x = 0; x < this.length; x++) {
r = this.charAt(x) + r;
}
return r;
}
Therefore, by refactoring the class, dividing related logical attributes into classes separately from those where there is a lower relation, we tend to obtain classes that are much simpler to be instantiated and to be reused and have a better encapsulation, requiring fewer parameters in the constructors. How to refactor this sometimes leads to the design pattern Façade.
The design pattern Façade
Sometimes a class represents a very complex set of subsystems. In this case it is appropriate to divide it into these subsystems (or more often, to integrate several complex subsystems into a simpler interface). A class can be used to aggregate all these subcomponents into a larger component so that the resulting class is a facade for all these systems (so the name of this project pattern is Façade). For instantiation, this means that:
Each of these subsystems could be instantiated independently, but by instantiating them (or otherwise obtaining instances) in the scope of Façade, the complexity of their creation is encapsulated.
Each of our subcomponents has its own rules and represents a smaller responsibility within a larger system, and so by separating them, we move towards the principle of single responsibility, high cohesion and also good encapsulation.
An example of a Façade that would be it:
public class Carro {
private Roda frontalEsquerda;
private Roda frontalDireita;
private Roda traseiraEsquerda;
private Roda traseiraDireita;
private Cambio cambio;
private Motor motor;
private Volante volante;
private Porta portaEsquerda;
private Porta portaDireita;
private Tanque tanqueCombustivel;
private Radiador radiador;
// ...
}
Note that in the above case, although the Carro
has several subcomponents, when it is instantiated, it will be the responsibility of the constructor (or some other manufacturing method, as described below) to provide concrete implementations of internal details such as the tanqueCombustivel
or the cambio
.
Also, in the standard Façade, encapsulation is improved as it is expected to have methods like this:
public double getCapacidadeCombustivel() {
return tanque.getCapacidadeCombustivel();
}
public double getNivelCombustivel() {
return tanque.getNivelCombustivel();
}
public void abastecer(TipoCombustivel tipo, double litros) {
if (!motor.isCombustivelAceito(tipo)) {
throw new IllegalArgumentException("Este carro não deve ser abastecido com esse tipo de combustível.");
}
tanque.abastecer(tipo, litros);
}
public List<TipoCombustivel> getTiposCombustivelAceitos() {
return motor.getTiposCombustivelAceitos();
}
And it’s not supposed to have methods like this:
public void setPortaDireita(Porta portaDireita) {
this.portaDireita = portaDireita;
}
public void setNivelCombustivel(double nivel) {
tanque.setNivelCombustivel(nivel);
}
public Motor getMotor() {
return motor;
}
A case of this closer to reality would be that of a class to make an HTTP request to download and/or upload something. Instead of putting it all in one gigantic class RequisicaoHttp
, you would have a class to represent a header, one to represent the HTTP method, one to represent the body of the request, one to represent the body of the response, the status code, the invoked URL, etc. If you want something that already deals with serialization/marshalling and deserialization/unmarshalling of the request and response in objects (rather than strings or strings of raw bytes), will model these behaviors in classes separately as well. This is exactly the case with namespace System.Net.Http
of the C#.
However, it may be that this type of refactoring is not sufficient or not possible. Still, there are alternatives that can be used as listed below.
Class specialization
Let’s assume you have a class Funcionario
which is used to model doctors, teachers, lawyers, accountants, etc., and there are attributes that only make sense in certain cases. In this situation, it is appropriate to create specialized classes for each of these specific cases each with its specific attributes, so that no class will have attributes that are used only in certain cases. That would then make the class Funcionario
in a superclass or an interface.
Sometimes it occurs that a class has a lot of attributes because it models a complex object that can have a number of different behaviors, which in itself is already a violation of the principle of sole responsibility.
The solution in these cases is to move the behaviors to separate classes. For example, instead:
function Peca(tabuleiro, cor, x, y, tipo) {
function verificarMovimentoRei() {
// ...
}
function verificarMovimentoDama() {
// ...
}
function verificarMovimentoBispo() {
// ...
}
this.mover = function() {
if (tipo === "Rei") {
if (verificarMovimentoRei()) // ...
// ...
} else if (tipo === "Dama") {
if (verificarMovimentoDama()) // ...
// ...
} else if (tipo === "Bispo") {
if (verificarMovimentoBispo()) // ...
// ...
} else //...
// ...
}
}
You better do it:
function Rei() {
function verificarMovimento(tabuleiro, cor, x, y) {
// ...
}
}
function Dama() {
function verificarMovimento(tabuleiro, cor, x, y) {
// ...
}
}
function Bispo() {
function verificarMovimento(tabuleiro, cor, x, y) {
// ...
}
}
function Peca(tabuleiro, cor, x, y, tipo) {
this.mover = function() {
if (tipo.verificarMovimento(tabuleiro, cor, x, y) // ...
// ...
}
}
This tends to facilitate the creation of objects because first it improves cohesion and the issue of single responsibility, but also because there are often attributes that only have sense of being used in specific behaviors.
Sometimes an object can change behavior (imagine the pawn that is promoted and turns into another piece). In this case the default is the State, who is the twin brother of Strategy, but when the behavior is changeable.
Note that a class can implement several distinct behaviors, each in its own Strategy or State.
Overload of builders
Remember that in many programming languages (not all), constructors can be overloaded. Sometimes, although the object can even have a large number of attributes and model complex rules, there are only a small number of situations where it is valid to create one of them from scratch and each of these depend on reasonably simple parameter sets, maybe independent of each other. In this case, a possible output would be to have multiple constructors, each working with a different set of parameters. Also remember that one constructor can call another.
However, it is not always possible to model all cases where the object is manufactured by means of multiple constructors, especially considering that, due to the fact that all constructors of a class have the same name in many programming languages, It can happen that there are completely different cases that work with parameters of the same type. Thus, working with multiple manufacturing methods instead of multiple builders can be the output.
It is possible to place the constructor as private or internal and then add static methods to manufacture the instances, each of these covering a specific case. That’s what happens to class java.util.regex.Pattern
, for example.
It is possible that the class has a behavior modeled by an interface (or that you refactor it to achieve this). Then, you can define static factory methods that produce instances in various ways can be made available. Real examples are the class javax.swing.BorderFactory
and the methods of(...)
of the interface java.util.List
(added to Java 9).
The design pattern Prototype
Sometimes the complexity of creating an object is in creating copies of an existing object with slightly different properties. The base object is something simple, but we need several derived objects. A direct implementation by passing a truck parameters on the builder would lead us to something like this:
Personagem modelo = ...;
Personagem novo = new Personagem(
modelo.getClasse(),
modelo.getForca(),
modelo.getInteligencia(),
modelo.getPoder(),
novaVelocidade, // Esse daqui não é copiado do modelo.
modelo.getHP(),
modelo.getMP());
Note that copying the attributes of an object to another being the two of the same class and being this code in a different class is a thing that causes a high coupling and a low cohesion. Thus, it is better for the object to provide a method that returns another object similar to being modified (or even already modified), so that the complexity of creating derived objects is reduced. For example, let’s assume that the class Personagem
have methods like this:
public Personagem comForca(int novaForca) {
return new Personagem(
this.classe, novaForca, this.inteligencia, this.velocidade, this.hp, this.mp);
}
public Personagem comVelocidade(int novaVelocidade) {
return new Personagem(
this.classe, this.forca, this.inteligencia, novaVelocidade, this.hp, this.mp);
}
We can use it like this:
Personagem modelo = ...;
Personagem novo = modelo.comVelocidade(novaVelocidade);
Another possibility would be to do just that:
// Na classe Personagem:
public Personagem clone() {
return new Personagem(/* Aquele montão de parâmetros... */);
}
// No código que usa a classe:
Personagem modelo = ...;
Personagem novo = modelo.clone();
novo.setVelocidade(novaVelocidade);
The second approach is simpler and more flexible, but the first is more robust.
The design pattern Factory
The Factory is an object whose purpose is to create a certain other object. It should be easy to obtain an instance of these objects (via constructor, Factory Method, Singleton or similar thing).
A real example is the class javax.swing.PopupFactory
which contains two different methods to create popups.
The advantage of this approach is that it is possible to Factory before calling the methods of creating instances, which can even be called multiple times with the same instance of Factory.
A special kind of Factory is the one that allows multiple distinct implementations. This is the design standard Abstract Factory, where the Factory is defined by an abstract class or interface and it is possible to create several specialized instances, each building the object in question in its own way. Often, in such cases, the object in question to be produced is also specified by an interface or abstract class.
The design pattern Builder
Already the Builder is to use in circumstances where creation is more complicated, where each method configures an aspect of the object being produced. For example:
ServidorHttp s = new ServidorHttpBuilder()
.porta(1234)
.baseUrl("http://www.example.com")
.staticFileLocation("/public")
.addFilter(new AccessControlFilter())
.addFilter(new LoginFilter())
.addServices(services)
.build();
In this case, each method of the Builder with the exception of the latter (the build()
) can both return own Builder (ie, returns this
, self
, Me
or the equivalent according to the programming language), or else returns a new instance of Builder.
This still has the disadvantage of not ensuring that all methods of Builder that they should be called are indeed called, nor make sure that none of them is called twice, nor ensure that they are called in the right order (there may be cases where this is important). The solution in that case would be to do the ServidorHttpBuilder
have only the method porta
that returns a ServidorHttpBuilder2
which has only the method baseUrl
that returns a ServidorHttpBuilder3
which has only the method staticFileLocation
, etc. This approach ensures that the final method build()
can only be called if all the methods that have to be called have been, that none has been called twice and that they are called in the correct order, otherwise a compilation error occurs. However, usually this approach adds quite complexity and an excessive number of new classes, being feasible in a few cases.
Often the difficulty of instantiating a class, is in providing it with other objects that it needs to work, namely its dependencies.
The idea is to free the class that wants to use the class to be instantiated (client class) from locating all dependencies and placing them in the class to be instantiated. Note that in this case, the previous standards help little, as none of them will free the client class from this job, only make it easier.
Therefore, an approach to be used to provide the appropriate dependencies to an object, freeing the classes that wish to use it from having to know how to find them is necessary. The name of it is Inversion of control.
One way to have inversion of control is to delegate this complexity to a framework. The framework is configured through annotations, XML, JSON, code conventions or anything else in order to know what the dependency injection points of the classes are. These points can be parameters in the constructor, setters or loose attributes that will be populated via Reflection. Thus, the code that wants to get an instance of the class, asks one from the framework and the framework is responsible for locating all the dependencies and injecting them, freeing the code that you want to just use the object from having to worry about it. EJB, CDI and Spring are well-known examples of dependency injection frameworks. For example, in the class to be instantiated, this is:
public class Refeicao {
private Fruta frutaSaborosa;
private Fruta frutaDoce;
public Refeicao (
@Inject @Qualifier("saborosa") Fruta frutaSaborosa,
@Inject @Qualifier("doce") Fruta frutaDoce)
{
this.frutaSaborosa = frutaSaborosa;
this.frutaDoce = frutaDoce;
}
// ...
}
In the framework configuration, this is put:
<bean id="saborosa" class="com.example.frutas.Morango"/>
<bean id="doce" class="com.example.frutas.Abacaxi"/>
So, when the framework is instantiating the class Refeicao
, it will already automatically find that Morango
is the tasty fruit and that Abacaxi
is the sweet fruit.
Dependency injection is the most common form of control inversion, but it is not the only one. Another popular form is the design pattern Service Locator. In this pattern, there is an object (the Service Locator) that is the central responsible for providing implementations of several objects. Thus, the object to be instantiated asks the Service Locator the implementation of each of its dependencies. The Service Locator can be done by name, by interface of which some implementation is desired or by some other criteria.
For example:
public class Refeicao {
private Fruta frutaSaborosa;
private Fruta frutaDoce;
public Refeicao() {
ServiceLocator locator = ServiceLocator.getInstance();
this.frutaSaborosa = (Fruta) locator.find("saborosa");
this.frutaDoce = (Fruta) locator.find("doce");
}
// ...
}
Completion
There are several possible alternatives to reducing the complexity of creating an object. First of all, it is necessary to verify that the class is well designed, respecting the principle of sole responsibility and that of high cohesion. The design patterns Façade, Strategy and State can be very useful in this task, in addition to using class specialization.
Then one can consider the use of dependency injection, Service Locator or another form of inversion of control can be an output. If this is not enough, it is convenient to adopt overloads of builders, Factory Method, Factory or Prototype. If this is not enough, you can go to the most complex design patterns Abstract Factory or Builder.
What are the advantages/disadvantages of applying each of the 5 approaches to a class of the type you refer to and in that scenario? Or, on the contrary, it’s something more generic where each of the 5 approaches apply or can apply to different types/scenarios, such as Victor Stafusa’s response?
– ramaral
@ramaral the first. I find Victor’s answer very good, but still do not know if he answered tangentially the question. Directly did not answer.
– Maniero
I noticed your concern to define a context, however I asked to be sure.
– ramaral
@ramaral you are intending to give a more aligned answer to the question?
– Maniero
I thought I would, but I couldn’t get anything I thought would be enough to post. My little experience in this area (I’m just a curious one), although I devote some time to thinking about these subjects, does not allow me to arrange my ideas consistently and rationally to formulate a response.
– ramaral
Apart from what is obvious to say regarding hypotheses 1 and 2 I would only add that I would opt for a type class Factory to build the object incrementally. Each of the Factory methods would return an interface whose contract allows to use the object in a valid state and receive an object(interface) corresponding to the state immediately prior to what it will build. I do not know if this fits in with point 4.
– ramaral
Of course, the use of interfaces does not guarantee that the object is used in an invalid state (it is always possible to cast for the "total" object). However, doing so (the cast) will be a premeditated action and "whoever" does so must be aware of the consequences.
– ramaral