Devo rifattare le mie classi astratte nelle classi di Helper? Uso reale di classi astratte e parole chiave protette?

1

Gran parte del mio codice sembra attraversare la seguente evoluzione -

Iterazione 1

interface IAccountManager {
    void Import(string importData);
}
class SalesAccountManager : IAccountManager {
    private int GetAccountId(string data) { 
        //Some data conversions, database references, calculations etc
        return accountId;
    }
    private int GetAccountName(string data) { 
        //Some data conversions, database references, calculations etc
        return name;
    }
    public void Import(string importData) {
        SalesAccount a = new SalesAccount();
        a.Id = GetAccountId(importData.Substring(11, 55); //hardcoding just to keep this example simple
        a.Name = GetAccountName(importData);
        a.Save();
    }
}

Funziona tutto bene, tutti sono contenti ma poi trovo che devo aggiungere un BusinessAccountManager . Dopo un'ulteriore ispezione, sembra che i metodi GetAccountId e GetAccountName non cambieranno. Quindi, ciò che sembra una soluzione naturale? Creiamo solo una classe base astratta che eredita da IAccountManager e rendiamo questi metodi protetti e rendiamo Import astratto. Ora, abbiamo -

Iterazione 2

abstract class AbstractAccountManage : IAccountManager {
    protected int GetAccountId(string data) {...No change here...}
    protected int GetAccountName(string data) {...No change here...}
    public abstract void Import(string importData);
}

class SalesAccountManager : AbstractAccountManager {
    public void Import(string importData) {...No change here...}
}
class BusinessAccountManager : AbstractAccountManager {
    public void Import(string importData) {
        BusinessAccount a = new BusinessAccount();
        a.Id = GetAccountId(importData.Substring(11, 55); //hardcoding just to keep this example simple
        a.Name = GetAccountName(importData);
        a.Save();
    }
}

Ma sembra che questa classe astratta sia fondamentalmente una classe di supporto, quindi avrei potuto fare quanto segue:

Iteration 3

interface IAccountManagerStrategy {
    public int GetAccountId(string data);
    public int GetAccountName(string data);
}
class AccountManagerStrategy : IAccountManagerStrategy {
    public int GetAccountId(string data) {...No change here...}
    public int GetAccountName(string data) {...No change here...}
}
interface IAccountManager {
    void Import(string importData);
}

class SalesAccountManager : IAccountManager {
    private IAccountManagerStrategy accountManagerStrategy {get;}
    public SalesAccountManager(IAccountManagerStrategy accountManagerStrategy) {
        this.accountManagerStrategy = accountManagerStrategy;
    }
    public void Import(string importData) {
        SalesAccount a = new SalesAccount();
        a.Id = accountManagerStrategy.GetAccountId(importData.Substring(11, 55); //hardcoding just to keep this example simple
        a.Name = accountManagerStrategy.GetAccountName(importData);
        a.Save();
    }
}

class BusinessAccountManager : AbstractAccountManager {
    //Similar to above but for a BusinessAccount        
}

Nota: l'implementazione dei metodi GetAccountId e GetAccountName non è molto probabile che cambi (ma non si sa mai).

Con lo sfondo sopra -

Domande

  1. Da un punto di vista dell'architettura software, è un buon approccio per refactoring tali classi astratte che fungono da repository di metodi privati condivisi (simili a quelli precedenti) in classi helper (o pattern di strategia, se vogliamo chiamarlo così ) come visto in Iteration 3? Questa domanda non è per questo caso specifico ma in generale.

  2. Se è un buon approccio per fare questo refactoring, allora qual è l'uso reale delle classi astratte e la parola chiave protected per quanto riguarda il design / architettura del software?

  3. Una classe astratta come AbstractAccountManager in precedenza violerebbe SRP dopo che i metodi sono stati cambiati da privato a protetto (suppongo si possa sostenere che non fa più solo una cosa, ma è difficile dire quando fermarsi identificando quale dovrebbe essere una singola responsabilità)?

Nota: questa domanda non riguarda la composizione rispetto al polimorfismo. Si tratta di capire l'uso corretto di abstract e capire perché o perché non refactoring i metodi concreti di una classe astratta in classi helper / utility.

    
posta Achilles 04.02.2017 - 22:16
fonte

1 risposta

2

From a software architecture perspective, is it a good approach to refactor such abstract classes that act as repositories of shared private methods (similar to above) into helper classes (or strategy pattern, if we want to call it that) as seen in Iteration 3? This question is not for this specific case but in general.

In generale questa è una buona idea. Entrambe le classi di helper e una classe base astratta assumono il fardello di sapere come implementare un certo pacchetto di funzionalità da una classe, e quindi ci permettono di consolidare la funzionalità condivisa. Il vantaggio di una classe helper è che ci permette di cambiare quella funzionalità in modo più dinamico e indipendente, possiamo sostituire l'helper con un mock / stub durante il test, iniettare diverse implementazioni dell'helper e cambiare in modo indipendente due differenti dipendenze dall'helper senza cambiare le loro interfacce.

Una classe astratta ha il vantaggio della convenienza: richiede meno tasti per delegare le funzionalità a una classe base rispetto a una classe helper. Questo non è un vantaggio significativo. E quando la classe base astratta fornisce solo alcuni metodi privati, i suoi principali vantaggi, che servono essenzialmente come modello o trasformazione dall'interfaccia all'interfaccia, non vengono sfruttati. Quindi penso che nel complesso questa sia una solida idea.

If it is a good approach to do that refactoring, then what is the real use of abstract classes and the protected keyword as far as software design/architecture is concerned?

Ci sono spesso casi in cui alcune funzionalità possono essere definite come wrapper attorno ad un insieme più piccolo di funzionalità. Ci sono alcune sottocasi di questo:

