I had an interesting discussion with a coworker while planning a new feature for our current project. We are practicing Domain Driven Design and maintain a strict separation between the GUI and the domain logic. We actually treat them as separate applications.
Our GUI displays a 3D scene within a canvas. Our domain has a Visualization bounded context which contains a View3D entity. The View3D models things like the viewing frustum. Our rendering engine uses the View3D to render a Scene3D onto our canvas. There is nothing really exciting going on here.
We wanted to let the user pan the scene by dragging the mouse across the canvas. My task was to make the changes in the GUI to capture the drag events and send them down as PanView commands to the domain service. My coworker’s task was to write the domain service to handle those commands and update the View3D.
Because our domain logic is decoupled from the actual rendering engine or GUI, it actually does not know anything about the canvas. It keeps track of the viewing frustum in its own coordinate system which is indepedent of the screen canvas coordinates or the rendering engine coordinates. Because of this, the PanView command needs to express the movement in some unitless system. Since we know that the aspect ratio of the canvas matches the aspect ratio of the viewport, we decided to express the movement as fractions of the viewport size:
publicclass PanView{ /// <summary> /// The reference point on the viewport being moved, /// expressed as a fraction of the viewport size /// </summary> public Coordinate2D Reference; /// <summary> /// How far to move, expressed as a fraction of the viewport size. /// </summary> public Vector2D Delta;}
This representation is very easy for the user interface to produce and equally easy for the domain to consume and the two never need know about the other’s coordinate system. It also correctly captures all of the information required to implement the feature. If the user grabs the center of the canvas and moves down to the bottom of the canvas. We’d send down { Reference=(0.5, 0.5), Delta=(0, 0.5) }, signaling that the view should move halfway down.
While we were talking about some of the 3D math involved in the operation, I casually mentioned that he should not be surprised if the GUI sends down commands where the Deltas are > 1 or < -1 (or where Reference+Delta is > 1 or < -1). He should make sure the domain logic does not choke on that.
I was thinking about the case where the user clicks and starts dragging and moves the mouse outside the bounds of the canvas. If the user moves way down outside the canvas, I’d like the view to continue panning until the user lets go. Without even a pause he assured me that he would throw an exception during command validation if such a nonsense command was given to his service. From his point of view, the pan operation should be clipped to the canvas and absolutely should not continue as the user moves the mouse outside the canvas area. You see, he’s worked on an application that did it my way and the users did not like it.
We went back and forth a bit on it. But ultimately it did not matter who’s opinion was “right”. Eventual user acceptance testing would decide the issue. The real issue here was how the domain should act. What should it allow? There is no mathematical reason to deny Pan requests that move outside the viewing frustum. Our 3d world is not bounded by the frustum.
This was a GUI concern. The domain should happily consume such pan requests. It definitely should not arbitrarily deny certain requests for no real domain reason. It should be the choice of the user interface to decide if it wants to allow pan operations to continue outside the bounds of the canvas.
When implementing your domain services it is important to keep this in mind. Your command validation should ensure that the command makes sense for the current state of the domain. It should only deny those requests which would put the entity in an invalid state. In this example, our domain does not really care how much we pan. It is still in a valid state and can always be panned in the reverse direction if the user wishes. Leave it to the user interface to decide what makes for a good user experience.
Tell us what you need and one of our experts will get back to you.