¿Un constructor que valida sus argumentos viola SRP?

66

Estoy tratando de adherirme al Principio de Responsabilidad Única (SRP) tanto como sea posible y me acostumbré a un cierto patrón (para el SRP en los métodos) confiando en gran medida en los delegados. Me gustaría saber si este enfoque es sólido o si hay problemas graves con él.

Por ejemplo, para verificar la entrada a un constructor, podría presentar el siguiente método (la Streamentrada es aleatoria, podría ser cualquier cosa)

private void CheckInput(Stream stream)
{
    if(stream == null)
    {
        throw new ArgumentNullException();
    }

    if(!stream.CanWrite)
    {
        throw new ArgumentException();
    }
}

Este método (posiblemente) hace más de una cosa.

  • Comprueba las entradas
  • Lanzar diferentes excepciones

Para cumplir con el SRP, por lo tanto, cambié la lógica a

private void CheckInput(Stream stream, 
                        params (Predicate<Stream> predicate, Action action)[] inputCheckers)
{
    foreach(var inputChecker in inputCheckers)
    {
        if(inputChecker.predicate(stream))
        {
            inputChecker.action();
        }
    }
}

Lo que supuestamente solo hace una cosa (¿lo hace?): Verifique la entrada. Para la comprobación real de las entradas y el lanzamiento de las excepciones, he introducido métodos como

bool StreamIsNull(Stream s)
{
    return s == null;
}

bool StreamIsReadonly(Stream s)
{
    return !s.CanWrite;
}

void Throw<TException>() where TException : Exception, new()
{
    throw new TException();
}

y puede llamar CheckInputcomo

CheckInput(stream,
    (this.StreamIsNull, this.Throw<ArgumentNullException>),
    (this.StreamIsReadonly, this.Throw<ArgumentException>))

¿Es esto mejor que la primera opción o introduzco una complejidad innecesaria? ¿Hay alguna manera de que aún pueda mejorar este patrón, si es viable?

Paul Kertscher
fuente
26
Podría argumentar que CheckInputtodavía está haciendo varias cosas: está iterando sobre una matriz y llamando a una función de predicado y llamando a una función de acción. ¿No es eso una violación del SRP?
Bart van Ingen Schenau
8
Sí, ese es el punto que estaba tratando de hacer.
Bart van Ingen Schenau
135
Es importante recordar que es el principio de responsabilidad única ; No es el principio de acción única . Tiene una responsabilidad: verificar que la secuencia esté definida y que se pueda escribir.
David Arno
40
Tenga en cuenta que el objetivo de estos principios de software es hacer que el código sea más fácil de leer y mantener. Su CheckInput original es mucho más fácil de leer y mantener que su versión refactorizada. De hecho, si alguna vez me encuentro con su método CheckInput final en una base de código, lo descartaría todo y lo reescribiría para que coincida con lo que tenía originalmente.
17 de 26
17
Estos "principios" son prácticamente inútiles porque puede definir "responsabilidad única" de la forma que quiera seguir adelante, sea cual sea su idea original. Pero si intentas aplicarlos rígidamente, supongo que terminas con este tipo de código que, para ser sincero, es difícil de entender.
Casey

Respuestas:

151

SRP es quizás el principio de software más incomprendido.

Una aplicación de software se crea a partir de módulos, que se crean a partir de módulos, que se crean a partir de ...

En la parte inferior, una sola función, como CheckInputsolo contendrá un poco de lógica, pero a medida que avanza, cada módulo sucesivo encapsula más y más lógica y esto es normal .

SRP no se trata de hacer una sola acción atómica . Se trata de tener una sola responsabilidad, incluso si esa responsabilidad requiere múltiples acciones ... y, en última instancia, se trata de mantenimiento y capacidad de prueba :

  • promueve la encapsulación (evitando los objetos de Dios),
  • promueve la separación de preocupaciones (evitando cambios ondulantes en toda la base de código),
  • ayuda a la capacidad de prueba al reducir el alcance de las responsabilidades.

