Difficoltà di progettazione orientata agli oggetti a 3 livelli?

4

Sto seguendo la formazione al mio nuovo lavoro per utilizzare un buon design orientato agli oggetti con uno stile di programmazione a 3 livelli. Il mio supervisore dice che ho un problema di progettazione con il mio codice:

class Program
{
    public static void Main()
    {
        UserInputValidator Validater = new UserInputValidator();
        int logSelected = -2;
        Console.Write("\nFilepath: ");
        string filePath = Console.ReadLine();
        while (logSelected != -1)
        {
            Console.WriteLine("\n" + "SELECT AN OPTION MENU");
            Console.WriteLine("-----------------------");
            Console.Write("\n" +
            /* Display option menu here with several options */
            "9 ) Exit\n\n");
            Console.WriteLine("-----------------------");
            logSelected = Convert.ToInt32(Console.ReadLine());
            Validater.ValidateOptionsMenu(logSelected, filePath);
        }
    }

    public void DisplayMessage(string message)
    {
        Console.WriteLine(message);
    }
}

Validator.ValidateOptionsMenu () gestisce la logica per l'avvio di un processo a seconda dell'opzione selezionata.

Questa è un'applicazione a 3 livelli con livello di presentazione, business e accesso ai dati. Il programma si trova nel livello Presentazione e gli altri livelli utilizzano un codice simile al seguente per visualizzare un messaggio:

Program program = new Program();
program.DisplayMessage(message);

Ho bisogno di capire perché questa è una pratica sbagliata - il mio supervisore dice che il programma di classe avvia solo l'applicazione, come la classe App in WPF. Deve avere solo una responsabilità. L'interazione con l'utente (il mio menu delle opzioni) è delegata a un'altra classe. Sto avendo difficoltà a capire il mio supervisore e non sono sicuro di come risolverlo. Qualcuno può aiutarmi a capire?

    
posta Guillermo_GB 29.04.2011 - 17:51
fonte

6 risposte

9

È difficile da vedere con questo esempio perché il programma è quasi del tutto banale, tuttavia il tuo supervisore sta rilevando il fatto che il metodo Main contiene la logica dell'applicazione core (cioè definisce come si comporta fondamentalmente l'applicazione).

Perché questa è una brutta cosa? Bene in questo esempio non è proprio come il programma è molto semplice (che è ciò che rende difficile da vedere), tuttavia in un'applicazione più complessa questo potrebbe causare un paio di problemi:

  1. In primo luogo Main potrebbe essere responsabile di un paio di attività aggiuntive come l'analisi degli argomenti e la gestione degli errori. È una buona idea assicurarsi che ogni "componente" (ad es. Metodo o classe) sia responsabile di una sola cosa: ciò renderebbe Main responsabile sia dell'analisi degli argomenti che del ciclo principale dell'applicazione.

  2. In secondo luogo (e ancora più importante) spesso è necessario estrapolare alcune funzionalità e riutilizzarle come parte di un'applicazione più grande (ad esempio un'applicazione Windows Form o forse un'applicazione Web).

Immagina di aver scritto il codice di cui sopra e ora devi prendere il "nucleo" principale dell'applicazione (un loop che accetta l'input di testo e quindi emette un menu di testo) e incorporarlo come parte di un'applicazione Windows Form separata. Invece di leggere dalla console la tua app ora deve accettare input da una casella di testo, e invece di scrivere sulla console ora ha bisogno di scrivere l'output in una seconda casella di testo.

Al momento hai alcuni problemi che ti impedirebbero di farlo senza modificare il codice che hai postato:

  • In primo luogo, la classe Program viene normalmente contrassegnata come internal e la classe Main spesso ha un input args e quindi potresti non essere nemmeno in grado di chiamare questo metodo (con input sensibili) da un altro assembly.
  • In secondo luogo, il codice viene inviato direttamente alla console, non fornendo alcun modo per fornire un meccanismo di output alternativo.
  • Infine il codice si legge anche direttamente dalla console, non fornendo alcun modo per accettare input da una fonte alternativa.

In effetti, per aiutare a capire questo, ti consiglio di provare questo come esercizio:

  • Primo tentativo di modificare il codice precedente per permetterti di riutilizzarlo in un'applicazione esterna.
  • Quindi, in un progetto Windows Form separato, aggiungi un riferimento alla tua applicazione console e prova a riutilizzare questo codice in modo che l'utente interagisca attraverso le caselle di testo anziché la console.

