¿Cuál es el principio más básico a seguir para asignar responsabilidades en un diseño orientado a objetos?
Asignar la responsabilidad al experto en información, es decir, a la clase que tiene la información necesaria para poder cumplir con la responsabilidad.
Vamos a trabajar en una aplicación de punto de venta1, donde tenemos clases para representar un ticket de venta. Para este ejemplo utilizamos un ticket2 simplificado, que luce más o menos así:
Fecha: 31/3/2021
2 de 'Agua mineral' a $25
1 de 'Café cortado' a $35
1 de 'Café expreso' a $31En este ejemplo, compramos el 31 de marzo del 2021 dos aguas minerales a $25 cada una, un café cortado a $35, y un café expreso a $31.
⚠️ ImportanteComenzar asignando las responsabilidades enunciándolas claramente primero.
Para mostrar las clases y sus responsabilidades vamos a utilizar "tarjetas" con tres secciones:
|
|
|
En la sección de arriba va el nombre de la clase, en la sección de abajo a la izquierda la lista de responsabilidades de hacer y conocer de esa clase, y en la sección de abajo a la derecha la lista de clases que colabora con ésta para cumplir esas responsabilidades.
Estas "tarjetas" se llaman CRC por "clases", "responsabilidades" y "colaboraciones".
En este ejemplo tenemos como punto de partida las tarjetas CRC para clases que ya existen en la aplicación de punto de venta. La clase SaleTicket representa el ticket de venta; esta clase tiene la responsabilidad de conocer la fecha y las líneas de los ítems vendidos; también tiene la responsabilidad de armar el texto para imprimir el ticket. La clase TicketLineItem representa las líneas de los ítems vendidos y colabora con la clase SaleTicket:
|
Conocer fecha y hora Conocer una o más líneas de ítems vendidos Imprimir el ticket |
TicketLineItem |
La responsabilidad de la clase
SaleTicketde imprimir el ticket la analizaremos en otro artículo sobre el principio de responsabilidad única o SRP por sus siglas en inglés.
La clase TicketLineItem representa la línea del ticket con la cantidad y el producto vendido en esa línea. La clase ProductSpecification representa los productos y colabora con la clase TicketLineItem:
|
Conocer la cantidad del producto Conocer el producto |
ProductSpecification |
Por ejemplo, en el ticket de arriba, 2 de 'Agua mineral' a $25 es una línea del ticket.
La clase ProductSpefication representa los productos con su precio y no necesita colaborar con ninguna clase:
|
Conocer la descripción Conocer el precio |
|
Por ejemplo, en el ticket de arriba, Agua mineral es un producto que cuesta $25.
Vean estas mismas clases programadas en C#:
public class SaleTicket
{
private ArrayList lineItems = new ArrayList();
public DateTime DateTime { get; set; }
public void AddLineItem(TicketLineItem item)
{
this.lineItems.Add(item);
}
public void RemoveLineItem(TicketLineItem item)
{
this.lineItems.Remove(item);
}
public void PrintTicket()
{
Console.WriteLine($"Fecha: {this.DateTime}");
foreach (TicketLineItem item in this.lineItems)
{
Console.WriteLine($"{item.Quantity} de '{item.Product.Description}' a ${item.Product.Price}");
}
}
}public class TicketLineItem
{
public TicketLineItem(double quantity, ProductSpecification product)
{
this.Quantity = quantity;
this.Product = product;
}
public double Quantity { get; set; }
public ProductSpecification Product { get; set; }
}public class ProductSpecification
{
public ProductSpecification(string description, double price)
{
this.Description = description;
this.Price = price;
}
public string Description { get; set; }
public double Price { get; set; }
}Un ejemplo de enviar el mensaje con selector PrintTicket() a una instancia de SaleTicket con dos aguas minerales a $25.00, un café cortado a $35.00 y un café expreso a $31.00, sería el que ya hemos visto:
Fecha: 31/3/2021
2 de 'Agua mineral' a $25
1 de 'Café cortado' a $35
1 de 'Café expreso' a $31Ahora bien, en este ejemplo, si quisiéramos agregar al ticket el total de la venta, ¿quién debe tener la responsabilidad de conocer ese total?
Por la guía Expert, deberíamos mirar qué clases tienen la información necesaria para determinar el total. El total de una venta se calcula sumando el subtotal de las líneas del ticket; y a su vez, el subtotal de cada línea se calcula como el producto de la cantidad vendida en esa línea, multiplicada por el precio del producto vendido en esa línea.
¿Qué se necesita para determinar el total de la venta? Es necesario conocer todas las instancias de TicketLineItem de un ticket y la suma de los subtotales de cada línea. Sólo las instancias de SaleTicket tienen la responsabilidad de conocer esta información; por lo tanto, por Expert, SaleTicket es la clase correcta para asumir la responsabilidad de determinar el total; es el experto de información.
Modificamos la tarjeta CRC de la clase SaleTicket para que quede así, los cambios en negrita:
|
Conocer fecha y hora Conocer una o más líneas de ítems vendidos Imprimir el ticket Calcular el total |
SalesLineItem |
En C# agregamos una nueva propiedad Total a la clase SaleTicket y agregamos el total de la venta en el método GetTicketText() como aparece a continuación; solo mostramos el código nuevo, los puntos … representan el código que ya apareció antes.
public class SaleTicket
{
…
+ public double Total
+ {
+ get
+ {
+ double result = 0;
+ foreach (TicketLineItem item in this.lineItems)
+ {
+ result = result + (item.Quantity * item.Product.Price);
+ }
++ return result;
+ }
+ }
…
public void PrintTicket()
{
Console.WriteLine($"Fecha: {this.DateTime}");
foreach (TicketLineItem item in this.lineItems)
{
Console.WriteLine($"{item.Quantity} de '{item.Product.Description}' a ${item.Product.Price}");
}
+ Console.WriteLine($"Total: ${this.Total}");
}
}Te puede llamar la atención que implementemos la resposabilidad de conocer el total de la venta como una propiedad de sólo lectura
Totaly no como un método con firmadouble GetTotal()o algo así. Tratamos de representar las responsabilidad de conocer como propiedades en C#. En este casoTotales una propiead de sólo lectura -sólo tiene implementado sugety no suset- porque el total de la venta cambia sólo cuando se agregan o quitan líneas, o cambian las cantidades de las líneas.
Ahora el resultado de enviar el mensaje con selector PrintTicket() a la misma instancia de SaleTicket del ejemplo anterior sería:
Fecha: 31/3/2021
2 de 'Agua mineral' a $25
1 de 'Café cortado' a $35
1 de 'Café expreso' a $31
Total: $116Aún podemos hacer más. ¿Qué información se necesita para calcular el subtotal de una línea? La cantidad vendida y el precio del producto en esa línea.
La clase TicketLineItem tiene la responsabilidad de conocer la cantidad vendida en la línea y el producto vendido; el producto es una instancia de ProductSpecification, que a su vez tiene la responsabilidad de conocer el precio. Por lo tanto, asignamos la responsabilidad de conocer el subtotal de una línea del ticket a la clase TicketLineItem. La tarjeta CRC queda así, la modificación en negrita:
|
Conocer la cantidad de cada producto Conocer un producto Conocer el subtotal |
ProductSpecification |
El código en C# queda así, las modificaciones marcadas en verde:
public class TicketLineItem
{
…
+ public double SubTotal
+ {
+ get
+ {
+ return this.Quantity * this.Product.Price;
+ }
+ }
}public class Sale
{
…
public double Total
{
get
{
double result = 0;
foreach (TicketLineItem item in this.lineItems)
{
- result = result + (item.Quantity * item.Product.Price);
+ result = result + item.SubTotal;
}
return result;
}
}
…
}La guía Expert es usada más que ninguna otra guía en la asignación de responsabilidades; es un principio guía básico usado continuamente en el diseño orientado a objetos. Expert expresa la intuición de sentido común de que los objetos hacen cosas relacionadas con la información que tienen.
Noten que para cumplir una responsabilidad a menudo es necesario información que está desperdigada a través de diferentes clases de objetos. Esto implica que hay “expertos parciales” que colaboran para cumplir con la responsabilidad.
La encapsulación se mantiene, porque los objetos usan su propia información para cumplir con las responsabilidades. Esto mantiene el acoplamiento3 bajo, lo que produce programas más robustos y fáciles de mantener. El comportamiento se distribuye a través de clase que tienen la información requerida, promoviendo definiciones de clases más cohesivas4 que son más fáciles de entender y de mantener.
1 La que usan las tiendas y supermercados para hacer las facturas de las ventas.
2 El papelito que te entregan cuando comprás algo.
3 Veremos acoplamiento más adelante; por ahora vean este ejemplo.
4 Veremos también cohesión más adelante; por ahora vean este ejemplo
