Dos definiciones contradictorias del Principio de segregación de interfaz: ¿cuál es la correcta?

14

Al leer artículos sobre ISP, parece haber dos definiciones contradictorias de ISP:

Según la primera definición (ver 1 , 2 , 3 ), el ISP establece que las clases que implementan la interfaz no deberían verse obligadas a implementar funcionalidades que no necesitan. Por lo tanto, interfaz gruesaIFat

interface IFat
{
     void A();
     void B();
     void C();
     void D();
}

class MyClass: IFat
{ ... }

debe dividirse en interfaces más pequeñas ISmall_1yISmall_2

interface ISmall_1
{
     void A();
     void B();
}

interface ISmall_2
{
     void C();
     void D();
}

class MyClass:ISmall_2
{ ... }

ya que de esta manera my MyClasspuede implementar solo los métodos que necesita ( D()y C()), sin verse obligado a proporcionar implementaciones ficticias para A(), B()y C():

Pero de acuerdo con la segunda definición (ver 1 , 2 , respuesta de Nazar Merza ), el ISP afirma que MyClientllamar a los métodos MyServiceno debe ser consciente de los métodos MyServiceque no necesita. En otras palabras, si MyClientsolo necesita la funcionalidad de C()y D(), en lugar de

class MyService 
{
    public void A();
    public void B();
    public void C();
    public void D();
}

/*client code*/      
MyService service = ...;
service.C(); 
service.D();

debemos segregar los MyService'smétodos en interfaces específicas del cliente :

public interface ISmall_1
{
     void A();
     void B();
}

public interface ISmall_2
{
     void C();
     void D();
}

class MyService:ISmall_1, ISmall_2 
{ ... }

/*client code*/
ISmall_2 service = ...;
service.C(); 
service.D();

Por lo tanto, con la primera definición, el objetivo del ISP es " facilitar la vida de las clases que implementan la interfaz IFat ", mientras que con la segunda, el objetivo del ISP es " facilitar la vida de los clientes que llaman métodos de MyService ".

¿Cuál de las dos definiciones diferentes de ISP es realmente correcta?

@MARJAN VENEMA

1)

Entonces, cuando va a dividir IFat en una interfaz más pequeña, qué métodos terminan en qué ISmallinterface debe decidirse en función de la cohesión de los miembros.

Si bien tiene sentido colocar métodos cohesivos dentro de la misma interfaz, pensé que con el patrón ISP las necesidades del cliente tienen prioridad sobre la "cohesión" de una interfaz. En otras palabras, pensé que con ISP deberíamos agrupar dentro de la misma interfaz aquellos métodos necesarios para clientes particulares, incluso si eso significa dejar fuera de esa interfaz aquellos métodos que, por el bien de la cohesión, también deberían ponerse dentro de esa misma interfaz.

Por lo tanto, si había muchos clientes que solo necesitarían llamar CutGreens, pero no también GrillMeat, entonces, para adherirnos al patrón de ISP, ¿solo deberíamos ponerlos CutGreensdentro ICook, pero no también GrillMeat, a pesar de que los dos métodos son altamente cohesivos?

2)

Creo que su confusión se deriva de una suposición oculta en la primera definición: que las clases implementadoras ya están siguiendo el principio de responsabilidad única.

Al "implementar clases que no siguen SRP", ¿se refiere a las clases que implementan IFatoa las clases que implementan ISmall_1/ ISmall_2? ¿Asumo que te refieres a clases que implementan IFat? Si es así, ¿por qué supone que no siguen SRP?

Gracias

EdvRusj
fuente
44
¿Por qué no puede haber múltiples definiciones que sean atendidas por el mismo principio?
Bobson
55
Estas definiciones no se contradicen entre sí.
Mike Partridge
1
No, por supuesto, la necesidad del cliente no tiene prioridad sobre la cohesión de una interfaz. Puede llevar esta forma de "regla" hasta el extremo y terminar con interfaces de método único en todo el lugar que no tienen absolutamente ningún sentido. Deje de seguir las reglas y comience a pensar en los objetivos para los que se crearon estas reglas. Con "clases que no siguen SRP" no estaba hablando de ninguna clase específica en su ejemplo o que ya no seguían SRP. Lea de nuevo. La primera definición solo conduce a dividir una interfaz si la interfaz no sigue a ISP y la clase sigue a SRP.
Marjan Venema
2
La segunda definición no se preocupa por los implementadores. Define las interfaces desde la perspectiva de las personas que llaman y no hace suposiciones sobre si los implementadores ya existen o no. Probablemente asume que cuando sigues a ISP y vienes a implementar esas interfaces, por supuesto, seguirías a SRP al crearlas.
Marjan Venema
2
¿Cómo saber de antemano qué clientes existirán y qué métodos necesitarán? No puedes Lo que puede saber de antemano es cuán cohesiva es su interfaz.
Tulains Córdova

Respuestas:

6

Ambos son correctos

Según lo leo, el propósito del ISP (Principio de segregación de interfaz) es mantener las interfaces pequeñas y enfocadas: todos los miembros de la interfaz deben tener una cohesión muy alta. Ambas definiciones están destinadas a evitar las interfaces "jack-of-all-trades-master-of-none".

