Implementing standard software design patterns (focus on MVC) in R
Asked Answered
G

1

56

Currently, I'm reading a lot about Software Engineering, Software Design, Design Patterns etc. Coming from a totally different background, that's all new fascinating stuff to me, so please bear with me in case I'm not using the correct technical terminology to describe certain aspects ;-)

I ended up using Reference Classes (a way of OOP in R) most of the time because object orientation seems to be the right choice for a lot of the stuff that I'm doing.

Now, I was wondering if anyone has some good advice or some experience with respect to implementing the MVC (Model View Controller; also known as MVP: Model View Presenter) pattern in R, preferably using Reference Classes.

I'd also be very interested in info regarding other "standard" design patterns such as observer, blackboard etc., but I don't want to make this too broad of a question. I guess the coolest thing would be to see some minimal example code, but any pointer, "schema", diagram or any other idea will also be greatly appreciated!

For those interested in similar stuff, I can really recommend the following books:

  1. The Pragmatic Programmer
  2. Design Patterns

UPDATE 2012-03-12

I did eventually come up with a small example of my interpretation of MVC (which might not be totally correct ;-)).

Package Dependencies

require("digest")

Class Definition Observer

setRefClass(
    "Observer",
    fields=list(
        .X="environment"
    ),
    methods=list(
        notify=function(uid, ...) {
            message(paste("Notifying subscribers of model uid: ", uid, sep=""))
            temp <- get(uid, .self$.X)
            if (length(temp$subscribers)) {
                # Call method updateView() for each subscriber reference
                sapply(temp$subscribers, function(x) {
                    x$updateView()        
                })
            }    
            return(TRUE)
        }
    )
)

Class Definition Model

setRefClass(
    "Model",
    fields=list(
        .X="data.frame",
        state="character",
        uid="character",
        observer="Observer"
    ),
    methods=list(
        initialize=function(...) {
            # Make sure all inputs are used ('...')
            .self <- callSuper(...)
            # Ensure uid
            .self$uid <- digest(c(.self, Sys.time()))
            # Ensure hash key of initial state
            .self$state <- digest(.self$.X)
            # Register uid in observer
            assign(.self$uid, list(state=.self$state), .self$observer$.X)
            .self
        },
        multiply=function(x, ...) {
            .self$.X <- .X * x 
            # Handle state change
            statechangeDetect()
            return(TRUE)
        },
        publish=function(...) {
            message(paste("Publishing state change for model uid: ", 
                .self$uid, sep=""))
            # Publish current state to observer
            if (!exists(.self$uid, .self$observer$.X)) {
                assign(.self$uid, list(state=.self$state), .self$observer$.X)
            } else {
                temp <- get(.self$uid, envir=.self$observer$.X)
                temp$state <- .self$state
                assign(.self$uid, temp, .self$observer$.X)    
            }
            # Make observer notify all subscribers
            .self$observer$notify(uid=.self$uid)
            return(TRUE)
        },
        statechangeDetect=function(...) {
            out <- TRUE
            # Hash key of current state
            state <- digest(.self$.X)
            if (length(.self$state)) {
                out <- .self$state != state
                if (out) {
                # Update state if it has changed
                    .self$state <- state
                }
            }    
            if (out) {
                message(paste("State change detected for model uid: ", 
                   .self$uid, sep=""))
                # Publish state change to observer
                .self$publish()
            }    
            return(out)
        }
    )
)

Class Definition Controller and Views

setRefClass(
    "Controller",
    fields=list(
        model="Model",
        views="list"
    ),
    methods=list(
        multiply=function(x, ...) {
            # Call respective method of model
            .self$model$multiply(x) 
        },
        subscribe=function(...) {
            uid     <- .self$model$uid
            envir   <- .self$model$observer$.X 
            temp <- get(uid, envir)
            # Add itself to subscribers of underlying model
            temp$subscribers <- c(temp$subscribers, .self)
            assign(uid, temp, envir)    
        },
        updateView=function(...) {
            # Call display method of each registered view
            sapply(.self$views, function(x) {
                x$display(.self$model)    
            })
            return(TRUE)
        }
    )
)
setRefClass(
    "View1",
    methods=list(
        display=function(model, x=1, y=2, ...) {
            plot(x=model$.X[,x], y=model$.X[,y])
        }
    )
)
setRefClass(
    "View2",
    methods=list(
        display=function(model, ...) {
            print(model$.X)
        }
    )
)

Class Definition For Representing Dummy Data