L'obiettivo finale è che sia l'applicazione Windows Forms sia l'applicazione Console funzionino utilizzando lo stesso codice .

Questo esercizio dovrebbe aiutare a evidenziare e spiegare alcuni dei precedenti.

    
risposta data 29.04.2011 - 18:26
fonte
2

C'è una quantità di informazioni che ti mancano. Il tuo livello di interfaccia utente è semplicemente l'oggetto Console. Il livello dell'interfaccia utente è il mediatore tra l'utente e il computer. Con Console, hai solo l'abilità di scrivere cose sulla console e leggere l'input dalla console. Quindi il tuo livello di interfaccia utente è molto semplice. L'applicazione, tuttavia, necessita anche di un controller.

Il controller, che spesso non è menzionato nelle definizioni di livello N, è responsabile di dire all'interfaccia utente quando e cosa disegnare. Nel tuo caso, Program è il controller. Nelle applicazioni più complicate il livello UI dirà al controller quando l'utente ha richiesto un qualche tipo di operazione (ad esempio, aggiorna queste informazioni, mostrami il widget B, ecc.). L'applicazione non può non farlo perché il tuo livello di interfaccia utente è la console. Richiede informazioni dallo strato dell'interfaccia utente tramite Console.ReadLine. Quindi ora che abbiamo una buona definizione di cosa fa il livello dell'interfaccia utente e cosa fa il controller, usiamolo per spiegare come risolverlo.

Dopo che il controller riceve l'input, dovrebbe decidere quale operazione di livello inferiore chiamare e formattare l'input dall'interfaccia utente in qualcosa che il livello inferiore può utilizzare. Ad esempio chiamando Convert.ToInt32 in modo che la stringa dalla console possa essere passata a una funzione. Puoi prendere questo input formattato e chiamare il tuo livello aziendale.

Ora quando chiami qualcosa da un livello inferiore, di solito ti aspetti che vengano restituite alcune informazioni. Ad esempio un file di registro o un valore booleano per indicare il completamento. Il livello aziendale deve restituire queste informazioni in modo che si possa fare qualcosa con esso. Il livello aziendale non si cura di ciò che viene fatto con esso. Si preoccupa solo di restituire le informazioni corrette. Il controller sa cosa deve essere fatto con esso, cioè scrivere del testo sulla console. Quindi la fine di questa operazione si conclude con il controller che dice all'interfaccia utente come e cosa scrivere alla console, o disegnare sullo schermo o visualizzare in un browser web.

In conculsione, tutte le informazioni in questo concetto, meno gli esempi specifici della tua situazione, dovrebbero essere trasferite in qualsiasi applicazione tu scriva. Fondamentalmente il controller media tra l'interfaccia utente e il livello aziendale, e nella maggior parte dei casi quando qualcuno fa riferimento al livello dell'interfaccia utente, in realtà indicano il livello dell'interfaccia utente più questo controller.

Aggiornamento: È una cattiva pratica fare ciò che hai fatto perché diventa estremamente difficile mantenere il software dopo che è stato scritto. C'è solo una cosa coerente nello sviluppo del software, e cioè l'applicazione cambierà. Un pezzo di software viene sviluppato solo una volta, cioè una piccola porzione della sua vita. Passa il resto del suo tempo a funzionare e ad essere modificato per adattarsi alle nuove funzionalità. Fino a quando non è stato effettivamente necessario mantenere il codice che non segue questa architettura N-tier, è difficile comprendere appieno lo scopo della separazione delle preoccupazioni. La ragione più importante per separare queste cose è che puoi adattare il tuo software a cambiamenti imprevisti in un maniero più efficiente e affidabile rispetto a quando questi limiti non vengono rispettati.

    
risposta data 29.04.2011 - 19:43
fonte
1

Raccomando di leggere sui criteri da utilizzare per la decomposizione dei sistemi nei moduli di D.L. Parnas. È accessibile, interessante e pertinente alla domanda che stai ponendo. Potrebbe anche fornire la risposta che stai cercando.

    
risposta data 29.04.2011 - 18:25
fonte
1

Quando il supervisore dice che il programma dovrebbe avere una sola responsabilità, ciò che intende è questo:

  1. Program.Main () avvia l'applicazione e ottiene tutto in esecuzione. Questa è una responsabilità.
  2. Program.DisplayMessage () mostra i messaggi all'utente. Questa è un'altra responsabilità.

