solid - Aprendizaje del principio de responsabilidad única con C#
solid clean code (3)
Comencemos con lo que realmente significa el Principio de Responsabilidad Único (SRP):
Una clase debe tener una sola razón para cambiar.
Esto significa efectivamente que cada objeto (clase) debe tener una responsabilidad única; si una clase tiene más de una responsabilidad, estas responsabilidades se juntan y no pueden ejecutarse de manera independiente, es decir, los cambios en uno pueden afectar o incluso romper el otro en una implementación particular.
Lo que se debe leer definitivamente es la fuente en sí (capítulo pdf de "Desarrollo de software ágil, principios, patrones y prácticas" ): el principio de responsabilidad única
Habiendo dicho eso, debes diseñar tus clases para que, de manera ideal, solo hagan una cosa y hagan una cosa bien.
Primero piense en las "entidades" que tiene, en su ejemplo puedo ver User
y Channel
y el medio entre ellas a través del cual se comunican ("mensajes"). Estas entidades tienen ciertas relaciones entre sí:
- Un usuario tiene varios canales a los que se ha unido.
- Un canal tiene una cantidad de usuarios.
Esto también lleva naturalmente a hacer la siguiente lista de funcionalidades:
- Un usuario puede solicitar unirse a un canal.
- Un usuario puede enviar un mensaje a un canal al que se ha unido.
- Un usuario puede dejar un canal.
- Un canal puede negar o permitir la solicitud de un usuario para unirse
- Un canal puede patear a un usuario.
- Un canal puede transmitir un mensaje a todos los usuarios en el canal.
- Un canal puede enviar un mensaje de bienvenida a usuarios individuales en el canal.
El SRP es un concepto importante, pero difícilmente debe mantenerse por sí solo. Igualmente importante para su diseño es el Principio de Inversión de Dependencia (DIP). Para incorporar eso en el diseño, recuerde que sus implementaciones particulares de las entidades User
, Message
y Channel
deben depender de una abstracción o interfaz en lugar de una implementación concreta particular. Por esta razón comenzamos con el diseño de interfaces no de clases concretas:
public interface ICredentials {}
public interface IMessage
{
//properties
string Text {get;set;}
DateTime TimeStamp { get; set; }
IChannel Channel { get; set; }
}
public interface IChannel
{
//properties
ReadOnlyCollection<IUser> Users {get;}
ReadOnlyCollection<IMessage> MessageHistory { get; }
//abilities
bool Add(IUser user);
void Remove(IUser user);
void BroadcastMessage(IMessage message);
void UnicastMessage(IMessage message);
}
public interface IUser
{
string Name {get;}
ICredentials Credentials { get; }
bool Add(IChannel channel);
void Remove(IChannel channel);
void ReceiveMessage(IMessage message);
void SendMessage(IMessage message);
}
Lo que esta lista no nos dice es por qué motivo se ejecutan estas funcionalidades. Es mejor que pongamos la responsabilidad de "por qué" (administración y control del usuario) en una entidad separada; de esta manera, las entidades de User
y Channel
no tienen que cambiar si el "por qué" cambia. Podemos aprovechar el patrón de estrategia y el ID aquí y podemos hacer que cualquier implementación concreta de IChannel
dependa de una entidad IUserControl
que nos dé el "por qué".
public interface IUserControl
{
bool ShouldUserBeKicked(IUser user, IChannel channel);
bool MayUserJoin(IUser user, IChannel channel);
}
public class Channel : IChannel
{
private IUserControl _userControl;
public Channel(IUserControl userControl)
{
_userControl = userControl;
}
public bool Add(IUser user)
{
if (!_userControl.MayUserJoin(user, this))
return false;
//..
}
//..
}
Verá que en el diseño anterior, el SRP no es ni siquiera perfecto, es decir, un IChannel
aún depende de las abstracciones IUser
e IMessage
.
Al final, uno debe esforzarse por lograr un diseño flexible y ligeramente acoplado, pero siempre hay que hacer concesiones y áreas grises que también dependen del lugar donde espera que cambie su aplicación.
El SRP llevado al extremo en mi opinión conduce a un código muy flexible pero también fragmentado y complejo que podría no ser tan fácilmente comprensible como un código más simple pero un tanto más estrechamente acoplado.
De hecho, si siempre se espera que dos responsabilidades cambien al mismo tiempo, posiblemente no debería separarlas en diferentes clases, ya que esto llevaría, para citar a Martin, a un "olor a complejidad innecesaria". Lo mismo ocurre con las responsabilidades que nunca cambian: el comportamiento es invariable y no es necesario dividirlo.
La idea principal aquí es que debe hacer un llamado de juicio donde vea que las responsabilidades / el comportamiento posiblemente cambien de manera independiente en el futuro, qué comportamiento es co-dependiente entre sí y siempre cambiará al mismo tiempo ("atado en la cadera") y qué comportamiento nunca cambiará en primer lugar.
Estoy tratando de aprender el Principio Único de Responsabilidad (SRP), pero me está resultando bastante difícil, ya que me resulta muy difícil saber cuándo y qué debo eliminar de una clase y dónde debo ubicarlo / organizarlo.
Estaba buscando en Google algunos materiales y ejemplos de código, pero la mayoría de los materiales que encontré, en lugar de hacerlo más fácil de entender, lo hicieron difícil de entender.
Por ejemplo, si tengo una lista de usuarios y de esa lista tengo un control llamado de clase que hace muchas cosas como Enviar un mensaje de saludo y despedida cuando un usuario entra / sale, verifique el clima que el usuario debería poder ingresar o no y patearlo, recibir comandos y mensajes de usuario, etc.
Del ejemplo, no necesita mucho para entender, ya estoy haciendo demasiado en una clase, pero aún no tengo la suficiente claridad sobre cómo dividir y reorganizarlo después.
Si entiendo el SRP, tendría una clase para unirme al canal, para el saludo y el adiós, una clase para la verificación del usuario, una clase para leer los comandos, ¿verdad?
Pero, ¿dónde y cómo usaría la patada, por ejemplo?
Tengo la clase de verificación, así que estoy seguro de que tendría todo tipo de verificación de usuarios, incluido el clima o no, un usuario debería ser expulsado.
Entonces, ¿la función de patada estaría dentro de la clase de unión al canal y se llamaría si falla la verificación?
Por ejemplo:
public void UserJoin(User user)
{
if (verify.CanJoin(user))
{
messages.Greeting(user);
}
else
{
this.kick(user);
}
}
Apreciaría si me pudieran echar una mano aquí con materiales de C # fáciles de entender que están en línea y gratuitos o mostrándome cómo dividiré el ejemplo citado y, si es posible, algunos códigos de muestra, consejos, etc.
Me fue muy fácil aprender este principio. Me lo presentaron en tres partes pequeñas, del tamaño de un bocado:
- Haz una cosa
- Hacer eso solo
- Haz eso bien
El código que cumple con esos criterios cumple con el principio de responsabilidad única.
En su código anterior,
public void UserJoin(User user)
{
if (verify.CanJoin(user))
{
messages.Greeting(user);
}
else
{
this.kick(user);
}
}
UserJoin no cumple con el SRP; está haciendo dos cosas, a saber, saludar al usuario si puede unirse o rechazarla si no puede. Podría ser mejor reorganizar el método:
public void UserJoin(User user)
{
user.CanJoin
? GreetUser(user)
: RejectUser(user);
}
public void Greetuser(User user)
{
messages.Greeting(user);
}
public void RejectUser(User user)
{
messages.Reject(user);
this.kick(user);
}
Funcionalmente, esto no es diferente del código originalmente publicado. Sin embargo, este código es más fácil de mantener; ¿Qué sucede si se produce una nueva regla de negocios que, debido a los recientes ataques de seguridad informática, desea registrar la dirección IP del usuario rechazado? Simplemente modificaría el método RejectUser. ¿Qué pasa si desea mostrar mensajes adicionales al iniciar sesión de usuario? Solo actualiza el método GreetUser.
En mi experiencia, SRP hace que el código sea mantenible. Y el código de mantenimiento tiende a recorrer un largo camino hacia el cumplimiento de las otras partes de SOLID.
Mi recomendación es comenzar con lo básico: ¿qué cosas tienes? Mencionó varias cosas como Message
, User
, Channel
, etc. Además de las cosas simples, también tiene comportamientos que pertenecen a sus cosas . Algunos ejemplos de comportamientos:
- un mensaje puede ser enviado
- un canal puede aceptar un usuario (o podría decir que un usuario puede unirse a un canal)
- un canal puede patear a un usuario
- y así...
Tenga en cuenta que esta es sólo una forma de verlo. ¡Puedes abstraer cualquiera de estos comportamientos hasta que la abstracción no signifique nada y todo! Pero, una capa de abstracción por lo general no duele.
Desde aquí, hay dos escuelas de pensamiento comunes en OOP: encapsulación completa y responsabilidad única. Lo primero lo llevaría a encapsular todo el comportamiento relacionado dentro de su objeto propietario (resultando en un diseño inflexible), mientras que lo segundo lo desaconsejaría (resultando en un acoplamiento flexible y flexible).
Seguiría, pero es tarde y necesito dormir un poco ... Estoy haciendo de esto una publicación comunitaria, para que alguien pueda terminar lo que empecé y mejorar lo que tengo hasta ahora ...
¡Feliz aprendizaje!