Vamos con el siguiente patrón de creación, el Builder.
Este es uno de mis patrones preferidos, no encontraréis forma más elegante de construir un objeto complejo, además es un férreo aliado del testing y TDD.
Este patrón nos permite crear un objeto por partes, le vamos proporcionando las distintas partes al Builder y cuando ya lo tiene todo le pedimos que nos entregue el objeto construido, además podemos utilizar el mismo Builder para crear varias instancias del mismo tipo pero con distintos atributos.
Este patrón se podría utilizar en colaboración con el patron de diseño Factory method, de esta forma la factoría podría devolver un Builder de nuestra clase concreta y utilizaríamos el Builder para construir tantos objetos como queramos y con los atributos que necesitemos.
Implementación del patrón Builder
Para el ejemplo vamos a suponer que estamos haciendo un juego de coches donde el usuario puede construir su coche por piezas, puede elegir qué neumáticos utilizar, el chasis, que motor ponerle…
Chasis
Vamos a empezar con el chasis, en nuestro juego tendremos varios tipos de chasis y dos de ellos serán el normal y el explosivo. El normal tendrá una resistencia y con cada golpe se irá dañando, y el explosivo explotará al colisionar (un poco random pero nos vale 🙂)
public abstract class Chassis : MonoBehaviour
{
protected abstract void OnCollisionEnter(Collision other);
}
public class NormalChassis : Chassis
{
protected override void OnCollisionEnter(Collision other)
{
// Se va dañando con cada colisión
}
}
public class ExplosiveChassis : Chassis
{
protected override void OnCollisionEnter(Collision other)
{
// Explota al colisionar
}
}
Neumáticos
También podremos utilizar distintos tipos de neumáticos. Por brevedad no vamos a aplicarles ninguna lógica pero además de tener un componente visual distinto, podrían afectar a la conducción.
public abstract class Tyre : MonoBehaviour {}
public class WetTyre : Tyre {}
public class SoftTyre : Tyre {}
public enum TyrePositions
{
FrontLeft,
FrontRight,
RearLeft,
RearRight
}
Vehículo
También tendremos el vehículo que contendrá todos estos componentes.
public class Vehicle : MonoBehaviour
{
private Dictionary<TyrePositions, Tyre> _tyres;
private Chassis _chassis;
public void SetComponents(Dictionary<TyrePositions, Tyre> tyres, Chassis chassis)
{
_tyres = tyres;
_chassis = chassis;
}
}
Como podéis ver, el vehículo no sabe nada sobre cómo se crean sus componentes, ni si son hijos del vehículo o no, esto es perfecto, estamos respetando el principio de una sola responsabilidad.
Clase Builder
Vamos a ver ahora cómo construir un vehículo, la clase Builder.
public class VehicleBuilder
{
private Chassis _chassis;
private readonly Dictionary<TyrePositions, Tyre> _tyres = new Dictionary<TyrePositions, Tyre>();
private Vehicle _vehicle;
private Vector3 _position;
private Quaternion _rotation;
public VehicleBuilder()
{
// Default values
_rotation = Quaternion.identity;
_position = Vector3.zero;
}
public VehicleBuilder WithPosition(Vector3 position)
{
_position = position;
return this;
}
public VehicleBuilder WithRotation(Quaternion rotation)
{
_rotation = rotation;
return this;
}
public VehicleBuilder WithChassis(Chassis chassis)
{
_chassis = chassis;
return this;
}
public VehicleBuilder WithTyre(Tyre tyre, TyrePositions position)
{
_tyres.Add(position, tyre);
return this;
}
public VehicleBuilder FromVehiclePrefab(Vehicle vehicle)
{
_vehicle = vehicle;
return this;
}
public Vehicle Build()
{
CheckPreConditions();
var vehicle = Object.Instantiate(_vehicle, _position, _rotation);
var chassis = Object.Instantiate(_chassis, vehicle.transform);
var tyres = new Dictionary<TyrePositions, Tyre>(4);
foreach (var tyre in _tyres)
{
var tyreInstance = Object.Instantiate(tyre.Value);
var tyrePositions = tyre.Key;
tyres.Add(tyrePositions, tyreInstance);
}
vehicle.SetComponents(tyres, chassis);
return vehicle;
}
private void CheckPreConditions()
{
Assert.IsNotNull(_vehicle);
Assert.IsNotNull(_chassis);
Assert.AreEqual(4, _tyres.Count);
Assert.IsNotNull(_tyres[TyrePositions.FrontLeft]);
Assert.IsNotNull(_tyres[TyrePositions.FrontRight]);
Assert.IsNotNull(_tyres[TyrePositions.RearLeft]);
Assert.IsNotNull(_tyres[TyrePositions.RearRight]);
}
}
La clase builder se caracteriza por tener métodos With, From y otros tipos para ir almacenando las variables con las que crearemos el objeto.
Los métodos donde asignamos las variables devuelven un VehicleBuilder (this) para permitirnos hacer este tipo de cosas:
var vehicle = new VehicleBuilder()
.WithTyre(xxx, yyy)
.FromVehicle(vvv)
.Build();
¿Veis que verbose queda? Facilita muchísimo la lectura.
Después tenemos un constructor donde asignamos los valores por defecto y un método Build donde hacemos los checks pertinentes y construimos el objeto.
Este patrón de diseño nos permite construir distintos objetos utilizando las mismas variables pero cambiando solo las que nos interesen. En este ejemplo vamos a construir dos vehículos iguales pero el segundo estará en una posición distinta.
var builder = new VehicleBuilder();
var vehicle1 = builder
.WithTyre(xxx, yyy)
.FromVehicle(vvv).
.WithPosition(new Vector(1, 1, 1))
.Build();
var vehicle1 = builder
.WithPosition(new Vector(2, 2, 2))
.Build();
Como el Builder se encarga de instanciar los prefabs, no tenemos que preocuparnos de que estemos asignando las mismas referencias a todos los vehículos.
Consumidor
Por ultimo vamos a ver como podríamos consumir este Builder por partes, asignando las distintas piezas en momentos distintos y construyendo el vehículo al final.
public class Consumer : MonoBehaviour
{
[SerializeField] private Tyre[] _possibleTyres;
[SerializeField] private Chassis[] _possibleChassis;
[SerializeField] private Vehicle[] _possibleBaseVehicle;
private readonly VehicleBuilder _vehicleBuilder = new VehicleBuilder();
public void SelectTyre(int tyreIndex, TyrePositions tyrePosition)
{
Assert.IsTrue(tyreIndex < _possibleTyres.Length, "Invalid tyre index");
_vehicleBuilder.WithTyre(_possibleTyres[tyreIndex], tyrePosition);
}
public void SelectChassis(int chassisIndex)
{
Assert.IsTrue(chassisIndex < _possibleChassis.Length, "Invalid chassis index");
_vehicleBuilder.WithChassis(_possibleChassis[chassisIndex]);
}
public void CreateVehicle()
{
_vehicleBuilder.Build();
}
}
Esta clase podría ser un menú o lo que nos interesara.
Conclusión
Hemos visto cómo implementar el patrón de creación Builder, la forma más elegante de crear un objeto complejo sin morir en el intento. Estoy casi seguro que de una forma u otra ya habíais aplicado este patrón de forma intuitiva, bueno, ahora ya sabéis su nombre y cómo explotarlo.
Como siempre, AQUÍ tenéis el proyecto de ejemplo que hemos utilizado para este post. Si os ha quedado cualquier duda, no dudéis en comentar esta entrada y os intentaremos resolver vuestras dudas.
Fuentes
- El libro de GANG OF FOUR, patrones de diseño (Erich Gamma)
- Game Programming Patterns (Robert Nystrom – Electronic Arts)
- Agile Principles, Patterns, and Practices in C# (Robert C. Martin)
Otros patrones de diseño
- Patrones de diseño – Template Method
- Patrones de diseño – Service Locator
- Patrones de diseño – Composite
- Patrones de diseño – Abstract Factory
- Patrones de diseño – Object Pool
- Patrones de diseño – Singleton y Monostate