MVC

Wzorce projektowe

  |   9 min czytania

Zastosowanie

MVC (ang. Model-View-Controller) (wzorzec architektoniczny) ułatwia organizowanie struktury aplikacji poprzez podział odpowiedzialności na trzy rozdzielne warstwy: widoku (View), kontrolera (Controller) oraz modelu (Model). Warstwa widoku odpowiedzialna jest za sposób prezentacji danych z modelu w interfejsie użytkownika. Warstwa kontrolera obsługuje zdarzenia systemu oraz interakcję użytkownika poprzez delegowanie zadań do modelu oraz powiadamianie widoku o bieżącym stanie. Natomiast warstwa modelu odpowiada za logikę biznesową tzn. za przetwarzanie, przechowywanie i sposób dostarczania danych. Dzięki separacji odpowiedzialności kod staje się bardziej uporządkowany i elastyczny na modyfikacje (zmiana jednej warstwy nie musi pociągać za sobą zmiany kodu w pozostałych warstwach), a warstwa modelu jest zdolna do przeprowadzenia testów. MVC jest bardziej pojęciem konceptualnym niż zbiorem formalnych reguł implementacji.

Ograniczenia

Ze względu na specyfikę architektury systemu Android (cykl życia Aktywności i jej odpowiedzialności) warstwy View oraz Controller są często implementowane w Aktywności przez co nie ma między nimi wyraźnej rozdzielności. Z tego powodu zdolna do przeprowadzenia niezależnych testów jest tylko warstwa Modelu. Implementacja może zostać usprawniona (zgodnie z ideą wzorca) poprzez przeniesienie warstwy View do oddzielnej klasy i pozostawieniu Aktywności odpowiedzialności warstwy Controller. Jednakże takie rozwiązanie jest tylko połowiczne. Wprowadza sztuczny klasowy podział separacji warstw View oraz Controller, ponieważ Aktywność ciągle jest odpowiedzialna za inicjalizacje widoków w warstwie View, które są powiązane z jej cyklem życia. Co więcej warstwa View oraz Controller są zależne od klas Androidowych i mają dostęp do klasy Model, co nadal wpływa na podział odpowiedzialności i możliwości niezależnego testowania. Rozwiązaniem tego problemu może być zastosowanie wzorca MVP.

Użycie

Wzorzec MVC można wykorzystać w małych projektach o niewielkiej ilości ekranów, gdzie ewentualnemu testowaniu podlega przede wszystkim logika biznesowa.

Implementacja

Klasa View inicjalizuje widok oraz realizuje zachowania obiektów widoku, zmiany ich właściwości oraz stanów. View przechowuje referencje do modelu z którego czerpie niezbędne dane do obsługi obiektów widoku. Klasa Controller nadzoruje poprawną obsługę procesów między interakcją użytkownika i systemem. Przechwytuje zdarzenia oraz akcje użytkownika i systemu, deleguje wykonanie zadania do modelu, a następnie powiadamia widok o stanie żądanej akcji. Klasa Model implementuje logikę systemu, a także zarządza danymi.

MVC diagram

Poniższy listing przedstawia implementacja wzorca MVC przy zachowaniu klasowej rozdzielności warstw.

public class View {

    //some view fields like buttons, textview etc
    private TextView textView;
    private EditText editText;
    private Button button;

    private Model model;

    //parent view object also can be passed here
    public View(Model model) {
        this.model = model;
        initView();
    }

    private void initView() {
        //init views (in Android like findViewById from passed parent view in constructor)
        //set default text/image etc in the views
    }

    public void showError(String error) {
        //show error on the screen (in Android like Toast)
    }

    public String getInput() {
        return editText.getText().toString();
    }

    public void updateText() {
        //update tasks list in TextView
        textView.setText(model.getData());
        //show response text in the textView
    }
}

public class Controller {

    private Model model;
    private View view;

    public Controller(Model model, View view) {
        this.model = model;
        this.view = view;
    }

    private void actionRequest() {
        //controller catches some event like click on the button
        //do proper action
        String input = view.getInput();
        try {
            boolean success = model.addData(input);
            if(success)
                view.updateText();
            else
                view.showError("Task has not been added.").
        }
        catch(Exception exception) {
            view.showError("Unexpected error occured.").
        }
    }
}