  1. Controlla le operazioni non sicure. Supponiamo che io abbia uno stack che dovrebbe lanciare un IllegalOperationException quando è vuoto e viene chiamato pop. Posso usare una classe astratta per astrarre questi controlli di sicurezza, assicurando così che le classi dei bambini non possano dimenticare di implementarle. Questo può apparire come

    public abstract class SafeStack<T> {
        protected abstract T UnsafePop();
        protected abstract int Size { get; }
    
        public T Pop() {
            if (this.Size <= 0) {
                throw new IllegalOperationException("Cannot pop an empty stack.");
            }
            return this.UnsafePop();
        }
    }
    
  2. "Classi di modelli". A volte ci si imbatte in casi in cui si dispone di un certo numero di classi che implementano alcune funzionalità comuni con la stessa ricetta. Quindi potrei avere

    public class ExcelReportGenerator {
        private void SaveToExcelFile(Summary summary){}
    
        public void SaveReport(ReportData data) {
            CleanReportData cleaned = data.Clean();
            Summary summary = cleaned.Summary();
            SaveToExcelFile(summary);
        }
    }
    
    public class HtmlReportGenerator {
        private void SaveToHtmlFile(Summary summary){}
    
        public void SaveReport(ReportData data) {
            CleanReportData cleaned = data.Clean();
            Summary summary = cleaned.Summary();
            SaveToHtmlFile(summary);
        }
    }
    

    Puoi rimuovere la duplicazione creando un ReportGenerator astratto con un metodo protected abstract SaveToFile(Summary) .

  3. Un sacco di delega. Supponiamo che tu abbia classi che implementano alcuni ComplicatedMethod(ComplicatedParameters parameters) dove i parametri hanno molti parametri opzionali / valori predefiniti ragionevoli. Se si desidera definire metodi che delegano a ComplicatedMethod(ComplicatedParameters) con vari valori predefiniti impostati, una classe base astratta con abstract ComplicatedMethod(ComplicatedParameters) con i metodi di delega definiti è un modo abbastanza ragionevole per ottenere ciò.

Ora, non devi usare classi astratte per nessuno di questi. È sempre possibile simulare una classe astratta definendo un'interfaccia per la classe figlio e prendendo quell'interfaccia come parametro costruttore. E nell'ultimo decennio circa i linguaggi mainstream hanno definito altri buoni modi per realizzare la stessa cosa, come classi generiche, metodi di estensione o valori predefiniti dell'interfaccia.

Penso che ci siano ancora casi in cui stai cercando di ottenere un mix dei precedenti tre in cui le finezze dell'utilizzo di una classe base astratta possono sopraffare i lati negativi dell'ereditarietà per renderla utile.

Would an abstract class like AbstractAccountManager above be violating SRP after the methods were changed from private to protected (I suppose it can be argued that its no longer doing only one thing, but difficult to say when to stop identifying what a single responsibility should be)?

Questo è un pensiero interessante e forse? Come noti, la "responsabilità" è una cosa difficile da capire veramente, ma in generale, passando da private a protected si aggiunge alla tua interfaccia. Una classe non sigillata ha tre interfacce: quella che mostra al mondo ( public ) quella che mostra ai suoi figli ( public e protected ) e quella che mostra ai suoi overrider ( virtual ). Se pensi che la responsabilità sia legata all'interfaccia di una classe, che è giusta, allora l'aggiunta di un membro protetto sta aumentando la responsabilità.

Bonus, domanda non richiesta. Mi sembra che il tuo esempio stia piangendo per un'implementazione generica. Qualcosa come

class AccountManager<TAccount> : IAccountManager where T : IAccount, new() {
    private int GetAccountId(string data) { 
        //Some data conversions, database references, calculations etc
        return accountId;
    }
    private int GetAccountName(string data) { 
        //Some data conversions, database references, calculations etc
        return name;
    }
    public void Import(string importData) {
        TAccount a = new TAccount();
        a.Id = GetAccountId(importData.Substring(11, 55); //hardcoding just to keep this example simple
        a.Name = GetAccountName(importData);
        a.Save();
    }
}
    
risposta data 05.02.2017 - 00:55
fonte

Leggi altre domande sui tag