Los olores del código (Code Smells en inglés) son la forma que utilizamos para referirnos a signos en el código fuente que podrían indicar un problema más profundo.
Un code smell no tiene por qué implicar que una aplicación no funcione correctamente. Indica un problema de diseño que puede enlentecer el desarrollo, generar más errores en el futuro y hacer aparecer una mayor cantidad de bugs en nuestra aplicación. Dentro de las buenas prácticas de programación, con el objetivo de escribir cada vez mejor código, necesitamos ir aprendiendo todos estos signos.
16. public void Draw(int x, int y, int width, int heigth,
int borderSize,
Color borderColor, Color backgroundColor)
{
// ...
}
17. public void Draw(BorderedRectangle rectangle)
{
// ...
}
public class Rectangle
{
public int X { get; set; }
public int Y { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public Color Color { get; set; }
}
public class BorderedRectangle : Rectangle
{
public int BorderSize { get; set; }
public Color BorderColor { get; set; }
}
18. class Rectangle
{
public void Draw(int x, int y, int width, int height,
int borderSize, Color borderColor, Color backgroundColor)
{
this.DrawRectable(x, y, width, height, backgroundColor);
this.DrawRectangleBorder(x, y, width, height, borderSize,
borderColor);
}
private void DrawRectable(int x, int y, int width, int height,
Color color)
{
// ...
}
private void DrawRectangleBorder(int x, int y, int width,
int height, int borderSize, Color color)
{
// ...
}
19. public class Rectangle
{
public int X { get; set; }
public int Y { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public Color Color { get; set; }
public void Draw() { /* ... */ }
}
public class BorderedRectangle : Rectangle
{
public int BorderSize { get; set; }
public Color BorderColor { get; set; }
public void Draw() { base.Draw(); /* ... */ }
}
20. namespace MyTech
{
class MyTechConnection { }
class MyTechConnectionHndl { }
class Order {
public bool Order(OrderRequest request) { /* ... */ }
}
class Inv
{
int Do(int a, string s, double d) { /* ... */ }
}
}
21. namespace MyTechnology
{
class Connection { }
class ConnectionHandler { }
class OrderManager {
public bool Request(Order order) { /* ... */ }
}
class Invoice
{
bool Pay(int userId, string account, double price) {
/* ... */
}
}
}
24. public class Euro {
public decimal Value { get; set; }
public virtual decimal GetConvertedValue()
{
return this.Value;
}
}
public class USDolar : Euro {
public override decimal GetConvertedValue()
{
return this.Value * USDolarChange;
}
}
public class BritishPounds : Euro {
public override decimal GetConvertedValue()
{
return this.Value * BritishPoundsChange;
}
}
25. public decimal GetDiscount()
{
var basePrice = quantity * itemPrice;
if (quantity > 1000)
return basePrice * 0.95;
else if (quantity > 500)
return 20.0d;
}
26. public decimal GetDiscount()
{
if (quantity > 1000)
return CalculateBasePrice() * 0.95;
else if (quantity > 500)
return 20.0d;
}
private decimal CalculateBasePrice()
{
return quantity * itemPrice;
}
27. abstract class Membership
{
public abstract IEnumerable<User> GetUsers();
public abstract IEnumerable<Profile> GetProfiles();
public abstract User Auth(string user, string password);
public abstract IEnumerable<Profile> GetUserProfile(User user);
}
class MyMembership : Membership
{
public override IEnumerable<User> GetUsers() {
throw new NotImplementedException(); }
public override IEnumerable<Profile> GetProfiles() {
throw new NotImplementedException(); }
public override User Auth(string user, string password) {
// ...
}
public override IEnumerable<Profile> GetUserProfile(User user) {
// ...
}
}
¿Liskov?
28. abstract class Membership
{
public abstract User Auth(string user, string password);
public abstract IEnumerable<Profile> GetUserProfile(User user);
}
class MyMembership : Membership
{
public override User Auth(string user, string password)
{
// ...
}
public override IEnumerable<Profile> GetUserProfile(User user)
{
// ...
}
}
29. class TokiotaService
{
// ...
}
class MicrosoftService
{
// ...
}
public IEnumerable<TokiotaItem> GetTokiotaItems()
{
return this.tokiotaService.GetProyects();
}
public IEnumerable<MicrosoftItem> GetMicrosoftItems()
{
return this.microsoftService.GetPartnerCollaborations();
}
30. class Item { /* ... */ }
class IAdapter
{
IEnumerable<Item> GetItems();
}
class MicrosoftAdapter : IAdapter { /* ... */ }
class TokiotaAdapter : IAdapter { /* ... */ }
public IEnumerable<Item> GetItems(IAdapter adapter)
{
return adapter.GetItems();
}
31. public decimal CalculateTotalPrice() {
if (this.isChristmas) {
return this.CalculateChristmas();
}
else {
if (this.HasDiscount()) {
return this.CalculateDiscounted(this.GetDiscount());
}
else {
if (this.isTimeOfSale) {
return this.CalculateDiscounted(this.GetSalesDiscount());
}
else
{
return this.CalculateStandard();
}
}
}
}
32. public decimal CalculateTotalPrice()
{
if (this.isChristmas)
return this.CalculateChristmas();
if (this.HasDiscount())
return this.CalculateDiscounted(this.GetDiscount());
if (this.isTimeOfSale)
return this.CalculateDiscounted(this.GetSalesDiscount());
return this.CalculateStandard();
}
33.
34. class Customer
{
public string Name { get; set; }
public string LastName { get; set; }
public string toXML()
{
return @"<Customer>" +
"<Name>" +
this.Name +
"</Name>" +
"<LastName>" +
this.LastName +
"</LastName>" +
"</Customer>";
}
}
Open-Close?
35. class Customer
{
private readonly CustomerXmlSerializer serializer = ...;
public string Name { get; set; }
public string LastName { get; set; }
public string toXML()
{
return this.serializer.Serialize(this);
}
}
class CustomerXmlSerializer : XmlSerializer
{
public string Serialize(Customer c)
{
var sb = new StringBuilder();
sb.Append(this.StartTag("Customer"));
sb.Append(this.WriteTag("Name", c.Name));
sb.Append(this.WriteTag("LastName", c.LastName));
sb.Append(this.EndTag("Customer"));
}
36. class OrderManager
{
decimal CalculateOrderPrice(Product[] products, int[] units) {
/* ... */
}
}
class OrderViewModel
{
public Product[] Products { get; set; }
public int[] Units { get; set; }
}
class OrderService
{
void SendOrder(Product[] products, int[] units) { /* ... */ }
}
¿Y si queremos
añadir IVA?
37. class Order
{
public Product[] Products { get; set; }
public int[] Units { get; set; }
public decimal GetPrice() { /* ... */ }
}
class OrderManager
{
decimal CalculateOrderPrice(Order order) {
return order.GetPrice();
}
}
class OrderViewModel
{
public Order Order { get; set; }
}
class OrderService
{
void SendOrder(Order order) { /* ... */ }
}
38. class Vehicle { }
class Car : Vehicle { }
class Truck : Vehicle { }
class Motorbike : Vehicle { }
class VehicleXmlSerializer { }
class CarXmlSerializer : VehicleXmlSerializer { }
class TruckXmlSerializer : VehicleXmlSerializer { }
class MotorbikeXmlSerializer : VehicleXmlSerializer { }
Vehicle
Class
Car
Truck
Motorbike
Xml
Car
Truck
MotorBike
39. class VehicleXmlSerializer
{
public VehicleXmlSerializer(IToXmlStrategy[] strategies) { }
}
interface IToXmlStrategy { }
interface IToXmlStrategy<T> where T : Vehicle { }
class CarToXmlStrategy : IToXmlStrategy<Car> { }
class TruckTOXmlStrategy : IToXmlStrategy<Truck> { }
class MotorbikeToXmlStrategy : IToXmlStrategy<Motorbike> { }
40. class OrderViewModel
{
decimal CalculateTax() { return totalPrice * 0.21; }
}
class Invoice
{
decimal ShowTax() { return order.TotalPrice * 0.21; }
}
class ViewModel
{
public decimal Tax { get { return 21.0; } }
}
41. static class Globals
{
public const decimal TaxPercent = 21.0m;
public const decimal TaxDelta = TaxPercent / 100;
}
class OrderViewModel
{
decimal CalculateTax() { return totalPrice * Globals.TaxDelta; }
}
class Invoice
{
decimal ShowTax() { return order.TotalPrice * Globals.TaxDelta; }
}
class ViewModel
{
public decimal Tax { get { return Globals.TaxPercent; } }
}
42.
43. class LazyClass
{
public static string FormatName(string name,
string lastname, string surname)
{
return string.Format("{1}, {0} ({2})",
name, lastname, surname);
}
}
Solo un método
llamada desde
solo un lugar
44. class CustomerInfo
{
public string Name { get; set; }
public string LastName { get; set; }
public string Surname { get; set; }
public override string ToString()
{
return string.Format("{1}, {0} ({2})",
this.Name,
this.LastName, this.Surname);
}
}
45. class Settings
{
public string Name { get; set; }
public string Host { get; set; }
public bool UseSsl { get; set; }
public int NumerOfConnections { get; set; }
}
class SettingsManager
{
public void Save(Settings settings) { /* ... */ }
public Settings Load() { /* ... */ }
}
Una clase con las propiedades y
otra con los métodos
46. class Settings
{
public string Name { get; set; }
public string Host { get; set; }
public bool UseSsl { get; set; }
public int NumerOfConnections { get; set; }
public void Save() { /* ... */ }
public void Load() { /* ... */ }
}
47. class MyClass
{
public int MyProperty { get; set; }
public string Name { get; set; }
public void Process() { /* ... */ }
public void Send() { /* ... */ }
}
class MyClassOld
{
public string Name { get; set; }
public void Process() { /* ... */ }
}
48. class MyClass
{
public int MyProperty { get; set; }
public string Name { get; set; }
public void Process() { /* ... */ }
public void Send() { /* ... */ }
}
49. class CustomerController {
public ActionResult Edit(Customer customer) {
if (Model.IsValid) {
this.customerRepository.InsertOrUpdate(customer);
this.customerRepository.Save();
return RedirectToAction("Index");
}
return View();
}
}
class EmployeeController {
public ActionResult Edit(Employee employee) {
if (Model.IsValid) {
this.employeeRepository.InsertOrUpdate(employee);
this.employeeRepository.Save();
return RedirectToAction("Index");
}
return View();
}
}
50. class ControllerBase<TEntity> where TEntity : IEntity
{
IRepository<Employee> repository;
public ActionResult Edit(TEntity entity)
{
if (Model.IsValid)
{
this.repository.InsertOrUpdate(entity);
this.repository.Save();
return RedirectToAction("Index");
}
return View();
}
}
class CustomerController : ControllerBase<Customer> { }
class EmployeeController : ControllerBase<Employee> { }
51. public interface ITextFormatStrategy { string Format(string input); }
public class CommentTextFormatContext {
private ITextFormatStrategy strategy;
public CommentTextFormatContext(ITextFormatStrategy strategy) {
this.strategy = strategy;
}
public int ExecuteStrategy(string input) {
return strategy.Format(input);
}
}
public class RemoveHtmlTagsStrategy : ITextFormatStrategy {
public string Format(string input) {
return Regex.Replace(input, @"<[^>]*>", string.Empty);
}
}
¿Solo una estrategia? YAGNI
52. public class Comment
{
public void SetMessage(string message)
{
this.Text = Regex.Replace(message, @"<[^>]*>", string.Empty);
}
}
53. // calculates the total price of the order
public decimal Calculate()
{
// in christmas time
if (DateTime.Now >= new DateTime(DateTime.Now.Year, 12, 25)
&& DateTime.Now <= new DateTime(DateTime.Now.Year + 1, 1, 5))
{
// it has a discount of 10%
return this.TotalPrice * 0.9;
}
else
{
return thisw.TotalPrice;
}
}
56. public class Product
{
public bool HasBeenOrdered(Order order)
{
return order.Products.Contains(this);
}
}
public class Order
{
public List<Product> Products { get; set; }
}
57. public class Product { }
public class Order
{
public List<Product> Products { get; set; }
public bool HasBeenOrdered(Product product)
{
return this.Products.Contains(product);
}
}
58. public class Customer
{
public string Name { get; set; }
public string LastName { get; set; }
public string PhoneNumber { get; set; }
public string Address { get; set; }
}
public class Order
{
private Customer customer;
public string GetSummary()
{
var summary = string.Format("{0} {1}n{2}n{3}",
this.customer.Name,
this.customer.LastName,
this.customer.PhoneNumber,
this.customer.Address);
summary += "n" + this.TotalPrice + " €";
return summary;
}
}
59. public class Customer
{
public string Name { get; set; }
public string LastName { get; set; }
public string PhoneNumber { get; set; }
public string Address { get; set; }
public override string ToString() {
return string.Format("{0} {1}n{2}n{3}",
this.Name,
this.LastName,
this.PhoneNumber,
this.Address);
}
}
public class Order
{
private Customer customer;
public string GetSummary() {
return this.customer.ToString() + "n" + this.TotalPrice + " €";
}
}
61. public decimal CalculateTotalPrice()
{
/* ... */
if (this.Customer.IsEuropean)
{
/* ... */
}
/* ... */
}
public class Customer
{
public bool IsEuropean
{
get
{
return this.Address.IsEuropean;
}
}
}
62. class MyConnection
{
private SqlConnection connection = new SqlConnection();
public string ConnectionString
{
get { return this.connection.ConnectionString; }
}
public void Open()
{
this.connection.Open();
}
public MyCommand CreateCommand()
{
return new MyCommand(this.connection.CreateCommand());
}
}
POO/OOP: es un paradigma de programación (como la programación funcional) que propone la creación de diferentes objetos reutilizables, que se popularizó en los años 90 y sus características son:
Abstracción: implica la creación de objetos que indican un comportamiento. Por ejemplo una interfaz o una clase base.
Encapsulamiento: cuando un objeto contiene todos los métodos y propiedades que comprenden un comportamiento de un tema en concreto, se dice que está encapsulado. Se le pueden añadir niveles de acceso.
Herencia: Los objetos no se encuentran aislados, se pueden comunicar entre ellos y pueden formar una estructura jerárquica.
Cohesión: significa que cuando encapsulemos el comportamiento, este tenga sentido sobre el mismo tema. Es decir que no mezclemos un objeto que sirva para escribir en consola y para realizar un cálculo complejo matemático. La cohesión añade sentido a la encapsulación.
Poliformisfo: son comportamientos diferentes entre objetos distintos que comparten la misma firma: nombre y parámetros.
El término “Technical Debt” (Deuda Técnica) fue introducido en 1992 por Ward Cunningham. Es una metáfora que viene a explicar que la falta de calidad en el código fuente de nuestro proyecto, genera una deuda que repercutirá en sobrecostes, tanto en el mantenimiento de un software, como en la propia operativa funcional de la aplicación. Cuando hablamos de un sobrecoste, puede significar desde tener que dedicarle más tiempo o más desarrolladores de los estimados, hasta acumular malestar general y mal ambiente.
SOLID: Que es el acrónimo de 5 principios.
Single Responsibility: una clase o función solo debe tener una y solo una razón para existir o ser modificada.
Open-Close: el código debe estar abierto a la extensión, pero cerrado a modificaciones.
Liskov Substitution: las clases derivadas, deben poder ser sustituidas por su clase base.
Interface Segregation: Desgranar las interfaces lo más fino posible, para que sean lo más específicas posible.
Dependency Inversion: Hay que depender de las abstracciones no de las concreciones.
En castellano “Code Smells” se traduce como “Olores de código”. Parece ser que fue Kent Beck quien acuñó este concepto a finales de la década de los 90. Pero no se popularizó hasta su aparición en el conocido libro de Martin Fowler: “Refactoring: improving the Design of Existing Code”.
¿Qué son?
Los olores del código, son indicadores que señalan, por lo general, un problema más profundo: violaciones de los principios de diseño, con un impacto negativo en la calidad del desarrollo.
¿Qué es calidad?
Podríamos decir muchas cosas acerca de la calidad: que tenga un buen diseño, que las métricas de código sean de un valor y otro, que r# no nos llame la atención, que… pero la mejor forma de definir la calidad del código es:
“Programa siempre como si el tipo que mantendrá tu código fuera un psicópata violento que sabe donde vives.” - Martin Golding
¿Cómo lo soluciono?
Refactoring: Es el proceso de mejorar la calidad del código fuente sin alterar su funcionalidad.
Pero… no hay una receta mágica, solo guías.
Es fundamental el criterio del desarrollador.
Para aplicar los Refactorings se deben crear los Test necesarios para asegurarnos que no vamos a romper nada, y luego realizar los cambios paso a paso..
Rigidez: Cuando algo es rígido significa que no se puede moldear. Un código rígido es aquel que resulta muy difícil de mantener y extender.
Fragilidad: Si alguna vez te has encontrado con un programa que al cambiar un detalle, empieza a fallar en muchos puntos donde no has tocado nada, ya te has encontrado con código frágil. Un código que solemos tratar con miedo y bordeamos para modificarlo lo menos posible, generando así más código mal oliente.
Inmovilidad: Aunque la solución tiene partes que serían interesantes en otros sistemas, el esfuerzo y riesgo de separarlas del resto del código es tan grande que nunca se reutilizan, se vuelven a escribir.
Viscosidad: Podemos definir como viscoso un entorno de desarrollo que es lento e ineficiente. Pero también es viscoso aquel código que es más difícil de usar tal y como está diseñado. Por lo general preferiremos realizar unos “hacks” que programarlo como estaba pensado.
Complejidad innecesaria: Contiene elementos muy complejos difíciles de identificar y entender su utilidad, artefactos y funciones que no se usan, y código que no es útil en absoluto.
Repeticiones de código: la consecuencia de la maniobra informática más conocida del mundo: copiar y pegar. Crear código repetido a lo largo de toda la aplicación implica que no se ha conseguido identificar esas partes y agruparlas en funciones, clases y contextos comunes.
Opacidad: Decimos que un objeto es opaco cuando no se puede ver a través de él. Es decir, un código que es difícil de entender y leer.
The Bloaters:
Agrupa smells que indican la existencia de algún aspecto que con el tiempo y el crecimiento pueden volver incontrolable el código.The Object Orientation Abusers:
El común denominador de este tipo de smells es que representan casos donde la solución no explota completamente las posibilidades del diseño orientado a objetos.The Change Preventers:
Estos smells dificultan la posibilidad de realizar cambios en nuestro software o de simplemente seguir avanzando en el desarrollo. Violan la regla sugerida por Fowler y Beck, que dice que las clases y los posibles cambios deben tener una relación de uno a uno.
The Dispensables:
Estos Smells tienen en común la existencia de algún elemento innecesario que debería ser eliminado del código fuente.
The Couplers:
Son smells que alertan sobre problemas en el manejo del acoplamiento entre componentes, pudiendo ser este excesivo y o mal diseñado.
Los métodos largos son mas difíciles de leer, de entender y sobre todo de depurar. Mantener los métodos con pocas líneas ayuda a poder evitar este problema. Si ya tenemos métodos largos podemos considerar separarlos en métodos mas pequeños.
Similar a los métodos largos también son difíciles de leer, entender y depurar. Además las clases grandes muestran otros problemas, como por ejemplo que una única clase contiene demasiadas responsabilidades.
No utilizar una set de variables de tipos primitivos como un sustituto simple para una clase. Si el tipo de datos es lo suficientemente complejo, escribir una clase para que lo represente.
Cuantos mas parámetros reciba un método mas complejo se vuelve de entender y utilizar. Para evitar esto podemos mantener la baja cantidad de parámetros que recibe un método o considerar crear una clase o estructura para pasar estos valores.
Si siempre vemos que la misma información viaja todo el tiempo junta, es muy probable que esos datos sueltos pertenezcan a una misma clase. Considerar integrarlos en la clase que corresponda.
Switchs o cualquier implementación oculta de switch (if, else, if…).
Que podrían indicar una falta de utilización de mecanismos de herencia.
El más simple es reemplazar las condiciones usando el polimorfismo. Pero otros refactoring para switch sería los patrones visitor, decorator, strategy, command…
Estar atentos a los objetos que contenga muchos campos opcionales o innecesarios. Si estamos usando un objeto como parámetro de un método asegurarnos que se use todo de él.
Si heredamos de una clase, pero no utilizamos ninguna funcionalidad, ¿para qué vamos a mantener esa herencia? Esto rompe el principio de sustitución de Liskov.
Si dos clases son similares en el interior, pero difieren en el exterior, es probable que se puedan modificar para compartir una interfaz común.
Tener cuidado al detectar o utilizar bloques condicionales muy largos, particularmente los que tienden a crecer con cada cambio que se introduce. Considerar una alternativa orientada a objetos como el uso de un patrón Decorator, Strategy, o State.
Si a través del tiempo, los cambios en una clase necesitan que se toquen muchas partes diferentes de esta, es probable que la clase contenga funcionalidad que no esta relacionada. Considerar aislar las distintas partes en clases diferentes.
Si un cambio en una clase requiere cambios en cascada en muchas otras clases, seguramente nos encontramos en una situación donde una funcionalidad esta mal distribuida.
Cada vez que se realiza una subclase de una clase, se tiene que hacer una sublclase de otra. Considerar unir la jerarquía en una sola clase.
Y también magic strings
Si una clase hace poco y nada, debería eliminarse o combinarse con otra clase.
Evitar clases que solamente almacenen información. Las clases deberían contener información y métodos para operar sobre esta.
Eliminar código que no se utilice. Para eso tenemos sistemas de control de código fuente!
El código duplicado es la peor anti-practica en el desarrollo de software. Eliminar la duplicación siempre que sea posible y estar siempre atento a los casos más sutiles de casi duplicación. DRY!!!
El código que escribimos HOY tiene que ser para resolver los problemas que tenemos HOY. No nos preocupemos por el mañana. YAGNI you aint gonna need it
Hay una línea muy fina entre los comentarios que iluminan y los que oscurecen. Son necesarios? Se puede refactorizar el código para no se necesiten?
Comentar por qué y no el qué
Métodos que realizan un uso extensivo de otra clase, probablemente deberían pertenecer a la otra clase. Deberíamos considerar mover estos métodos a la clase que corresponda.
Estar atentos a las clases que pasan mucho tiempo juntas o que se comunican de modos inapropiados. Las clases deberían conocer lo menos posible unas de otras.
Atentos a las secuencias de llamadas ,posiblemente entre clases, a métodos o variables temporales para obtener datos de rutina. Cualquier intermediario es una dependencia disfrazadas.
Si una clase está delegando todo su trabajo, ¿para qué existe? Elimiar todos los intermediarios. Atencion a las clases que son meramente wrappers sin agregar funcionalidades al framework o contexto.
Cuidado con las clases que exponen innecesariamente su interior. Refactorizar clases para minimizar su interfaz pública. Debe existir una razón de peso para que cada metodo, evento o propiedad que sea pública. Si no existe hay que ocultarlo.