El hecho de que CheckInputse implemente con dos controles y genere dos excepciones diferentes es irrelevante en cierta medida.

CheckInputtiene una responsabilidad limitada: garantizar que la entrada cumpla con los requisitos. Sí, hay múltiples requisitos, pero esto no significa que haya múltiples responsabilidades. Sí, podría dividir los cheques, pero ¿cómo ayudaría eso? En algún momento, los cheques deben estar listados de alguna manera.

Comparemos:

Constructor(Stream stream) {
    CheckInput(stream);
    // ...
}

versus:

Constructor(Stream stream) {
    CheckInput(stream,
        (this.StreamIsNull, this.Throw<ArgumentNullException>),
        (this.StreamIsReadonly, this.Throw<ArgumentException>));
    // ...
}

Ahora, CheckInputhace menos ... ¡pero la persona que llama hace más!

Ha cambiado la lista de requisitos desde CheckInput, donde están encapsulados, a Constructordonde están visibles.

¿Es un buen cambio? Depende:

  • Si CheckInputsolo se llama allí: es discutible, por un lado, hace visibles los requisitos, por otro lado, abarrota el código;
  • Si CheckInputse llama varias veces con los mismos requisitos , entonces viola DRY y tiene un problema de encapsulación.

Es importante darse cuenta de que una sola responsabilidad puede implicar mucho trabajo. El "cerebro" de un auto sin conductor tiene una sola responsabilidad:

Conduciendo el automóvil a su destino.

Es una responsabilidad única, pero requiere coordinar una tonelada de sensores y actores, tomar muchas decisiones e incluso tiene requisitos posiblemente conflictivos 1 ...

... sin embargo, todo está encapsulado. Entonces al cliente no le importa.

1 seguridad de los pasajeros, seguridad de los demás, respeto de la normativa, ...

Matthieu M.
fuente
2
Creo que la forma en que está utilizando la palabra "encapsulación" y sus derivados es confusa. Aparte de eso, ¡gran respuesta!
Fabio dice reinstalar a Mónica el
44
Estoy de acuerdo con su respuesta, pero el argumento del cerebro del automóvil sin conductor a menudo tienta a las personas a romper el SRP. Como dijiste, son módulos hechos de módulos hechos de módulos. Puede identificar el propósito de todo ese sistema, pero ese sistema debe dividirse por sí mismo. Puedes analizar casi cualquier problema.
Sava B.
13
@SavaB .: Claro, pero el principio sigue siendo el mismo. Un módulo debe tener una responsabilidad única, aunque de mayor alcance que sus componentes.
Matthieu M.
3
@ user949300 Bien, ¿qué tal simplemente "conducir"? Realmente, "conducir" es la responsabilidad y "con seguridad" y "legalmente" son requisitos sobre cómo cumple esa responsabilidad. Y a menudo enumeramos los requisitos cuando declaramos una responsabilidad.
Brian McCutchon el
1
"SRP es quizás el principio de software más incomprendido". Como lo demuestra esta respuesta :)
Michael
41

