sábado, 4 de agosto de 2012

Implementando SOLID – LSP

Siguiendo con los principios SOLID, y el empuje de Nelo p/compartir como resolver cada uno de los ejercicios, vamos a continuar con el principio de liskov.

Esta es la solución de Nelo: http://nelopauselli.blogspot.com.ar/2012/08/implementando-solid-lsp.html

El código del ejercicio es este:
[TestFixture]
class MailBuilderTest
{
[Test]
public void TestContactInformation()
{
// arrange
ContactInformation contactInformation = new ContactInformation()
{
FirstName = "Homero",
LastName = "Simpson"
};
IMailMessageBuilder<ContactInformation> mailBuilder = new ContactInformationMailMessageBuilder();
// act
MailMessage message = mailBuilder
.WithFrom("cliente@gmail.com")
.WithTo("empresa@gmail.com")
.WithSubject("Hola")
.WithEntity(contactInformation).BuildMessage();
// assert
Assert.That(message.From.Address, Is.EqualTo("cliente@gmail.com"));
Assert.That(message.To, Has.All.Matches<MailAddress>(x => x.Address == "empresa@gmail.com"));
Assert.That(message.Subject, Is.EqualTo("Hola"));
Assert.That(message.Body, Is.StringContaining("Nombre: Homero"));
Assert.That(message.Body, Is.StringContaining("Apellido: Simpson"));
}
[Test]
public void TestContactInformationSubsidiary()
{
// arrange
ContactInformationSubsidiary contactInformation = new ContactInformationSubsidiary()
{
FirstName = "Homero",
LastName = "Simpson",
Subsidiary = "Retiro"
};
IMailMessageBuilder<ContactInformation> mailBuilder = new ContactInformationMailMessageBuilder();
// act
MailMessage message = mailBuilder
.WithFrom("cliente@gmail.com")
.WithTo("empresa@gmail.com")
.WithSubject("Hola")
.WithEntity(contactInformation).BuildMessage();
// assert
Assert.That(message.From.Address, Is.EqualTo("cliente@gmail.com"));
Assert.That(message.To, Has.All.Matches<MailAddress>(x => x.Address == "empresa@gmail.com"));
Assert.That(message.Subject, Is.EqualTo("Hola"));
Assert.That(message.Body, Is.StringContaining("Nombre: Homero"));
Assert.That(message.Body, Is.StringContaining("Apellido: Simpson"));
Assert.That(message.Body, Is.StringContaining("Sucursal: Retiro"));
}
[Test]
public void TestContactInformationAuctionSaleArtWork()
{
// arrange
ContactInformation contactInformation = new ContactInformationAuction()
{
FirstName = "Homero",
LastName = "Simpson",
Author = "Picasso",
Dimensions = "3x3"
};
IMailMessageBuilder<ContactInformation> mailBuilder = new ContactInformationMailMessageBuilder();
// act
MailMessage message = mailBuilder
.WithFrom("cliente@gmail.com")
.WithTo("empresa@gmail.com")
.WithSubject("Hola")
.WithEntity(contactInformation).BuildMessage();
// assert
Assert.That(message.From.Address, Is.EqualTo("cliente@gmail.com"));
Assert.That(message.To, Has.All.Matches<MailAddress>(x => x.Address == "empresa@gmail.com"));
Assert.That(message.Subject, Is.EqualTo("Hola"));
Assert.That(message.Body, Is.StringContaining("Nombre: Homero"));
Assert.That(message.Body, Is.StringContaining("Apellido: Simpson"));
Assert.That(message.Body, Is.StringContaining("Autor: Picasso"));
Assert.That(message.Body, Is.StringContaining("Dimensiones: 3x3"));
}
}
public IMailMessageBuilder<ContactInformation> WithEntity(ContactInformation contactInformation)
{
ParseContactInformation(contactInformation);
if (contactInformation is ContactInformationSubsidiary)
{
ContactInformationSubsidiary contact = contactInformation as ContactInformationSubsidiary;
ParseContactInformationSubsidiary(contact);
}
else if (contactInformation is ContactInformationAuction)
{
ContactInformationAuction contact = contactInformation as ContactInformationAuction;
ParseContactInformationAuction(contact);
}
return this;
}

