Accoppiamento di implementazione

2

Questa domanda riguarda le migliori pratiche dell'interfaccia Segregation Principle. Uso esempi astratti di seguito, ma la domanda nasce dal codice reale che ho visto che esegue ciò che posso solo chiamare "Accoppiamento implementazione".

Se dovessi rompere la mia classe di MilkMan nei suoi rispettivi ruoli, vedrei che MilkMan ha tre ruoli. Lui interpreta il ruolo di Person (cammina e mastica il bubblegum), interpreta il ruolo di Deliverer (funzioni di inventario e contabilità?) E colpisce mia moglie quando non sono a casa.

Quindi quando scrivo / rivedo la mia logica di contabilità (o Unit Testing it), vedo che alcuni metodi nella mia classe contabile possono prendere un parametro MilkMan . Quando vedo questo, ovviamente ho qualcosa di sbagliato. L'unica cosa di cui si preoccupa la mia classe contabile (o Test di unità di questo tipo) è il ruolo di Deliverer.

Quindi quello che faccio è identificare i ruoli del mio MilkMan e segregare alcune interfacce:

  • IPerson
  • IDeliverer
  • ICovetThyW

Ora, quando la mia classe contabile (oi suoi Test unitari) devono prendere in giro o passare in giro la funzionalità di Deliverer, le relazioni sono chiare. Ora posso avere il mio MailMan , UpsMan e PoolBoy parlare con la mia logica Contabilità passando chiaramente attorno a IDeliverer .

Quindi ora Unit Testing è molto più facile da prendere in giro e le relazioni diventano più chiare nel mio codice. Tutto è fantastico.

Cosa succede se un metodo in un'implementazione sta effettuando chiamate a un altro metodo nella stessa classe, ma che fa parte di un'altra implementazione? Prendi in considerazione il seguente codice:

namespace HomeAccounting.KeepAnEyeOnPayments.Sample
{
    class Program
    {
        static void Main(string[] args)
        {
            IDeliverer jose = new MilkMan();
            double paymentAmount = 69;
            if (jose.AcceptPaymentFromCustomer(paymentAmount))
            {
                System.Console.WriteLine("He got " + paymentAmount.ToString());
            }
        }
    }

    public class MilkMan : IPerson, IDeliverer, ICovetThyW
    {
        public bool CanWalk { get; set; }
        public bool CanChewBubblegum { get; set; }

        public bool AcceptPaymentFromCustomer(double amountDue)
        {
            if (GetPaymentFromCustomer(amountDue) == amountDue)
            {
                return true;
            }
            else
            {
                if (AlternativePaymentMethod(amountDue) == amountDue)
                {
                    return true;
                }
            }
            return false;
        }

        private double GetPaymentFromCustomer(double amountDue)
        {
            return 0; //Not willing and/or able to give money.
        }

        public double AlternativePaymentMethod(double amountDue)
        {
            return amountDue;
        }

    }

    public interface IPerson
    {
        bool CanWalk { get; set; }
        bool CanChewBubblegum { get; set; }
    }

    public interface IDeliverer
    {
        bool AcceptPaymentFromCustomer(double amountDue);
    }

    public interface ICovetThyW
    {
        double AlternativePaymentMethod(double amountDue);
    }
}

La domanda è : qualcuno vede qualcosa di sbagliato in questo? Ma in tutta serietà: mi piacerebbe sapere se c'è un termine o una discussione o una best practice elencati là fuori che dice che non dovresti accoppiare le tue implementazioni insieme.

Voglio sottolineare che l'implementazione esplicita dell'interfaccia (che spingo per) non rende nemmeno questo possibile. Ma onestamente non l'ho mai visto in un ambiente professionale da 15 anni.

    
posta Suamere 15.10.2014 - 21:36
fonte

2 risposte

1

In realtà penso che questo sia uno dei maggiori vantaggi di poter avere una classe, che implementa più interfacce, pur avendo una "implementazione". Penso che il problema sia con il tuo esempio, piuttosto che con l'idea. Ricordo di aver fatto una soluzione simile qualche tempo fa.