Citando al tío Bob sobre el SRP ( https://8thlight.com/blog/uncle-bob/2014/05/08/SingleReponilabilityPrinciple.html ):

El Principio de Responsabilidad Única (SRP) establece que cada módulo de software debe tener una y solo una razón para cambiar.

... Este principio es sobre las personas.

... Cuando escribe un módulo de software, desea asegurarse de que cuando se soliciten cambios, esos cambios solo puedan originarse en una sola persona, o más bien, en un solo grupo de personas estrechamente acopladas que represente una única función comercial definida de forma estrecha.

... Esta es la razón por la que no ponemos SQL en JSP. Esta es la razón por la que no generamos HTML en los módulos que calculan los resultados. Esta es la razón por la cual las reglas de negocio no deben conocer el esquema de la base de datos. Esta es la razón por la cual separamos las preocupaciones.

Explica que los módulos de software deben abordar las preocupaciones específicas de los interesados. Por lo tanto, respondiendo a su pregunta:

¿Es esto mejor que la primera opción o introduzco una complejidad innecesaria? ¿Hay alguna manera de que aún pueda mejorar este patrón, si es viable?

En mi opinión, solo está buscando un método, cuando debería buscar un nivel superior (nivel de clase en este caso). Quizás deberíamos echar un vistazo a lo que su clase está haciendo actualmente (y esto requiere más explicación sobre su escenario). Por ahora, tu clase sigue haciendo lo mismo. Por ejemplo, si mañana hay alguna solicitud de cambio sobre alguna validación (por ejemplo: "ahora la secuencia puede ser nula"), entonces aún debe ir a esta clase y cambiar las cosas dentro de ella.

Emerson Cardoso
fuente
44
La mejor respuesta. Para elaborar sobre OP, si los controles de guardia provienen de dos partes interesadas / departamentos diferentes, entonces se checkInputs()deben dividir, digamos en checkMarketingInputs()y checkRegulatoryInputs(). De lo contrario, está bien combinarlos todos en un solo método.
user949300
36

No, este cambio no es informado por el SRP.

Pregúntese por qué no hay ninguna verificación en su corrector para "el objeto pasado es una secuencia" . La respuesta es obvia: el lenguaje evita que la persona que llama compile un programa que no se transmite.

El sistema de tipos de C # es insuficiente para satisfacer sus necesidades; sus cheques están implementando la aplicación de invariantes que no se pueden expresar en el sistema de tipos hoy . Si hubiera una manera de decir que el método toma una secuencia de escritura no anulable, lo habría escrito, pero no lo hay, por lo que hizo lo siguiente mejor: impuso la restricción de tipo en tiempo de ejecución. Con suerte, también lo documentó, para que los desarrolladores que usan su método no tengan que violarlo, suspendan sus casos de prueba y luego solucionen el problema.

Poner tipos en un método no es una violación del Principio de Responsabilidad Única; tampoco es el método hacer cumplir sus precondiciones o afirmar sus postcondiciones.

Eric Lippert
fuente
1
Además, dejar el objeto creado en un estado válido es la única responsabilidad que tiene fundamentalmente un constructor. Si, como mencionó, requiere verificaciones adicionales que el tiempo de ejecución y / o el compilador no pueden proporcionar, entonces realmente no hay forma de evitarlo.
SBI
23

No todas las responsabilidades son iguales.

ingrese la descripción de la imagen aquí

ingrese la descripción de la imagen aquí

Aquí hay dos cajones. Ambos tienen una responsabilidad. Cada uno tiene nombres que le permiten saber qué les pertenece. Uno es el cajón de los cubiertos. El otro es el cajón de basura.

Entonces, ¿cuál es la diferencia? El cajón de los cubiertos deja en claro lo que no le pertenece. Sin embargo, el cajón de basura acepta todo lo que cabe. Sacar las cucharas del cajón de los cubiertos parece estar muy mal. Sin embargo, me cuesta pensar en cualquier cosa que se pierda si se retira del cajón de basura. La verdad es que puedes afirmar que cualquier cosa tiene una responsabilidad única, pero ¿cuál crees que tiene la responsabilidad individual más centrada?

Un objeto que tiene una sola responsabilidad no significa que solo una cosa pueda pasar aquí. Las responsabilidades pueden anidar. Pero esas responsabilidades de anidación deberían tener sentido, no deberían sorprenderte cuando las encuentres aquí y deberías extrañarlas si se hubieran ido.

Entonces cuando ofreces

CheckInput(Stream stream);

No me preocupa que esté revisando entradas y lanzando excepciones. Me preocuparía si también estaba revisando la entrada y guardando la entrada. Esa es una sorpresa desagradable. Uno que no extrañaría si se hubiera ido.

naranja confitada
fuente
21

Cuando se hace un nudo y escribe un código extraño para cumplir con un Principio de software importante, por lo general, ha entendido mal el principio (aunque a veces el principio es incorrecto). Como señala la excelente respuesta de Matthieu, todo el significado de SRP depende de la definición de "responsabilidad".

Los programadores experimentados ven estos principios y los relacionan con recuerdos del código que arruinamos; los programadores menos experimentados los ven y pueden no tener nada con lo que relacionarlos. Es una abstracción flotando en el espacio, toda sonrisa y ningún gato. Entonces adivinan, y por lo general va mal. Antes de que hayas desarrollado el sentido del caballo de programación, la diferencia entre el código extraño y complicado y el código normal no es del todo obvio.

Este no es un mandamiento religioso que debes obedecer independientemente de las consecuencias personales. Es más una regla práctica destinada a formalizar un elemento de programación de sentido común y ayudarlo a mantener su código lo más simple y claro posible. Si tiene el efecto contrario, tiene razón al buscar alguna entrada externa.

En la programación, no puede equivocarse mucho más que tratar de deducir el significado de un identificador a partir de los primeros principios con solo mirarlo, y eso se aplica tanto a los identificadores por escrito sobre la programación como a los identificadores en el código real.

Ed Plunkett
fuente
14

Rol CheckInput

Primero, permítanme decir lo obvio CheckInput : hacer una cosa, incluso si está revisando varios aspectos. En última instancia, verifica la entrada . Se podría argumentar que no es una cosa si se trata de métodos llamados DoSomething, pero creo que es seguro asumir que la entrada de verificación no es demasiado vaga.

Agregar este patrón para predicados podría ser útil si no desea que la lógica para verificar la entrada se coloque en su clase, pero este patrón parece bastante detallado para lo que está tratando de lograr. Puede ser mucho más directo simplemente pasar una interfaz IStreamValidatorcon un método único isValid(Stream)si eso es lo que desea obtener. Cualquier implementación de clases IStreamValidatorpuede usar predicados como StreamIsNullo StreamIsReadonlysi lo desean, pero volviendo al punto central, es un cambio bastante ridículo hacer en aras del mantenimiento del principio de responsabilidad única.

Prueba de cordura

Es mi idea que a todos se nos permita una "verificación de cordura" para asegurarnos de que al menos se esté tratando con un Stream que no sea nulo y que se pueda escribir, y este chequeo básico no está haciendo de alguna manera que su clase sea un validador de streams. Eso sí, sería mejor dejar los controles más sofisticados fuera de tu clase, pero ahí es donde se traza la línea. Una vez que necesite comenzar a cambiar el estado de su transmisión al leerla o dedicar recursos a la validación, ha comenzado a realizar una validación formal de su transmisión y esto es lo que debe incorporarse a su propia clase.

Conclusión

Creo que si está aplicando un patrón para organizar mejor un aspecto de su clase, merece estar en su propia clase. Dado que un patrón no encaja, también debe preguntarse si realmente pertenece o no a su propia clase en primer lugar. Creo que, a menos que crea que la validación de la transmisión probablemente cambiará en el futuro, y especialmente si cree que esta validación puede ser de naturaleza dinámica, entonces el patrón que describió es una buena idea, incluso si puede ser inicialmente trivial De lo contrario, no hay necesidad de hacer arbitrariamente que su programa sea más complejo. Llamemos a las cosas por su nombre. La validación es una cosa, pero verificar la entrada nula no es validación y, por lo tanto, creo que puede estar seguro de mantenerla en su clase sin violar el principio de responsabilidad única.

Neil
fuente
4

El principio enfáticamente no establece que un fragmento de código debería "hacer solo una cosa".

La "responsabilidad" en SRP debe entenderse a nivel de los requisitos. La responsabilidad del código es satisfacer los requisitos comerciales. Se viola SRP si un objeto satisface más de un requisito comercial independiente . Por independiente significa que un requisito podría cambiar mientras el otro requisito permanece en su lugar.

Es concebible que se introduzca un nuevo requisito comercial, lo que significa que este objeto en particular no debería ser legible, mientras que otro requisito comercial aún requiere que el objeto verifique si es legible. No, porque los requisitos empresariales no especifican detalles de implementación a ese nivel.

Un ejemplo real de una violación de SRP sería un código como este:

var message = "Your package will arrive before " + DateTime.Now.AddDays(14);

Este código es muy simple, pero aún es concebible que el texto cambie independientemente de la fecha de entrega esperada, ya que estos son decididos por diferentes partes del negocio.

JacquesB
fuente
Una clase diferente para prácticamente todos los requisitos suena como una pesadilla impía.
whatsisname
@whatsisname: Entonces quizás el SRP no sea para ti. Ningún principio de diseño se aplica a todo tipo y tamaño de proyectos. (Pero tenga en cuenta que solo estamos hablando de requisitos independientes (es decir, pueden cambiar de forma independiente), no solo cualquier requisito, ya que solo dependería de qué tan específicos sean.)
JacquesB
Creo que es más que el SRP requiere un elemento de juicio situacional que es difícil de describir en una sola frase pegadiza.
whatsisname
@whatsisname: estoy totalmente de acuerdo.
JacquesB
Se viola
Juzer Ali
3

Me gusta el punto de la respuesta de @ EricLippert :

Pregúntese por qué no hay verificación en su verificador para el objeto pasado en una secuencia . La respuesta es obvia: el lenguaje evita que la persona que llama compile un programa que no se transmite.

El sistema de tipos de C # es insuficiente para satisfacer sus necesidades; sus cheques están implementando la aplicación de invariantes que no se pueden expresar en el sistema de tipos hoy . Si hubiera una manera de decir que el método toma una secuencia de escritura no anulable, lo habría escrito, pero no lo hay, por lo que hizo lo siguiente mejor: impuso la restricción de tipo en tiempo de ejecución. Con suerte, también lo documentó, para que los desarrolladores que usan su método no tengan que violarlo, suspendan sus casos de prueba y luego solucionen el problema.

EricLippert tiene razón en que este es un problema para el sistema de tipos. Y dado que desea utilizar el principio de responsabilidad única (SRP), básicamente necesita que el sistema de tipos sea responsable de este trabajo.

En realidad, es posible hacer esto en C #. Podemos capturar literales nullen tiempo de compilación, luego capturar no literales nullen tiempo de ejecución. Eso no es tan bueno como una verificación completa en tiempo de compilación, pero es una mejora estricta respecto a no atrapar nunca en tiempo de compilación.

Entonces, ¿sabes cómo tiene C # Nullable<T>? Vamos a revertir eso y hacer un NonNullable<T>:

public struct NonNullable<T> where T : class
{
    public T Value { get; private set; }
    public NonNullable(T value)
    {
        if (value == null) { throw new NullArgumentException(); }
        this.Value = value;
    }
    //  Ease-of-use:
    public static implicit operator T(NonNullable<T> value) { return value.Value; }
    public static implicit operator NonNullable<T>(T value) { return new NonNullable<T>(value); }

    //  Hack-ish overloads that prevent null-literals from being implicitly converted into NonNullable<T>'s.
    public static implicit operator NonNullable<T>(Tuple<T> value) { return new NonNullable<T>(value.Item1); }
    public static implicit operator NonNullable<T>(Tuple<T, T> value) { return new NonNullable<T>(value.Item1); }
}

Ahora, en lugar de escribir

public void Foo(Stream stream)
{
  if (stream == null) { throw new NullArgumentException(); }

  // ...method code...
}

, solo escribe:

public void Foo(NonNullable<Stream> stream)
{
  // ...method code...
}

Entonces, hay tres casos de uso:

  1. El usuario llama Foo()con un no nulo Stream:

    Stream stream = new Stream();
    Foo(stream);

    Este es el caso de uso deseado, y funciona con o sin NonNullable<>.

  2. El usuario llama Foo()con un nulo Stream:

    Stream stream = null;
    Foo(stream);

    Este es un error de llamada. Aquí NonNullable<>ayuda a informar al usuario que no deberían estar haciendo esto, pero en realidad no los detiene. De cualquier manera, esto resulta en un tiempo de ejecución NullArgumentException.

  3. El usuario llama Foo()con null:

    Foo(null);

    nullno se convertirá implícitamente en a NonNullable<>, por lo que el usuario obtiene un error en el IDE antes del tiempo de ejecución. Esto es delegar la verificación nula al sistema de tipos, tal como lo recomendaría el SRP.

También puede extender este método para afirmar otras cosas sobre sus argumentos. Por ejemplo, dado que desea una secuencia que se pueda escribir, puede definir una struct WriteableStream<T> where T:Streamque compruebe ambas nully stream.CanWriteel constructor. Esto todavía sería una verificación de tipo en tiempo de ejecución, pero:

  1. Decora el tipo con el WriteableStreamcalificador, lo que indica la necesidad de las personas que llaman.

  2. Realiza la verificación en un solo lugar en el código, por lo que no tiene que repetir la verificación y throw InvalidArgumentExceptioncada vez.

  3. Se ajusta mejor al SRP empujando las tareas de verificación de tipos al sistema de tipos (según lo extendido por los decoradores genéricos).

Nat
fuente
3

Su enfoque es actualmente procesal. Estás rompiendo el Streamobjeto y validado desde el exterior. No hagas eso: rompe la encapsulación. Deje que el Streamresponsable de su propia validación. No podemos tratar de aplicar el SRP hasta que tengamos algunas clases para aplicarlo.

Aquí hay una Streamque realiza una acción solo si pasa la validación:

class Stream
{
    public void someAction()
    {
        if(!stream.canWrite)
        {
            throw new ArgumentException();
        }

        System.out.println("My action");
    }
}

¡Pero ahora estamos violando SRP! "Una clase debería tener una sola razón para cambiar". Tenemos una combinación de 1) validación y 2) lógica real. Tenemos dos razones por las que podría necesitar cambiar.