public class Model {

    private Database database;
    //other variables, logic managers or data providers like database, cache, network

    public Model() {
        //do init staff
        //initialize some instances
        database = new Database();
    }

    public boolean addData(String input) {
        boolean isSuccess = database.add("TASK", input);
        return success;
    }

    public String getData() {
        String tasks = "";
        for(int i=0; i<database.size("TASK"); i++)
            tasks += database.get("TASK", i) + "\n";
        return tasks;
    }
}

Klient inicjalizuje obiekt warstw dzięki czemu stają się one zdolne do współpracy.

//initialize MVC objects
Model model = new Model();
View view = new View(model);
Controller controller = new Controller(model, view);

//do some stuff, cach actions and events
controller.actionRequest();
//1. controller catched the event
//2. gets input from view
//3. delegates the job to model
//4. info about job status passed to controller
//5. view is notifed about request finished
//6. new data loaded from model

Przykład

Ekran wyszukiwania trasy ViewControllerActivity w procesie znajdywania pasażerskiego połączenia kolejowego wykorzystuje komunikacje sieciową. Ponadto zapamiętuje ostatnio poprawanie wprowadzoną stacje początkową i automatycznie uzupełnia polę stacji. Do implementacji tego procesu wykorzystano wzorzec MVC w wariancie połącznej warstwy View-Controller realizowaną przez Aktywność.

public class ViewControllerActivity extends AppCompatActivity {

    private EditText startEdit;
    private EditText destinationEdit;
    private Button searchButton;

    private TrackModel model;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        model = new TrackModel(this);

        //in clear MVC inflating layout, listeners and views update
        //should be done in separeted View layer
        initView();
        setListeners();
        setViews();
    }

    //view layer
    private void initView() {
        setContentView(R.layout.viewcontroller_activity);
        startEdit = findViewById(R.id.startEdit);
        destinationEdit = findViewById(R.id.destinationEdit);
        searchButton = findViewById(R.id.searchButton);
    }

    //view layer set listener passed from controller
    private void setListeners() {
        searchButton.setOnClickListener(v -> { searchAction(); });
    }

    //view layer
    private void setViews() {
        String home = model.getHomePlace();
        startEdit.setText(home);
    }

    //controller layer runs this method on the view layer
    private void showError(String text) {
        Toast.makeText(this, text, Toast.LENGTH_LONG).show();
    }

    //controller layer
    private void searchAction() {
        //controller get inputs from view
        String start = startEdit.getText().toString();
        String destination = destinationEdit.getText().toString();
        String result = model.search(start, destination);
        if(result.equals("EMPTY")) {
            showError("Incorrect start or destination place");
        }
        else {
            //show search result in another activity/popup or in list
        }
    }

    //other lifecycle methods
}

public class TrackModel {

    //some network provider
    private SharedPreferences sharedPref;
    private SharedPreferences.Editor editor;
    private Context context;

    public TrackModel(Context context) {
        this.context = context;
        sharedPref = context.getPreferences(Context.MODE_PRIVATE);
    }

    //could be some response message instead of String result
    public String search(String start, String destination) {
        //use some network library to login

        //if response is ok
        saveHome(start);
        return "14:00 " + start " - " + destination + " 15:30";
        return true; //mock

        //if response fail just return "EMPTY"
    }

    public String getHomePlace() {
        return sharedPref.getString("home", "");
    }

    public void saveHome(String start) {
        editor = sharedPref.edit();
        editor.putString("home", start);
        editor.commit();
    }
}

Biblioteki

Na pierwszy rzut oka można pomyśleć, że środowisko Android domyślnie implementuje wzorzec MVC, a nawet wymusza na programiścię jego realizacje. Takie przekonanie jest jednak pewnym uproszczeniem. Pomimo dość częstej praktyki traktowania Aktywności jako warstwę View-Controller, warto te warstwy odseparować. Programista implementujący wzorzec powinien pamiętać, że realizacja wzorca MVC jest zależna od architektury systemu, dlatego nie ma jedynej słusznej implementacji.