Debido a que otros desarrolladores también necesitan comprender el código que has escrito, debes asegurarte de que puedan entenderlo fácilmente. Para ello, puedes seguir las nueve reglas de la calistenia de objeto en la programación orientada a objetos.
Qué es la calistenia de objeto
La calistenia de objeto son básicamente ejercicios de programación, sustentados en 9 reglas para ayudarnos a escribir mejor código orientado a objetos.
Si ya escribes código mantenible, legible, comprobable y comprensible, estas reglas te ayudarán a escribir código que sea más fácil de mantener, más legible, más comprobable y más comprensible.
A continuación, te describo las 9 reglas de la calistenia de objeto:
1- Utiliza solo un nivel de sangría por método
Si tienes varias condiciones en diferentes niveles, o un bucle en otro bucle o una función que tiene más de una sangría, siempre puedes simplificarla o extraerla.
Esta regla te hará pensar en cómo asegurarte de que cada método haga exactamente una cosa: una estructura de control o un bloque de declaraciones por método.
La estructura condicional aquí son cosas como if o switch case o conditional.
Ejemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
func getStatus(healthPoint: Int) -> String { if healthPoint <= 0 { // una sangría de un nivel está bien return "Dead" } if healthPoint > 0 { if healthPoint < 50 { // sangría de dos niveles. esto violado la regla return "Low Health" } } return "alive" } |
Aquí, puedes simplificar fusionando el segundo if
1 2 3 4 5 6 7 8 9 10 11 |
func getStatus(healthPoint: Int) -> String { if healthPoint <= 0 { // una sangría de un nivel está bien return "Dead" } if healthPoint > 0, healthPoint < 50 { // se convierte en una sangría de un niveles return "Low Health" } return "alive" } |
¿Qué sucede si tienes un if largo porque simplificaste más de un condicional anidado? Puedes extraerla y darle más contexto:
1 2 3 4 |
.... let isLowHealth = healthPoint > 0 && healthPoint < 50 if isLowHealth { ..... |
2- No uses la palabra clave else
En realidad, nunca necesitarás la palabra clave else. Puedes eliminarla fácilmente utilizando la cláusula de return / throw.
Ejemplo:
Una cláusula de protección es un fragmento de código en la parte superior de una función o método que regresa antes de tiempo cuando se cumple alguna condición previa.
Por lo tanto, el código inferior o los bloques fuera del if no serán llamados. Se puede eliminar la condición usando return / throw, por lo que no bajará para ejecutar la parte principal de la función.
1 2 3 4 5 6 7 |
func printSeat(code: String) { if code.hasPrefix("A") || code.hasPrefix("B") { printVIPSeat() } else { printBasicSeat() } } |
Al refactorizar para hacer el retorno anticipado, se convertirá en:
1 2 3 4 5 6 |
func printSeat(code: String) { if code.hasPrefix("A") || code.hasPrefix("B") { printVIPSeat() } printBasicSeat() } |
3- Envuelve todas las primitivas y cadenas
Esta regla se centra en evitar los anti-patrones de código Primitive Obsession. Cada variable que es un tipo de dato primitivo como entero, flotante, cadena, matriz, mapa o literal de objeto ({}), necesitamos encapsularlas a una clase / más clases.
También puedes combinar dos o más variables que tienen un comportamiento relacionado y pueden representar cosas / conceptos del mundo real dentro de una clase.
Ejemplo:
1 2 3 |
let kilometer = 1 let meter = kilometer * 1000 |
El kilómetro y el metro se pueden representar en el mundo real: la distancia. Y puedes crear una clase para representar el valor y dar las propiedades y el comportamiento adecuados.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Distance { let value: Double let unitType: String init(_ value: Double, _ unitType: String) { self.value = value self.unitType = unitType } func toMeter() { // logica para convertir valor en metros } } let kilometer = Distance(1, "kilometer") let meter = kilometer.toMeter() |
Puedes ir más allá envolviendo cosas como la cadena de «kilómetros» a una clase llamada Unidad, y si tienes la necesidad de usar kilogramo, gramo o cualquier otra medida en tu código, también puedes hacer una clase más general llamada Medición en lugar de Distancia.
4- Utiliza colecciones de primera clase
Cada colección debe estar contenida o envuelta en su propia clase. Necesitaras asegurarte de que cada clase que tiene una colección no debería tener ningún otro miembro dentro de la clase.
Si esto sucede, necesitaras convertirlo en una nueva clase y todas las acciones / comportamientos posibles que necesitas en las colecciones (eliminar, filtrar, ordenar, etc.) debes colocarlas como métodos de esta colección de primera clase.
Ejemplo:
Tienes una clase Player.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Player { let name: String var killCount: Int var deathCount: Int init(_ name: String) { self.name = name self.killCount = 0 self.deathCount = 0 } func kill(_ enemy: Player) { enemy.deathCount += 1 killCount += 1 } } |
Y luego tienes la clase Match
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Match { var terrorits = [Player]() var counterTerrorist = [Player]() func add(terrorits: Player) { self.terrorits.append(terrorits) } func add(counterTerrorist: Player) { self.counterTerrorist.append(counterTerrorist) } func refreshMatchData() { _ = terrorits.sort { $0.killCount < $1.killCount } _ = counterTerrorist.sort { $0.killCount < $1.killCount } } } |
Y así puedes usar la clase Player y Match en la clase principal
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
let deathMatch = Match() let johnWich = Player("John Wick") let rambo = Player("Rambo") let puniher = Player("The Puniher") let deadShot = Player("Dead Shot") deathMatch.add(counterTerrorist: johnWich) deathMatch.add(counterTerrorist: rambo) deathMatch.add(terrorist: puniher) deathMatch.add(terrorist: deadShot) rambo.kill(puniher) deathMatch.refreshMatchData() |
En la clase Match tienes un miembro de la colección: terrorists, pero también otro miembro es una colección: counterTerrorists. Ambos tienen el objeto Player.
Necesitaras extraer este Array de Player, a una nueva clase que solo contenga esta Array, lo mejor para este contexto es Team, terrorists team y counterTerrorists team.
Aquí está la clase Match actualizada después de cambiar la colección primitiva a la colección de primera clase Team.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Match { var terrorist = Team() var counterTerrorist = Team() func add(terrorist: Player) { self.terrorist.add(terrorist) } func add(counterTerrorist: Player) { self.counterTerrorist.add(counterTerrorist) } func refreshMatchData() { terrorist.sortByKillCountDecending() counterTerrorist.sortByKillCountDecending() } } |
Aquí está la nueva clase Team que contiene el Array Player, también el comportamiento relacionado del Array que son la adición de un miembro y la clasificación de los miembros en métodos de add y sortByKillCountDescending.
1 2 3 4 5 6 7 8 9 10 11 |
class Team { var player = [Player]() func add(_ player: Player) { self.player.append(player) } func sortByKillCountDecending() { self.player.sorted { $0.killCount < $1.killCount } } } |
5- Utiliza solo un punto por línea
No debes encadenar llamadas a métodos que no tengan el mismo contexto o tipo de retorno.
Ejemplo:
1 2 3 4 |
let rawText = " any raw data that you can use " let username = "Davidsen" let mappedObject: [String] = rawText.trim().uppercased().replace("YOU",username).split(separator: " ").reduce(into: []) { (result, substring) -> () in result.append(String(substring)) } |
La ultima línea es realmente larga, por lo que no se sabrá dónde puede fallar o generar errores, y es por eso que debes dividir las llamadas largas del método en una nueva variable.
1 2 3 4 |
rawText.trim() // retorna string Aceptable rawText.trim().uppercased() // retorna string sigue siendo Aceptable rawText.trim().uppercased().replace("YOU",username) // retorna string sigue siendo Aceptable rawText.trim().uppercased().replace("YOU",username).split(separator: " ") // retorna una array, viola la regla de solo un punto por línea |
Ahora divide el punto que todavía devuelve cadenas a una nueva variable
1 |
let textWithUserName = rawText.trim().uppercased().replace("YOU",username) |
Continúa con la verificación de un punto por línea
1 2 |
let split = textWithUserName.split(separator: " ") // retorna [String.SubSequence]. Aceptable textWithUserName.split(separator: " ").reduce(into: []) { (result, substring) -> () in result.append(String(substring)) } // retorna [String]. viola la regla |
Al dividir nuevamente el punto que aún devuelve el Array a una nueva variable, el código final se convertirá en:
1 2 3 4 5 6 |
let rawText = " any raw data that you can use " let username = "Davidsen" let textWithUserName = rawText.trim().uppercased().replace("YOU",username) let textSubSequence = textWithUserName.split(separator: " ") let mappedString = textSubSequence.reduce(into: []) { (result, substring) -> () in result.append(String(substring)) } |
6- No abreviar
Pon un mejor nombre para la variable / clase, no los abrevie, incluso si la llamada es tan larga como la expresión misma debido al nombre descriptivo.
Ejemplo:
1 2 3 4 5 6 |
var temp = 1 let n = 5 for i in 0...n { temp *= 1 print("\(i) \(temp)") } |
Aquí, podemos cambiar temp, i y n a:
1 2 3 4 5 6 |
var temporary = 1 let count = 5 for index in 0...count { temporary *= 1 print("\(index) \(temporary)") } |
7- Mantén todas las entidades pequeñas
La regla actual es «Ninguna clase de más de 50 líneas y ningún paquete de más de 10 archivos».
Si tienes más de lo que dice la regla puede ser porque tienes muchas líneas de código repetitivo.
Comienza a refactorizar siempre tu código según cada función / clase de responsabilidad (Principio de responsabilidad única)
8- No usar clases con más de dos variables de instancia
Descomponer todas las clases y hacer que cada una de ellas tenga solo dos variables / atributos de instancia o estados.
Para seguir esta regla puedes tomar dos de las variables de instancia que estén relacionadas entre sí y extraerlas con una responsabilidad / comportamiento o tener una representación del mundo real, envuelta en una clase.
Ejemplo:
1 2 3 4 5 6 7 8 9 10 11 |
class Name { let first: String let middle: String let last: String init(first: String, middle: String, last: String) { self.first = first self.middle = middle self.last = last } } |
Al elegir la representación del mundo real en la forma habitual en que los padres dan nombre a los hijos: primer y segundo nombre será un nombre de pila, y el apellido será el nombre de familia heredado. Podemos cambiarlo a esto:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Name { let givenName: GivenName let surname: Surname init(givenName: String, surname: String) { self.givenName = givenName self.surname = surname } } class GivenName { init(... names) { self.names = names } } class Surname { init(... familyNames) { self.familyNames = familyNames } } |
9- No usar getters / setters / properties
Eliminar todos los establecedores y captadores, comenzar a crear funciones que procesen y devuelvan los datos que necesita.
Esta regla es la más controvertida de todas las reglas, porque generalmente ponemos getters / setters cuando aprendemos la programación orientada a objetos básica.
La clave es: Decir, No. Cualquier decisión basada completamente en el estado de un objeto siempre debe tomarse «dentro» del objeto mismo.
Ejemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Wallet { var balance: Int = 0 func setBalance(_ balance: Int) { self.balance = balance } func getBalance() -> Int { return self.balance } } var wallet = Wallet() wallet.setBalance(5000) // valor inicial wallet.setBalance(wallet.getBalance() + 2000); // quiero agregar 2000 al wallet print("Last Balance: \(wallet.getBalance())") |
Para establecer el valor inicial de un estado / propiedades de una clase, puedes ponerlo en constructor.
se puede crear una función en la billetera que sea más sencilla
1 2 3 |
func increase(_ money: Int) { self.balance += money } |
Para verificar si el valor del objeto es el mismo con otro objeto, puede introducir .equals.
Para imprimir el último estado de la billetera (con su saldo), puede introducir una .toString():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Wallet { var balance: Int = 0 func increase(_ money: Int) { self.balance += money } func toString() -> String { return "Last Balance: \(wallet.balance)" } } var wallet = Wallet(balance: 5000) wallet.increase(2000) print(wallet.toString()) |
Conclusión
Si te se sientes cómodo con estas reglas al principio, está bien, puede comenzar a enfocarse en implementarlas una por una, en lugar de probarlas todas directamente.
Seguir las nueve reglas de la calistenia de objeto con disciplina te obligará a encontrar respuestas más difíciles que te lleven a una comprensión mucho más rica de la programación orientada a objetos.
En general, te obliga a pensar en lugar de generar código. Incluso 5 líneas pueden resultar realmente complejas de entender, pero en este caso la legibilidad está asegurada; la facilidad de comprensión depende de quién escribe el código.
¿Qué te pareció las nueve reglas de la calistenia de objeto? Dejame tu comentario y no te olvides de compartirla 😄
I was able to finhd good info from your blog articles. Tish Giusto Verney
I’m glad to hear that 🙂