Podemos resolver esto validando decoradores . Primero, necesitamos convertir nuestro Streama una interfaz e implementarlo como una clase concreta.

interface Stream
{
    void someAction();
}

class DefaultStream implements Stream
{
    @Override
    public void someAction()
    {
        System.out.println("My action");
    }
}

Ahora podemos escribir un decorador que envuelva a Stream, realice la validación y difiera lo dado Streampara la lógica real de la acción.

class WritableStream implements Stream
{
    private final Stream stream;

    public WritableStream(final Stream stream)
    {
        this.stream = stream;
    }

    @Override
    public void someAction()
    {
        if(!stream.canWrite)
        {
            throw new ArgumentException();
        }
        stream.someAction();
    }
}

Ahora podemos componer estos de la forma que queramos:

final Stream myStream = new WritableStream(
    new DefaultStream()
);

¿Quieres validación adicional? Agrega otro decorador.

Miguel
fuente
1

El trabajo de una clase es proporcionar un servicio que cumpla con un contrato . Una clase siempre tiene un contrato: un conjunto de requisitos para usarlo, y promete cumplir con su estado y resultados siempre que se cumplan los requisitos. Este contrato puede ser explícito, a través de documentación y / o aserciones, o implícito, pero siempre existe.

Parte del contrato de su clase es que la persona que llama le da al constructor algunos argumentos que no deben ser nulos. La implementación del contrato es responsabilidad de la clase, por lo que verificar que la persona que llama ha cumplido su parte del contrato puede considerarse fácilmente dentro del alcance de la responsabilidad de la clase.