setRefClass(
    "MyData",
    fields=list(
        .X="data.frame"
    ),
    methods=list(
        modelMake=function(...){
            new("Model", .X=.self$.X)
        }
    )
)

Create Instances

x <- new("MyData", .X=data.frame(a=1:3, b=10:12))

Investigate model characteristics and observer state

mod <- x$modelMake()
mod$.X

> mod$uid
[1] "fdf47649f4c25d99efe5d061b1655193"
# Field value automatically set when initializing object.
# See 'initialize()' method of class 'Model'.

> mod$state
[1] "6d95a520d4e3416bac93fbae88dfe02f"
# Field value automatically set when initializing object.
# See 'initialize()' method of class 'Model'.

> ls(mod$observer$.X)
[1] "fdf47649f4c25d99efe5d061b1655193"

> get(mod$uid, mod$observer$.X)
$state
[1] "6d95a520d4e3416bac93fbae88dfe02f"

Note that the object's uid has automatically been registered in the observer upon initialization. That way, controllers/views can subscribe to notifications and we have a 1:n relationship.

Instantiate views and controller

view1 <- new("View1")
view2 <- new("View2")
cont  <- new("Controller", model=mod, views=list(view1, view2))

Subscribe

Controller subscribes to notifications of underlying model

cont$subscribe()

Note that the subscription has been logged in the observer

get(mod$uid, mod$observer$.X)

Display Registered Views

> cont$updateView()
  a  b
1 1 10
2 2 11
3 3 12
[1] TRUE

There's also a plot window that is opened.

Modify Model

> cont$model$multiply(x=10)
State change detected for model uid: fdf47649f4c25d99efe5d061b1655193
Publishing state change for model uid: fdf47649f4c25d99efe5d061b1655193
Notifying subscribers of model uid: fdf47649f4c25d99efe5d061b1655193
   a   b
1 10 100
2 20 110
3 30 120
[1] TRUE

Note that both registered views are automatically updated as the underlying model published its state change to the observer, which in turn notified all subscribers (i.e., the controller).

Open Questions

Here's what I feel like I'm not fully understanding yet:

  1. Is this a somewhat correct implementation of the MVC pattern? If not, what did I do wrong?
  2. Should "processing" methods (e.g. aggregate data, take subsets etc.) for the model "belong" to the model or the controller class . So far, I always defined everything a specific object can "do" as methods of this very object.
  3. Should the controller be sort of a "proxy" controlling every interaction between model and views (sort of "both ways"), or is it only responsible for propagating user input to the model (sort of "one way"?
Gosport answered 12/3, 2012 at 20:6 Comment(2)
This question is a duplicate: https://mcmap.net/q/553558/-the-use-of-design-patterns-in-r-closed. In addition, you have multiple goals in your question. One general about design patterns, and one specific to an example. The overlap between the duplicate question and this question is mainly in the general part. Maybe you could edit your question to only your question on your example. This also decreases your chances of being closed as too general, which happend to the duplicate question.Photolithography
@PaulHiemstra: thanks for the info! I did just finish a major edit and I guess for a start and in order to make this not too broad, I'm mainly after an implementation of MVC.Gosport
V
3
  1. It looks quite good, but I'm not so sure why you have an Observer additional to your other classes (maybe you can tell me) Usually the Controller IS an Observer. It's a really good idea to do this in R because when I learned it in Java it was not so easy to understand (Java hides some of the good parts)

  2. Yes and No. There are many different interpretations of this pattern. I like to have the methods in the Object, I'd say it belongs to the model. A simple example would be a sudoku solver that shows the solving steps in a GUI. Let's split it into some parts that can be separated into M, V and C: the raw data (2D array maybe), the sudoku functions (calc next step, ...), the GUI, someone who tells the GUI that a new step was calculated I'd put it like this: M: raw data + sudoku functions, C: who tells the GUI about changes / the model about GUI inputs, V: GUI without any logic others put the sudoku function into the Controller, is also right and might work better for some problems

  3. It's possible to have a "one way" controller like you call it and the View is an Observer of the model It's also possible to let the Controller do everything and Model and View don't know each other (have a look at Model View Presenter, that's about that)

Vitriolic answered 12/3, 2012 at 20:6 Comment(1)
Thanks a lot for your answer. It's been a while since I wrote this thing, so I'll actually have to give it a closer look again before I can answer your question about the additional Observer ;-)Gosport

© 2022 - 2024 — McMap. All rights reserved.