Due definizioni contraddittorie del principio di segregazione dell'interfaccia: qual è quella corretta?


14

Quando si leggono articoli su ISP, sembrano esserci due definizioni contraddittorie di ISP:

Secondo la prima definizione (vedi 1 , 2 , 3 ), l'ISP afferma che le classi che implementano l'interfaccia non dovrebbero essere costrette a implementare funzionalità di cui non hanno bisogno. Quindi, interfaccia fatIFat

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

class MyClass: IFat
{ ... }

dovrebbe essere suddiviso in interfacce più piccole ISmall_1eISmall_2

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

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

class MyClass:ISmall_2
{ ... }

poiché in questo modo my MyClassè in grado di implementare solo i metodi di cui ha bisogno ( D()e C()), senza essere costretto a fornire anche implementazioni fittizie per A(), B()e C():

Ma secondo la seconda definizione (vedi 1 , 2 , risposta di Nazar Merza ), l'ISP afferma che MyClientchiamare i metodi su MyServicenon dovrebbe essere consapevole dei metodi su MyServicecui non ha bisogno. In altre parole, se MyClientsolo ha bisogno della funzionalità di C()e D(), quindi invece di

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

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

dovremmo separare i MyService'smetodi in interfacce specifiche 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();

Pertanto, con la prima definizione, l'obiettivo dell'ISP è " semplificare la vita delle classi che implementano l'interfaccia IFat ", mentre con quest'ultima l'obiettivo dell'ISP è " semplificare la vita dei clienti che chiamano i metodi di MyService ".

Quale delle due diverse definizioni di ISP è effettivamente corretta?

@MARJAN VENEMA

1.

Quindi, quando hai intenzione di dividere IFat in un'interfaccia più piccola, quali metodi finiscono in cui ISmallinterface dovrebbe essere deciso in base alla coesione dei membri.

Mentre ha senso mettere metodi coerenti all'interno della stessa interfaccia, ho pensato che con il modello ISP le esigenze del cliente abbiano la precedenza sulla "coesione" di un'interfaccia. In altre parole, ho pensato che con l'ISP dovremmo raggruppare all'interno della stessa interfaccia quei metodi richiesti da determinati clienti, anche se ciò significa lasciare fuori da quella interfaccia quei metodi che dovrebbero, per motivi di coesione, essere inseriti all'interno di quella stessa interfaccia?

Così, se c'erano un sacco di clienti che saranno sempre e solo bisogno di chiamare CutGreens, ma non anche GrillMeat, poi per aderire al modello ISP dobbiamo solo mettere CutGreensdentro ICook, ma non anche GrillMeat, anche se i due metodi sono altamente coesivo ?!

2.

Penso che la tua confusione derivi da un presupposto nascosto nella prima definizione: che le classi attuatrici stanno già seguendo il principio della responsabilità unica.

"Implementando classi che non seguono SRP" ti riferisci a quelle classi che implementano IFato alle classi che implementano ISmall_1/ ISmall_2? Presumo che ti riferisci alle classi che implementano IFat? In tal caso, perché pensi che non seguano già SRP?

Grazie


4
Perché non ci possono essere più definizioni entrambe servite dallo stesso principio?
Bobson,

5
Queste definizioni non si contraddicono a vicenda.
Mike Partridge,

1
Ovviamente, il bisogno del cliente non ha la precedenza sulla coesione di un'interfaccia. Puoi portare questa "regola" molto lontano e finire con interfacce a metodo singolo in tutto il luogo che non hanno assolutamente alcun senso. Smetti di seguire le regole e inizia a pensare agli obiettivi per i quali queste regole sono state create. Con "classi che non seguono SRP" non stavo parlando di classi specifiche nel tuo esempio o che non stessero già seguendo SRP. Leggi ancora. La prima definizione porta a dividere un'interfaccia solo se l'interfaccia non segue l'ISP e la classe segue l'SRP.
Marjan Venema,

2
La seconda definizione non si preoccupa degli implementatori. Definisce le interfacce dal punto di vista dei chiamanti e non fa alcuna ipotesi sull'esistenza o meno di implementatori. Probabilmente si presume che quando si segue l'ISP e si arriva all'implementazione di quelle interfacce, si dovrebbe ovviamente seguire SRP durante la loro creazione.
Marjan Venema,

2
Come fai a sapere in anticipo quali clienti esisteranno e quali metodi avranno bisogno? Non puoi. Quello che puoi sapere prima è quanto sia coerente la tua interfaccia.
Tulains Córdova,

Risposte:


6

Sono corretti entrambi

Il modo in cui l'ho letto, lo scopo dell'ISP (Interface Segregation Principle) è di mantenere le interfacce piccole e focalizzate: tutti i membri dell'interfaccia dovrebbero avere una coesione molto elevata. Entrambe le definizioni hanno lo scopo di evitare interfacce "jack-of-all-trades-master-of-none".