La idea de que una clase implemente un contrato se debe a Bertrand Meyer , el diseñador del lenguaje de programación Eiffel y de la idea del diseño por contrato . El lenguaje Eiffel hace que la especificación y la verificación del contrato formen parte del lenguaje.

Wayne Conrad
fuente
0

Como se ha señalado en otras respuestas, SRP a menudo se malinterpreta. No se trata de tener un código atómico que solo haga una función. Se trata de asegurarse de que sus objetos y métodos solo hagan una cosa, y que la única cosa se haga en un solo lugar.

Veamos un mal ejemplo en pseudocódigo.

class Math
    private int a;
    private int b;
    def constructor(int x, int y) 
        if(x != null)
          a = x
        else if(x < 0)
          a = abs(x)
        else if (x == -1)
          throw "Some Silly Error"
        else
          a = 0
        end
        if(y != null)
           b = y
        else if(y < 0)
           b = abs(y)
        else if(y == -1)
           throw "Some Silly Error"
        else
         b = 0
        end
    end
    def add()
        return a + b
    end
    def sub()
        return b - a
    end
end

En nuestro ejemplo bastante absurdo, la "responsabilidad" del constructor Math # es hacer que el objeto matemático sea utilizable. Lo hace desinfectando primero la entrada y luego asegurándose de que los valores no sean -1.

