view-model-example

At Wedding Party our app allowed users to share photos in a shared timeline. Other people at the same wedding could do many things with the photos, including liking them. If only a few people liked a photo, we wanted to show details about each person who liked it. If many people liked the photo, we showed a short summary of the number of people who liked it instead of long list of names.

In this post, I will use a view model, and view model tests, to implement the "post likes" user interface. After the implementation, I will talk about the numerous advantages of using view models. Finally, I will provide references where you can learn more about view models.

The example

I'm going to start my example by diving right into the code. This might seem too early, without some introduction, but you will see that this code is easy to reason about. It follows the understandability-as-a-design-goal approach.

class PostLikesNamesViewModel

  DISPLAY_SUMMARY_IF_LONGER_THAN = 50

  def initialize names_list
    @names_list = names_list
  end

  def display_mode
    _full_message_is_longer_than_two_lines? ? :summary : :full
  end

  def message_to_display
    if display_mode() == :summary
      _summary_message()
    else
      _full_message()
    end
  end

  protected

    def _summary_message
      _num_likes().to_s + " likes"
    end

    def _full_message
      @full_message ||=
        @names_list.join(", ").reverse.sub(/ ,/, " & ").reverse
    end

    def _full_message_is_longer_than_two_lines?
      _full_message().size > DISPLAY_SUMMARY_IF_LONGER_THAN
    end

    def _num_likes
      @names_list.size
    end

end

require "minitest/autorun"

describe PostLikesNamesViewModel do

  def vm_with_names names
    @vm = PostLikesNamesViewModel.new names
  end

  describe "message to display" do

    describe "full message" do

      it "should have just the user's name" do
        vm_with_names ["Jen Kaufman"]
        assert_equal "Jen Kaufman", @vm.message_to_display()
      end

      it "should put & between the two names" do
        vm_with_names ["Jen Kaufman", "Andrew Fox"]
        assert_equal "Jen Kaufman & Andrew Fox", @vm.message_to_display()
      end

      it "should put commas between user names until last set of users" do
        vm_with_names ["Jen Kaufman", "Andrew Fox", "Roger Smith"]
        assert_equal "Jen Kaufman, Andrew Fox & Roger Smith",
          @vm.message_to_display()
      end

    end

    describe "summary message" do

      it "should display if there are too many users to fit" do
        vm_with_names ["Jen Kaufman", "Andrew Fox", "Roger Smith",
          "Becky Humphries", "Johnny Rocket"]
        assert_equal "5 likes", @vm.message_to_display()
      end

      it "should correctly work with more users" do
        vm_with_names ["Jen Kaufman", "Andrew Fox",
          "Roger Smith", "Becky Humphries", "Johnny Rocket", "Lenny"]
        assert_equal "6 likes", @vm.message_to_display()
      end

      it "should correctly work with *many* likes" do
        vm_with_names ("a".."z").to_a
        assert_equal "26 likes", @vm.message_to_display()
      end

    end

  end

  describe "display mode" do

    it "should be summary if message too long" do
      vm_with_names ["Jen Kaufman", "Andrew Fox",
        "Roger Smith", "Becky Humphries", "Johnny Rocket", "Lenny"]
      assert_equal :summary, @vm.display_mode()
    end

    it "should be full if message short enough" do
      vm_with_names ["Jen Kaufman", "Andrew Fox"]
      assert_equal :full, @vm.display_mode()
    end

  end

end

The Advantages of Using View Models

To address a potential criticism of the example head on: yes, there is a lot of code and a lot of tests for a seemingly simple user interface feature. I used this feature because it is just barely complicated enough to showcase some of the ideas of view models. The real benefits of view models become clear as things become more complicated. As a feature accretes more complexity, the view model remains easy to work with. In this section I will talk about the advantages of view models, and you will see how they can be useful, even on a feature as simple as this one.

Easy to reason about

Easy to test

The view model in the example can be initialized with only the names, which means it can be tested separately without needing to add user objects, post objects, etc. Note that the names list passed in is just an array of string literals, which makes is extremely easy to construct. book-code-complete-2 refers to this as "simple-data-parameter coupling", and suggests that it is preferable to object-parameter coupling (where you would pass in an object that could answer the messages passed to it). In a slightly more complicated example, using a view model would also solve the problem described here, because in your tests you can set up all the context you need for the component.

blog-post-pure-ui#problem-with-testing-ui-in-app

In addition, getting to the particular payment form in this example could be the result of more branching. You must first be logged in, maybe under a certain type of user, of a certain role and specific privileges, with some pre-existing data in their profile.

 

The public interface is small and directly (and obviously) usable by the consuming code.

Only the public interface is tested, but it's easy enough to setup the model in many different scenarios that we actually get good coverage of the private methods without actually coupling to them.

Note how the methods are in the language of the specification, not implementation. Specifically _full_message_is_longer_than_two_lines? talks about what our designer asked for, not some technical detail that we, as programmers, care about.

blog-post-presentation-model#view-is-utterly-simple

"the essence of the idea of the Presentation Model - all the decisions needed for presentation display are made by the Presentation Model leaving the view to be utterly simple."

 
video-from-legacy-to-ddd#words-the-business-people-use