Quindi il tuo Program.Main () si trova nella sua categoria ... come un metodo di infrastruttura che consente agli altri livelli di iniziare a lavorare insieme. Ma Program.DisplayMessage () si trova nel livello Presentazione. Hai stretto l'infrastruttura della tua applicazione al livello di presentazione.

Inoltre, come altri stanno dicendo, la tua classe Validator è strettamente accoppiata alla tua classe di Programma. Se il menu delle opzioni cambia in Program.Main, sarà necessario modificare anche la classe Validator. Lo scopo del design orientato agli oggetti è di far sì che non sia necessario apportare modifiche in più di una classe alla volta.

    
risposta data 29.04.2011 - 18:26
fonte
0

UserInputValidater deve conoscere il menu delle opzioni. Invece, pensa a mettere in metodi distinti su Validater che dovrebbe essere chiamato. Quindi il Programma deciderà quale metodo chiamare su UserInputValidater piuttosto che decidere su UserInputValidater. UserInputValidater è strettamente collegato al tuo menu delle opzioni a questo punto.

Idealmente, Program.Main chiamerebbe una classe di presentazione. Quindi la classe di presentazione deciderebbe quale metodo su UserInputValidater eseguire.

class Program {     
   public static void Main()
   {
        Presenter presenter = new Presenter();
         presenter.Start();
    }
}

class Presenter()
{
     public void Start()
     {  
            // all your UI code

            switch(userInput)
             {
                  case 1:
                   validator.(whatever method would deal with case 1) etc...
 ...
                  case 9:
                    //exit here or throw an event that Progam has subscribed to for signal to exit.
       }

     public void DisplayMessage(string message)
     {
          ....
     }
}

(considera questo psuedo-code)

    
risposta data 29.04.2011 - 18:07
fonte
0

Scusa, devo correre così non c'è tempo per una spiegazione, ma qui c'è un codice generale che lo renderebbe più OOP ... buona fortuna!

class Program
{
private IUserMenuRepository _userMenuRepository;
private UserMenu _userMenu;
private UserMenuService _userMenuService;

public static void Main()
{
    _userMenuRepository = new UserMenuRepository();
    _userMenuService = new UserMenuService(userMenuRepository);
    _userMenu = _userMenuService.GetAllUserMenuOptions();

    string filePath = Console.ReadLine();
    while (userMenu.LogSelected != LogSelectedType.Exit)
    {   
        foreach(var option in userMenu.Option)
        {
            Console.WriteLine(option);
        }
        userMenu.LogSelected = (LogSelectedType)Console.ReadLine();
    }
}

public void DisplayMessage()
{
    Console.WriteLine(_userMenuService.DisplayMessage(_userMenu)));
}
}

// DOMAIN LAYER
// Base Model class for all domain objects to inherit
 public class ModelBase
 {
private IList<string> _brokenRules {get;set;}

public ModelBase()
{
    _brokenRules = new List<string>();
}

public void AddBrokenRule(string brokenRule)
{
    _brokenRules.Add(brokenRule);
}

public abstract CheckForBrokenRules();

public bool IsValid()
{
    return _brokenRules.Count == 0;
}
}
// Model
public class UserMenu : ModelBase
 {
public int Id {get;set;}
public IList<string> Options {get;set}
public string FilePath {get;set;}
public LogSelectedType LogSelected {get;set;}

public UserMenu()
{
    LogSelected = LogSelectedType.Continue;
}

public override CheckForBrokenRules()
{
    // validate here
}

public enum LogSelectedType
{
    Exit = -1,
    Continue = -2
}
}


// Domain Service
public class UserMenuService
{
private IUserMenuRepository _userMenuRepository;
public class UserMenuService(IUserMenuRepository userMenuRepository)
{
    _userMenuRepository = userMenuRepository;
}

public IList<UserMenu> GetAllUserMenuOptions()
{
    _userMenuRepository.FindAll();
}

public string DisplayMessage(UserMenu userMenu)
{
    // return message
}
}

// Contract used for persistence
public interface IUserMenuRepository
{
IList<UserMenu> FindAll();
}

// DATA LAYER
// Repository layer used to pull the data
public class UserMenuRepository : IUserMenuRepository
{
public IList<UserMenu> FindAll()
{
    // connect to database and populate model for persistence
}
}
    
risposta data 30.04.2011 - 15:44
fonte

Leggi altre domande sui tag