Este es un SRP válido porque el constructor solo está haciendo una cosa. Está preparando el objeto matemático. Sin embargo, no es muy fácil de mantener. Viola SECO.

Así que demos otro paso

class Math
    private int a;
    private int b;
    def constructor(int x, int y)
        cleanX(x)
        cleanY(y)
    end
    def cleanX(int x)
        if(x != null)
          a = x
        else if(x < 0)
          a = abs(x)
        else if (x == -1)
          throw "Some Silly Error"
        else
          a = 0
        end
   end
   def cleanY(int y)
        if(y != null)
           b = y
        else if(y < 0)
           b = abs(y)
        else if(y == -1)
           throw "Some Silly Error"
        else
         b = 0
        end
    end
    def add()
        return a + b
    end
    def sub()
        return b - a
    end
end

En este pase, mejoramos un poco acerca de DRY, pero todavía tenemos formas de ir con DRY. SRP por otro lado parece un poco fuera de lugar. Ahora tenemos dos funciones con el mismo trabajo. Tanto cleanX como cleanY desinfectan la entrada.

Vamos a darle otra oportunidad

class Math
    private int a;
    private int b;
    def constructor(int x, int y)
        a = clean(x)
        b = clean(y)
    end
    def clean(int i)
        if(i != null)
          return i
        else if(i < 0)
          return abs(i)
        else if (i == -1)
          throw "Some Silly Error"
        else
          return 0
        end
    end
    def add()
        return a + b
    end
    def sub()
        return b - a
    end