Fondamentalmente, ho creato un comportamento, che ha funzionato e ha richiesto un po 'di lavoro prima e dopo sugli stessi file in una singola cartella. Per fare questo, ho creato due interfacce: IBeforeWork e IAfterWork . Il, c'era una classe, che assomigliava a questo:

public class FileChecker : IBeforeWork, IAfterWork
{
    private string directory;
    private string checksum;

    public FileChecker(string dir)
    {
        this.directory = dir;
    }

    void IBeforeWork.DoWork()
    {
        // calculate checksum from files in directory
    }

    void IAfterWork.DoWork()
    {
        // check checksum on files in directory
    }
}

Quindi, ho collegato un'istanza di questa classe in entrambe le posizioni e ho ottenuto un checksum perfettamente disaccoppiato per verificare i file nella directory.

    
risposta data 19.11.2014 - 08:58
fonte
0

Does anybody see anything wrong with this? ...

Penso di vedere un uso eccessivo di interface s perché ...

in all seriousness

Quando leggo questo: Cosa succede se un metodo in una implementazione sta effettuando chiamate a un altro metodo nella stessa classe, ma che fa parte di un'altra implementazione? l'odore del codice è chiaro.

Vedo che una situazione di ereditarietà naturale è corrotta dall'errata applicazione del principio di segregazione dell'interfaccia. Stai disconnettendo ciò che un oggetto è da ciò che fa tramite ISP. Il comportamento di "consegna" è un'estensione naturale di ciò che è l'oggetto. Un interface è usato per dare un comportamento comune alle classi non correlate. Questa non è la tua situazione.

Ridisegna per ereditare il comportamento di consegna

Mi sembra che tu abbia un sistema di consegna / pagamento del cliente. Un processo che può essere incorporato in una classe base. Questo esprime meglio, riflette e simula il tuo mondo reale. Con più implementazioni della stessa cosa stai chiedendo mal di testa di manutenzione.

I see that some methods in my accounting class(es) can take a MilkMan parameter. When I see this, I obviously have something wrong.

... a causa del design che stai cercando di forzare. Se invece MilkMan eredita DeliveryPerson eredita Person , allora quel parametro Milkman non ha bisogno di casting e fa solo la sua consegna. E allo stesso modo per qualsiasi sottoclasse DeliveryPerson .

Funzionalità di contabilità separata

Il lattaio consegna il latte e poi usa il processo di pagamento da raccogliere. Considera di rendere CustomerPayment una classe separata - questo è ciò che sei veramente dopo aver sospettato. Innanzitutto, come classe di appartenenza, modelliamo più esplicitamente questo particolare processo contabile. In secondo luogo, se DeliveryPerson ha un qualche tipo di account, o libro mastro o libro delle vendite o ... allora il processo CustomerPayment (classe) lo utilizza semplicemente.

O forse ogni cliente ha un account separato; ma ancora la classe CustomerPayment lo usa. Forse entrambi - aggiorna le vendite della persona di consegna e l'account cliente.

Sto immaginando che DeliveryPerson abbia un riferimento alla classe CustomerPayment (cioè il lattaio tiene un dispositivo portatile per elaborare l'ordine dei clienti). E il lattaio preme i pulsanti sul palmare (chiama i metodi CustomerPayment ) per elaborare l'ordine.

Altri punti

Milkman viola il principio di responsabilità singola. Non ha affari con l'implementazione dell'elaborazione dell'account.

Il fatto che stai duplicando questo comportamento contabile è un odore di codice e puzza molto di più dei problemi di separazione dell'interfaccia.

L'accoppiamento indesiderato di cui dovresti preoccuparti è con l'interfaccia utente e il database. Questo è l'accoppiamento che impedisce il test dell'unità.

Una buona separazione delle responsabilità di classe, in primo luogo, dovrebbe consentire al setup dell'unità di test di costruire facilmente qualsiasi oggetto composito. Quindi il mocking diventa meno un problema.

Il principio di segregazione delle interfacce non dice "favorire molte interfacce itty-bitty rispetto all'ereditarietà". Sarò il primo a dire che il test unitario aiuta a perfezionare il mio design ma a gridare ad alta voce, caro lettore - design prima.

Altre considerazioni sull'uso eccessivo delle interfacce

    
risposta data 19.10.2014 - 23:23
fonte

Leggi altre domande sui tag