El problema es que el método WithEntity hace un tratamiento especial para cada una de las subclases. El principio de Liskov nos dice que si dependemos de una clase base, debemos poder usar objetos de clases derivadas sin saberlo. Y este no es el caso. Los problemas que presenta no seguir este principio son:

  • Quien usa el método WIthEnity puede pensar, con todo derecho, que pasándole cualquier objeto derivado de ContactInformation va a funcionar.
  • Quien agrega otro subtipo de ContactInformation, puede olvidarse que existe este método WithEntity con tratamiento especial y usarlo.
  • El ejemplo está reducido y simplificado p/poder ser entendido fácilmente. En realidad se trata de un caso real, con muchos subtipos y una jerarquía algo más compleja. Modificar esta MailBuilder era realmente un problema, ya que implicaba volver a probar todos los casos.
Este código tiene una ventaja que favorece el reuso vs la duplicación, pero una desventaja, que es poco flexible. A veces la duplicación no es mala, ya que nos da flexibilidad. La solución propuesta es tener un MailBuilder por cada clase. Hay duplicación, pero también flexibilidad. Podemos cambiar lo que querramos, sin afectar lo existente.
class ContactInformationAuctionMessageBuilder : IMailMessageBuilder<ContactInformationAuction>
{
private MailMessage mailMessage;
private StringBuilder body = new StringBuilder();
public ContactInformationAuctionMessageBuilder()
{
mailMessage = new MailMessage();
}
public IMailMessageBuilder<ContactInformationAuction> WithTo(string to)
{
mailMessage.To.Add(to);
return this;
}
public IMailMessageBuilder<ContactInformationAuction> WithSubject(string subject)
{
mailMessage.Subject = subject;
return this;
}
public IMailMessageBuilder<ContactInformationAuction> WithFrom(string from)
{
mailMessage.From = new MailAddress(from);
return this;
}
public MailMessage BuildMessage()
{
mailMessage.Body = body.ToString();
return mailMessage;
}
public IMailMessageBuilder<ContactInformationAuction> WithEntity(ContactInformationAuction contact)
{
AddBodyLine("Nombre: {0}", contact.FirstName);
AddBodyLine("Apellido: {0}", contact.LastName);
AddBodyLine("Autor: {0}", contact.Author);
AddBodyLine("Dimensiones: {0}", contact.Dimensions);
return this;
}
private void AddBodyLine(String line, params object[] args)
{
body.AppendLine(String.Format(line, args));
}
}
public class ContactInformationMessageBuilder : IMailMessageBuilder<ContactInformation>
{
private MailMessage mailMessage;
private StringBuilder body = new StringBuilder();
public ContactInformationMessageBuilder()
{
mailMessage = new MailMessage();
}
public IMailMessageBuilder<ContactInformation> WithTo(string to)
{
mailMessage.To.Add(to);
return this;
}
public IMailMessageBuilder<ContactInformation> WithSubject(string subject)
{
mailMessage.Subject = subject;
return this;
}
public IMailMessageBuilder<ContactInformation> WithFrom(string from)
{
mailMessage.From = new MailAddress(from);
return this;
}
public MailMessage BuildMessage()
{
mailMessage.Body = body.ToString();
return mailMessage;
}
public IMailMessageBuilder<ContactInformation> WithEntity(ContactInformation contact)
{
AddBodyLine("Nombre: {0}", contact.FirstName);
AddBodyLine("Apellido: {0}", contact.LastName);
return this;
}
private void AddBodyLine(String line, params object[] args)
{
body.AppendLine(String.Format(line, args));
}
}
class ContactInformationSubsidiaryMessageBuilder : IMailMessageBuilder<ContactInformationSubsidiary>
{
private MailMessage mailMessage;
private StringBuilder body = new StringBuilder();
public ContactInformationSubsidiaryMessageBuilder()
{
mailMessage = new MailMessage();
}
public IMailMessageBuilder<ContactInformationSubsidiary> WithTo(string to)
{
mailMessage.To.Add(to);
return this;
}
public IMailMessageBuilder<ContactInformationSubsidiary> WithSubject(string subject)
{
mailMessage.Subject = subject;
return this;
}
public IMailMessageBuilder<ContactInformationSubsidiary> WithFrom(string from)
{
mailMessage.From = new MailAddress(from);
return this;
}
public MailMessage BuildMessage()
{
mailMessage.Body = body.ToString();
return mailMessage;
}
public IMailMessageBuilder<ContactInformationSubsidiary> WithEntity(ContactInformationSubsidiary contact)
{
AddBodyLine("Nombre: {0}", contact.FirstName);
AddBodyLine("Apellido: {0}", contact.LastName);
AddBodyLine("Sucursal: {0}", contact.Subsidiary);
return this;
}
private void AddBodyLine(String line, params object[] args)
{
body.AppendLine(String.Format(line, args));
}
}
[TestFixture]
class MailBuilderTest
{
[Test]
public void TestContactInformation()
{
// arrange
ContactInformation contactInformation = new ContactInformation()
{
FirstName = "Homero",
LastName = "Simpson"
};
IMailMessageBuilder<ContactInformation> mailBuilder = new ContactInformationMessageBuilder();
// act
MailMessage message = mailBuilder
.WithFrom("cliente@gmail.com")
.WithTo("empresa@gmail.com")
.WithSubject("Hola")
.WithEntity(contactInformation).BuildMessage();
// assert
Assert.That(message.From.Address, Is.EqualTo("cliente@gmail.com"));
Assert.That(message.To, Has.All.Matches<MailAddress>(x => x.Address == "empresa@gmail.com"));
Assert.That(message.Subject, Is.EqualTo("Hola"));
Assert.That(message.Body, Is.StringContaining("Nombre: Homero"));
Assert.That(message.Body, Is.StringContaining("Apellido: Simpson"));
}
[Test]
public void TestContactInformationSubsidiary()
{
// arrange
ContactInformationSubsidiary contactInformation = new ContactInformationSubsidiary()
{
FirstName = "Homero",
LastName = "Simpson",
Subsidiary = "Retiro"
};
IMailMessageBuilder<ContactInformationSubsidiary> mailBuilder = new ContactInformationSubsidiaryMessageBuilder();
// act
MailMessage message = mailBuilder
.WithFrom("cliente@gmail.com")
.WithTo("empresa@gmail.com")
.WithSubject("Hola")
.WithEntity(contactInformation).BuildMessage();
// assert
Assert.That(message.From.Address, Is.EqualTo("cliente@gmail.com"));
Assert.That(message.To, Has.All.Matches<MailAddress>(x => x.Address == "empresa@gmail.com"));
Assert.That(message.Subject, Is.EqualTo("Hola"));
Assert.That(message.Body, Is.StringContaining("Nombre: Homero"));
Assert.That(message.Body, Is.StringContaining("Apellido: Simpson"));
Assert.That(message.Body, Is.StringContaining("Sucursal: Retiro"));
}
[Test]
public void TestContactInformationAuction()
{
// arrange
ContactInformationAuction contactInformation = new ContactInformationAuction()
{
FirstName = "Homero",
LastName = "Simpson",
Author = "Picasso",
Dimensions = "3x3"
};
IMailMessageBuilder<ContactInformationAuction> mailBuilder = new ContactInformationAuctionMessageBuilder();
// act
MailMessage message = mailBuilder
.WithFrom("cliente@gmail.com")
.WithTo("empresa@gmail.com")
.WithSubject("Hola")
.WithEntity(contactInformation).BuildMessage();
// assert
Assert.That(message.From.Address, Is.EqualTo("cliente@gmail.com"));
Assert.That(message.To, Has.All.Matches<MailAddress>(x => x.Address == "empresa@gmail.com"));
Assert.That(message.Subject, Is.EqualTo("Hola"));
Assert.That(message.Body, Is.StringContaining("Nombre: Homero"));
Assert.That(message.Body, Is.StringContaining("Apellido: Simpson"));
Assert.That(message.Body, Is.StringContaining("Autor: Picasso"));
Assert.That(message.Body, Is.StringContaining("Dimensiones: 3x3"));
}
}

Seguramente hay otras formas de eliminar la duplicación. Podemos por ejemplo crear un MailBuilderString con la responsabilidad de armar un mail y usar este objeto dentro de cada uno de los MailBuilders para cada ContactInformation.

saludos!