end

Ahora finalmente estaban mejor sobre DRY, y SRP parece estar de acuerdo. Solo tenemos un lugar que hace el trabajo de "desinfección".

En teoría, el código es más fácil de mantener y mejor cuando vamos a corregir el error y ajustar el código, solo tenemos que hacerlo en un solo lugar.

class Math
    private int a;
    private int b;
    def constructor(int x, int y)
        a = clean(x)
        b = clean(y)
    end
    def clean(int i)
        if(i == null)
          return 0
        else if (i == -1)
          throw "Some Silly Error"
        else
          return abs(i)
        end
    end
    def add()
        return a + b
    end
    def sub()
        return b - a
    end
end

En la mayoría de los casos del mundo real, los objetos serían más complejos y SRP se aplicaría en un conjunto de objetos. Por ejemplo, la edad puede pertenecer a Padre, Madre, Hijo, Hija, por lo que en lugar de tener 4 clases que calculan la edad desde la fecha de nacimiento, tiene una clase Persona que hace eso y las 4 clases heredan de eso. Pero espero que este ejemplo ayude a explicar. SRP no se trata de acciones atómicas, sino de un "trabajo" realizado.

coteyr
fuente
-3

Hablando de SRP, al tío Bob no le gustan los cheques nulos esparcidos por todas partes. En general, usted, como equipo, debe evitar el uso de parámetros nulos para los constructores siempre que sea posible. Cuando publica su código fuera de su equipo, las cosas pueden cambiar.

Hacer cumplir la no nulabilidad de los parámetros del constructor sin garantizar primero la cohesión de la clase en cuestión da como resultado una hinchazón en el código de llamada, especialmente las pruebas.

Si realmente quiere hacer cumplir dichos contratos, considere usar Debug.Asserto algo similar para reducir el desorden:

public AClassThatDefinitelyNeedsAWritableStream(Stream stream)
{
   Assert.That(stream.CanWrite, "Put crucial information here, and not inane bloat.");

   // Go on normal operation.
}
abuzittin gillifirca
fuente