La segregazione dell'interfaccia e SRP (Single Responsibility Principle) hanno lo stesso obiettivo: garantire componenti software piccoli e altamente coerenti. Si completano a vicenda. La segregazione delle interfacce assicura che le interfacce siano piccole, mirate e altamente coesive. Seguire il principio della responsabilità unica garantisce che le classi siano piccole, mirate e altamente coerenti.

La prima definizione menzionata si concentra sugli implementatori, la seconda sui client. Che, contrariamente a @ user61852, prendo per essere gli utenti / chiamanti dell'interfaccia, non gli implementatori.

Penso che la tua confusione derivi da un presupposto nascosto nella prima definizione: che le classi attuatrici stanno già seguendo il principio della responsabilità unica.

Per me la seconda definizione, con i clienti come chiamanti dell'interfaccia, è un modo migliore per raggiungere l'obiettivo previsto.

segregante

Nella tua domanda dichiari:

in questo modo MyClass è in grado di implementare solo i metodi di cui ha bisogno (D () e C ()), senza essere costretto a fornire anche implementazioni fittizie per A (), B () e C ():

Ma questo sta capovolgendo il mondo.

  • Una classe che implementa un'interfaccia non detta ciò di cui ha bisogno nell'interfaccia che sta implementando.
  • Le interfacce determinano quali metodi dovrebbe fornire una classe di implementazione.
  • I chiamanti di un'interfaccia sono davvero quelli che dettano quale funzionalità hanno bisogno dell'interfaccia per fornire loro e quindi ciò che un implementatore dovrebbe fornire.

Quindi, quando hai intenzione di dividere IFatin un'interfaccia più piccola, quali metodi finiscono in quale ISmallinterfaccia dovrebbe essere decisa in base alla coesione dei membri.

Considera questa interfaccia:

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

Quali metodi inseriresti ICooke perché? Metteresti CleanSinkinsieme GrillMeatsolo perché ti capita di avere una classe che fa proprio questo e un paio di altre cose, ma niente di simile a nessuno degli altri metodi? O lo divideresti in altre due interfacce coerenti, come:

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

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

Nota sulla dichiarazione dell'interfaccia

La definizione di un'interfaccia dovrebbe preferibilmente essere autonoma in un'unità separata, ma se deve assolutamente convivere con il chiamante o l'implementatore, dovrebbe davvero essere con il chiamante. In caso contrario, il chiamante ottiene una dipendenza immediata dall'implementatore che sta completamente annullando lo scopo delle interfacce. Vedi anche: Dichiarare l'interfaccia nello stesso file della classe base, è una buona pratica? sui programmatori e perché dovremmo collocare interfacce con le classi che le usano piuttosto che con quelle che le implementano? su StackOverflow.


1
Riesci a vedere l'aggiornamento che ho fatto?
EdvRusj,

"il chiamante ottiene una dipendenza immediata dall'implementatore " ... solo se si viola DIP (principio di inversione di dipendenza), se le variabili interne del chiamante, i parametri, i valori di ritorno, ecc. sono di tipo ICookanziché di tipo SomeCookImplementor, come impone DIP, allora non non devi dipendere SomeCookImplementor.
Tulains Córdova,

@ user61852: se la dichiarazione dell'interfaccia e l'implementatore si trovano nella stessa unità, ottengo immediatamente una dipendenza da tale implementatore. Non necessariamente in fase di esecuzione, ma sicuramente a livello di progetto, semplicemente per il fatto che è lì. Il progetto non può più essere compilato senza di esso o qualunque cosa utilizzi. Inoltre, l'iniezione di dipendenza non è la stessa del principio di inversione di dipendenza. Potresti essere interessato a DIP in the wild
Marjan Venema il

Ho riutilizzato i tuoi esempi di codice in questa domanda programmers.stackexchange.com/a/271142/61852 , migliorandolo dopo che era già stato accettato. Ti ho dato il dovuto credito per gli esempi.
Tulains Córdova,

Cool @ user61852 :) (e grazie per il merito)
Marjan Venema,

14

Si confonde la parola "client" come utilizzata nella banda di quattro documenti con un "client" come nel consumatore di un servizio.

Un "client", come inteso da Gang of Four definizioni, è una classe che implementa un'interfaccia. Se la classe A implementa l'interfaccia B, allora dicono che A è un client di B. Altrimenti la frase "i client non dovrebbero essere costretti a implementare interfacce che non usano" non avrebbe senso dal momento che "i clienti" (come nei consumatori) don non implementare nulla. La frase ha senso solo quando vedi "client" come "implementatore".

Se "client" intendesse una classe che "consuma" (chiama) i metodi di un'altra classe che implementa la grande interfaccia, allora chiamando i due metodi che ti interessano e ignorando il resto, sarebbe sufficiente per tenerti disaccoppiato dal resto di i metodi che non usi.