La segregación de interfaz y el SRP (Principio de responsabilidad única) tienen el mismo objetivo: garantizar componentes de software pequeños y altamente coherentes. Se complementan entre sí. La segregación de interfaz garantiza que las interfaces sean pequeñas, enfocadas y altamente cohesivas. Seguir el principio de responsabilidad única asegura que las clases sean pequeñas, enfocadas y altamente cohesivas.

La primera definición que menciona se centra en los implementadores, la segunda en los clientes. Lo cual, a diferencia de @ user61852, considero que son los usuarios / llamantes de la interfaz, no los implementadores.

Creo que su confusión se deriva de una suposición oculta en la primera definición: que las clases implementadoras ya están siguiendo el principio de responsabilidad única.

Para mí, la segunda definición, con los clientes como llamadores de la interfaz, es una mejor manera de llegar al objetivo deseado.

Segregando

En su pregunta usted declara:

de esta manera, MyClass puede implementar solo los métodos que necesita (D () y C ()), sin verse obligado a proporcionar implementaciones ficticias para A (), B () y C ():

Pero eso está poniendo el mundo patas arriba.

  • Una clase que implementa una interfaz no dicta lo que necesita en la interfaz que está implementando.
  • Las interfaces dictan qué métodos debe proporcionar una clase de implementación.
  • Los llamadores de una interfaz realmente son los que dictan qué funcionalidad necesitan que la interfaz les proporcione y, por lo tanto, qué debe proporcionar un implementador.

Entonces, cuando se va a dividir IFaten una interfaz más pequeña, qué métodos terminan en qué ISmallinterfaz se debe decidir en función de la cohesión de los miembros.

Considere esta interfaz:

interface IEverythingButTheKitchenSink
{
     void DoDishes();
     void CleanSink();
     void CutGreens();
     void GrillMeat();
}

¿Qué métodos pondrías ICooky por qué? ¿Le puso CleanSinkjunto con GrillMeatsólo porque le sucede que tiene una clase que hace precisamente eso y un par de otras cosas, pero nada como cualquiera de los otros métodos? ¿O lo dividirías en dos interfaces cohesivas más, como:

interface IClean
{
     void DoDishes();
     void CleanSink();
}

interface ICook
{
     void CutGreens();
     void GrillMeat();
}

Nota de declaración de interfaz

Una definición de interfaz debería estar sola en una unidad separada, pero si necesita vivir con la persona que llama o con el implementador, realmente debería estar con la persona que llama. De lo contrario, la persona que llama obtiene una dependencia inmediata del implementador, lo que anula el propósito de las interfaces por completo. Consulte también: Declarar interfaz en el mismo archivo que la clase base, ¿es una buena práctica? en Programadores y ¿Por qué deberíamos colocar interfaces con clases que las usan en lugar de aquellas que las implementan? en StackOverflow.

Marjan Venema
fuente
1
¿Puedes ver la actualización que hice?
EdvRusj
"la persona que llama obtiene una dependencia inmediata del implementador " ... solo si viola DIP (principio de inversión de dependencia), si las variables internas, los parámetros, los valores de retorno, etc. de la persona que llama son de tipo en ICooklugar de tipo SomeCookImplementor, como lo exige DIP, entonces no 't tiene que depender SomeCookImplementor.
Tulains Córdova
@ user61852: si la declaración de interfaz y el implementador están en la misma unidad, inmediatamente obtengo una dependencia de ese implementador. No necesariamente en tiempo de ejecución, pero ciertamente en el nivel del proyecto, simplemente por el hecho de que está allí. El proyecto ya no puede compilarse sin él o lo que sea que use. Además, la inyección de dependencia no es lo mismo que el principio de inversión de dependencia. Quizás te interese DIP in the wild
Marjan Venema
Reutilicé sus ejemplos de código en esta pregunta programmers.stackexchange.com/a/271142/61852 , mejorándolo después de que ya fue aceptado. Te di el debido crédito por los ejemplos.
Tulains Córdova
Cool @ user61852 :) (y gracias por el crédito)
Marjan Venema
14

Confunde la palabra "cliente" como se usa en los documentos de la Banda de los Cuatro con un "cliente" como consumidor de un servicio.

Un "cliente", según lo previsto por las definiciones de la Banda de los Cuatro, es una clase que implementa una interfaz. Si la clase A implementa la interfaz B, entonces dicen que A es un cliente de B. De lo contrario, la frase "los clientes no deberían verse obligados a implementar interfaces que no usan" no tendría sentido ya que los "clientes" (como en los consumidores) no No implemente nada. La frase solo tiene sentido cuando ve "cliente" como "implementador".

Si "cliente" se refiere a una clase que "consume" (llama) los métodos de otra clase que implementa la interfaz grande, entonces llamar a los dos métodos que le interesan e ignorar el resto, sería suficiente para mantenerlo desconectado del resto de los métodos que no usas