At 14:35 the words the business people use

 
blog-post-acceptance-tests-at-single-level-of-abstraction#tell-story-others-can-understand

the developer can better tell the story of each scenario by describing behaviors consistently and at a high enough level that others will understand the goal and outcomes of the test

 
blog-post-automated-testing-for-league-of-legends#exposed-interface-has-relevant-semantics

"Most users of the BVS really only interact with tests themselves, since we go out of our way to ensure they don't have to think about any of the details that the driver handles. Along the same lines, we expose a fairly large standard library wrapping the RPC endpoints used to talk to the game. Part of the reason we do this is just to ensure that tests aren't closely coupled to the RPC interface, but the major reason we do it is to provide a standard set of behaviors that prevent sloppy test-writing and ensure consistency between tests."

 
talk-functional-principles-for-oop#environmental-access-isolated-and-called-out

At 16:45 environmental access is isolated and strictly called out

 
hacker-news-comment-view-model#logical-code-behind

"UI independent" just means that it should be a logical model of the View, rather than a visual model (e.g., don't expose Color or Brush properties, instead expose properties like IsSelected). So think of the View model like logical code-behind."

 

easy-to-understand-examples-do-not-show-complexity

Arguments in favor and questions

Development Speed

No need to create full-blown users and have them like a post just to check that the string is correctly processed. Compare that with sub-millisecond check that checks all scenarios (granted, iPhone takes longer, but we're still talking a few seconds vs. minutes)

Easy interface lends itself to temporarily hard-coding stuff into actual UI to show what things would look like with different data.

Allows us to reason about a sub-problem effectively without having to take overall system state into consideration. No matter what, if you pass in the same names, you'll have the same result.

Also, there are no side effects, so we don't have to think about how this object alters system state.

Less stack to load into your head when starting to develop at the beginning of the day (or after being gone for a weekend). Or, more importantly, if this hasn't been your code before and you're changing it.

POXOs and POXO tests run very fast, which should make testing less painful, and development speed faster.

Code Quality

Supports refactoring code aggressively to arrive at an optimal interface by giving confidence in the code's coverage.

Moves things out of the large PostUI God Object. If you see in git that the PostUI object has changes on it, would you feel comfortable?

Makes changes in history more obvious.

Process Flexibility

Doesn't require actual data initially, allowing possibility for mocking up the interface and showing it off. The difference between this and our current prototyping is, this will still work when the actual backing code is put into place.

Ease of Next Implementation

When we go to implement a feature on the other platform (say Android), if we have tested POXOs, (where the tests are also POXOs) we can do something closer to a 1 to 1 translation of the feature than we would if the framework was integral in the classes and tests.

If the tests are simple enough, a nearly direct translation of them (using editor macros) might be possible.

If the interface of the object speaks in the language of the specification rather than the language of the implementation, we can swap out the implementation within the method more easily.

Long Term Maintenence

Application could be an imperitive shell around a functional, well-tested core objects, allowing us to really focus integration tests on integration, not on ensuring that the details are also correct. Basically, the application, to the extent it can be, should be composed of well tested context-independent components.

When we find bugs, we'd add failing tests, fix that particular bug, and all tests would continue to work. We'd be building a stronger application over time.

Stronger connection to original requirements, particularly if the public interface matches the language of the specification.

The requirements of the specification are captured both in the tests and in the language of the implementation, which makes the requirements self-documenting. Without this, the requirements are in the developer's head while creating the initial implementation, but are lost after-the-fact, making maintenance work much more likely to cause behavioral regressions against the original intended functionality.

Bigger Picture

If we ever want to move to some kind of language abstraction framework (like J2obc) then having POXOs would be a big benefit.

Overall Benefit

This allows us to accrete complexity much more easily. Things that worked will continue to work and additional functionality has an obvious place to go.

Questions

Is ViewModel the right name? I took it from MVVM (Model View ViewModel).

""" MVVM facilitates a clear separation of the development of the graphical user interface (either as markup language or GUI code) from the development of the business logic or back end logic known as the model (also known as the data model to distinguish it from the view model). """

""" The view model of MVVM is a value converter[4] meaning that the view model is responsible for exposing the data objects from the model in such a way that those objects are easily managed and consumed """

I'm not sure how this is different from a "presenter", but I like the term ViewModel, because, at least the way I think about it, it implies that the model might change over time (unlike the presenters that gather up the data and then present it to the view.


On way to solve for this problem:

talk-hot-reloading-with-time-travel#redo-steps-while-iterating

At 3:15 "If you're iterating on your data transformations and you have some actions that cause those transformations, like you click a button and something happens, you need to redo these steps every time you change a function, because you have to make sure they work correctly."

 

is to have hot reloading, but that's only helpful for the cases you can see easily by hand. For handling more complexity, you need something that retries all the view logic.


podcast-giant-robots-115-brandon-bloom#working-at-a-higher-level

At 16:45 (not a direct quote), in physics, when you think about a block on an inclined plane and you measure the friction, you're not thinking about the wind, the quantum effects, etc... you're working at a higher level model.

 

In this case, the higher-level model is not taking platform or specific technologies into consideration, and focusing on the desired state of the view (not how you'll get there)

Referring Pages

write-view-model-tests-at-the-level-of-intent project-react-storybook