Lo spirito del principio è evitare che il "client" (la classe che implementa l'interfaccia) debba implementare metodi fittizi per conformarsi all'intera interfaccia quando si preoccupa solo di una serie di metodi correlati.

Inoltre mira ad avere il minor numero possibile di accoppiamenti in modo tale che le modifiche apportate in un punto causino il minor impatto. Separando le interfacce si riduce l'accoppiamento.

Questi problemi si presentano quando l'interfaccia fa troppo e ha metodi che dovrebbero essere divisi in più interfacce anziché in una sola.

Entrambi gli esempi di codice sono OK . È solo che nella seconda si assume che "client" significhi "una classe che consuma / chiama i servizi / metodi offerti da un'altra classe".

Non trovo contraddizioni nei concetti spiegati nei tre collegamenti che hai dato.

Mantieni chiaro che "client" è implementatore , in SOLID talk.


Ma secondo @pdr, mentre gli esempi di codice in tutti i collegamenti aderiscono all'ISP, la definizione dell'ISP riguarda più "l'isolamento del client (una classe che chiama metodi di un'altra classe) dal sapere di più sul servizio" piuttosto che " prevenzione da parte dei client (implementatori) costretti a implementare interfacce che non utilizzano. "
EdvRusj,

1
@EdvRusj La mia risposta si basa sui documenti nel sito web di Object Mentor (Bob Martin enterprise), scritto dallo stesso Martin quando era nella famosa Gang of Four. Come sapete, il Gnag of Four era un gruppo di ingegneri del software, tra cui Martin, che coniò l'acronimo SOLID, identificò e documentò i principi. docs.google.com/a/cleancoder.com/file/d/…
Tulains Córdova

Quindi non sei d'accordo con @pdr e quindi trovi più gradevole la prima definizione di ISP (vedi il mio post originale)?
EdvRusj,

@EdvRusj Penso che entrambi abbiano ragione. Ma il secondo aggiunge confusione non necessaria utilizzando la metafora client / server. Se devo sceglierne uno, andrei con la banda ufficiale di Four one, che è la prima. Ma ciò che è importante per ridurre l'accoppiamento e le dipendenze non necessarie è lo spirito dei principi SOLID dopo tutto. Non importa quale sia giusto. La cosa importante è che devi separare le interfacce in base ai comportamenti. È tutto. Ma in caso di dubbio, basta andare alla fonte originale.
Tulains Córdova,

3
Sono così in disaccordo con la tua affermazione che "client" è implementatore nel discorso SOLID. Per uno, è una sciocchezza linguistica chiamare un fornitore (implementatore) un cliente di ciò che sta fornendo (implementando). Inoltre non ho visto alcun articolo su SOLID che provi a comunicare questo, ma potrei semplicemente averlo perso. Soprattutto, però, imposta l'implementatore di un'interfaccia come quella che decide cosa dovrebbe essere nell'interfaccia. E questo non ha senso per me. I chiamanti / utenti di un'interfaccia definiscono ciò di cui hanno bisogno da un'interfaccia e gli implementatori (plurale) di tale interfaccia sono tenuti a fornirla.
Marjan Venema,

5

L'ISP consiste nell'isolare il client dalla conoscenza più del servizio di quanto non ne abbia bisogno (proteggendolo dalle modifiche non correlate, ad esempio). La tua seconda definizione è corretta. Secondo la mia lettura, solo uno di questi tre articoli suggerisce il contrario ( il primo ) ed è semplicemente sbagliato. (Modifica: No, non sbagliato, solo fuorviante.)

La prima definizione è molto più strettamente legata a LSP.


3
In ISP, i client non dovrebbero essere costretti a CONSUMARE componenti di interfaccia che non usano. In LSP, i SERVIZI non dovrebbero essere costretti a implementare il metodo D perché il codice chiamante richiede il metodo A. Non sono contraddittori, sono complementari.
pdr

2
@EdvRusj, l'oggetto che implementa InterfaceA che ClientA chiama potrebbe infatti essere esattamente lo stesso oggetto che implementa InterfaceB richiesto dal Client B. Nei rari casi in cui lo stesso client deve vedere lo stesso oggetto di Classi diverse, il codice non verrà di solito "tocca". Lo vedrai come una A per uno scopo e una B per l'altro scopo.
Amy Blankenship,

1
@EdvRusj: potrebbe essere utile ripensare la definizione di interfaccia qui. Non è sempre un'interfaccia in termini di C # / Java. Potresti avere un servizio complesso con un numero di classi semplici racchiuse attorno ad esso, in modo tale che il client A usi la classe wrapper AX per "interfacciarsi" con il servizio X. Pertanto, quando cambi X in un modo che influenza A e AX, non lo sei costretto a influenzare BX e B.
pdr

1
@EdvRusj: Sarebbe più accurato dire che A e B non si preoccupano se entrambi chiamano X o uno chiama Y e l'altro chiama Z. Questo è il punto fondamentale dell'ISP. Quindi puoi scegliere quale implementazione scegliere e cambiare idea in un secondo momento. L'ISP non favorisce una rotta o l'altra, ma LSP e SRP potrebbero.
pdr

1
@EdvRusj No, il client A sarebbe in grado di sostituire il servizio X con il servizio y, entrambi implementerebbero l'interfaccia AX. X e / o Y possono implementare altre interfacce, ma quando il Cliente le chiama come AX, non si preoccupa di quelle altre interfacce.
Amy Blankenship,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.