El espíritu del principio es evitar que el "cliente" (la clase que implementa la interfaz) tenga que implementar métodos ficticios para cumplir con toda la interfaz cuando solo le importa un conjunto de métodos relacionados.

También tiene como objetivo tener la menor cantidad de acoplamiento posible para que los cambios realizados en un solo lugar causen el menor impacto. Al segregar las interfaces, reduce el acoplamiento.

Ese problema aparece cuando la interfaz hace demasiado y tiene métodos que deberían dividirse en varias interfaces en lugar de solo una.

Ambos ejemplos de código están bien . Es solo que en el segundo asume que "cliente" significa "una clase que consume / llama a los servicios / métodos ofrecidos por otra clase".

No encuentro contradicciones en los conceptos explicados en los tres enlaces que proporcionó.

Solo tenga en cuenta que el "cliente" es el implementador , en SOLID talk.

Tulains Córdova
fuente
Pero según @pdr, si bien los ejemplos de código en todos los enlaces se adhieren al ISP, la definición del ISP se trata más de "aislar al cliente (una clase que llama a métodos de otra clase) de saber más sobre el servicio" que sobre " prevención de que los clientes (implementadores) se vean obligados a implementar interfaces que no usan ".
EdvRusj
1
@EdvRusj Mi respuesta se basa en los documentos del sitio web Object Mentor (empresa Bob Martin), escritos por el propio Martin cuando estaba en la famosa Gang of Four. Como saben, el Gnag of Four fue un grupo de ingenieros de software, incluido Martin, que acuñó el acrónimo SOLID, identificó y documentó los principios. docs.google.com/a/cleancoder.com/file/d/…
Tulains Córdova
¿Entonces no está de acuerdo con @pdr y por lo tanto encuentra la primera definición de ISP (ver mi publicación original) más agradable?
EdvRusj
@EdvRusj Creo que ambos tienen razón. Pero el segundo agrega confusión innecesaria al usar la metáfora cliente / servidor. Si tengo que elegir uno, iría con el oficial Gang of Four one, que es el primero. Pero lo importante es reducir el acoplamiento y las dependencias innecesarias, que es el espíritu de los principios SÓLIDOS después de todo. No importa cuál sea el correcto. Lo importante es que debe segregar las interfaces de acuerdo con los comportamientos. Eso es todo. Pero cuando tengas dudas, solo ve a la fuente original.
Tulains Córdova
3
Estoy muy en desacuerdo con su afirmación de que "cliente" es implementador en SOLID talk. Por un lado, es una tontería lingüística llamar a un proveedor (implementador) un cliente de lo que está proporcionando (implementando). Tampoco he visto ningún artículo sobre SOLID que intente transmitir esto, pero puede que simplemente me haya perdido eso. Lo más importante es que configura el implementador de una interfaz como el que decide qué debe estar en la interfaz. Y eso no tiene sentido para mí. Los llamadores / usuarios de una interfaz definen lo que necesitan de una interfaz y los implementadores (plural) de esa interfaz están obligados a proporcionarla.
Marjan Venema
5

ISP se trata de aislar al cliente de saber más sobre el servicio de lo que necesita saber (protegerlo contra cambios no relacionados, por ejemplo). Su segunda definición es correcta. Para mi lectura, solo uno de esos tres artículos sugiere lo contrario ( el primero ) y es simplemente incorrecto. (Editar: No, no está mal, solo engañoso).

La primera definición está mucho más estrechamente vinculada al LSP.

pdr
fuente
3
En ISP, los clientes no deberían verse obligados a CONSUMIR componentes de interfaz que no usan. En LSP, los SERVICIOS no deberían verse obligados a implementar el método D porque el código de llamada requiere el método A. No son contradictorios, son complementarios.
pdr
2
@EdvRusj, el objeto que implementa la interfaz A que llama ClientA puede ser, de hecho, el mismo objeto que implementa la interfaz B que necesita el cliente B. En los raros casos en que el mismo cliente necesita ver el mismo objeto que diferentes clases, el código no por lo general "tocar". Lo verás como una A para un propósito y una B para el otro propósito.
Amy Blankenship
1
@EdvRusj: Podría ayudar si reconsidera su definición de interfaz aquí. No siempre es una interfaz en términos de C # / Java. Podría tener un servicio complejo con una serie de clases simples, de modo que el cliente A use la clase de contenedor AX para "interactuar" con el servicio X. Por lo tanto, cuando cambia X de una manera que afecta a A y AX, no está obligado a afectar BX y B.
pdr
1
@EdvRusj: Sería más exacto decir que a A y B no les importa si ambos llaman a X o uno llama a Y y el otro llama a Z. ESO es el punto fundamental del ISP. Por lo tanto, puede elegir la implementación que elija y cambiar de opinión fácilmente más adelante. ISP no favorece una ruta u otra, pero LSP y SRP podrían.
pdr
1
@EdvRusj No, el cliente A podría reemplazar el Servicio X con el Servicio y, ambos implementarían la interfaz AX. X y / o Y pueden implementar otras interfaces, pero cuando el cliente los llama como AX, no le importan esas otras interfaces.
Amy Blankenship