Archive for the ‘Clean Code’ Category

Separating drag from drop

Tuesday, July 20th, 2010

In my previous post, I discussed separating drag-and-drop logic from view components as another tool to help achieve the cleanliness of single-responsibility classes. In this post, I’m going a step further by examining how the drag-and-drop functionality itself can be decoupled to achieve encapsulated components.

This post focuses on a scenario where objects are being dragged from one component to another (as opposed to simply rearranging objects within a single component). Further, these components may be quite different from each other (as opposed to being separate instances of the same component — although that scenario can benefit, too).

For example, one component may display a pool of objects that are “available” to be dragged somewhere. We can think of this as the “source” component (the “source” of the objects that can be dragged). In addition to this source component, we may have several “target” components. Perhaps the source/pool holds objects named “penny”, “nickel”, “dime”, and “quarter”. Then, we would have (at least) four “target” components displaying an image of each type of coin (and a place to drop a single, dragged object).

Across clients and projects, I see a tendency for drag-and-drop functionality to be lumped into a single, large view class. In the example discussed above, all of the logic for the source and target components, as well as their drag-and-drop logic, would be coupled (quadrupled?) together. This creates a lot of complexity, making it difficult to follow the flow. The method that handles the drag_drop logic, for example, would have to have conditional logic to determine whether the object was dropped on a target (and which one) … or dropped back on the source component (where do I need to add this object?). The same complexity typically exists for every drag-handler method. And, it often ends up a lot more complex if you start casting to different objects or implementing one-off behaviors for each component type. The root-cause of this complexity is the fact that each drag-handler method is responsible for multiple objects (the one where the drag started and the one where the object was dropped).

We tend to think of the happy-path drag-drop procedure progressing like this:

  1. drag start
  2. drag enter
  3. drag drop
  4. drag complete

This is simple and irresistibly logical. And, it is fine if all of your drag actions are occurring on a single component. However, if you are creating custom drag-and-drop functionality for separate components using this style of thinking, then the complexity and confusion discussed above can start to creep in.

It would be cleaner if each drag-handler was specific to a given component. But, if the source and target component logic is truly separated into separate components, what does this mean for the drag events (which events are called on which objects?)?

Once you separate these components, it eventually becomes clear that the events are separated like this:

  • On the component that the item is being dragged AWAY FROM:
    1. drag start
    2. drag complete
  • On the component that the item is being dragged TO:
    1. drag enter / over / exit
    2. drag drop

And, since the components are now separated, your drag-and-drop logic can only be for that object. This can really simplify your drag-and-drop logic. First of all, you can get rid of the conditional logic. Second, you can now build decoupled, encapsulated components that are not cluttered by the logic of other components.

You can couple this technique of drag-and-drop component separation with the previous post’s technique of keeping the drag-and-drop logic separate from the component. This can go a long way toward achieving clean, clear, single-responsibility classes. Instead of having all of this logic in a single, confusing, monolithic view class, the logic would now be split into four (or more) small, targeted classes (source component, target component, drag logic for source component, drag logic for target component). It can be quite refreshing to maintain code that is focused on a single-responsibility, rather than cluttered by logic that you are not interested in at the moment.

Below is some sample code to get you started. It shows the basic drag handlers for a source component and a target component. It does not show the components themselves, or refer to interfaces, etc. that you might use. But, hopefully, it’ll give you the basic idea.

*** Drag Logic for Source Component ***

public class DragSourceBehavior {

private var component:MyDragSourceUIComponent;

public function DragSourceBehavior(_component:MyDragSourceUIComponent) {
	component = _component;
	component.addEventListener(DragEvent.DRAG_ENTER, onDragEnter, false, 0, true);
	component.addEventListener(DragEvent.DRAG_DROP, onDragDrop, false, 0, true);
}

private function onDragStart(event:MouseEvent):void {
	if (DragManager.isDragging)
		return;

	var dragSource:DragSource = new DragSource();

	DragManager.doDrag(event.currentTarget, dragSource, event);

	component.removeObject(event.currentTarget);
}

private function onDragComplete(event:DragEvent):void {
	var dragObject:UIComponent = event.currentTarget as UIComponent;
	dragObject.dispatchEvent(new MouseEvent(MouseEvent.MOUSE_UP));

	if (droppedOnDifferentObject(event))
		removeDragEvents(event.currentTarget);
}

private function onDragEnter(event:DragEvent):void {
	DragManager.acceptDragDrop(event.currentTarget as IUIComponent);
}

public function onDragDrop(event:DragEvent):void {
	component.addObject(event.dragInitiator);
	addDragEvents(event.dragInitiator);
}

public function addDragEvents(dragObject:UIComponent):void {
	dragObject.addEventListener(MouseEvent.MOUSE_DOWN, onDragStart);
	dragObject.addEventListener(DragEvent.DRAG_COMPLETE, onDragComplete);
}

public function removeDragEvents(dragObject:UIComponent):void {
	dragObject.removeEventListener(MouseEvent.MOUSE_DOWN, onDragStart);
	dragObject.removeEventListener(DragEvent.DRAG_COMPLETE, onDragComplete);
}

private function droppedOnDifferentObject(event:DragEvent):Boolean {
	var droppedOnAnotherObject:Boolean = false;

	if (event.relatedObject != component)
		droppedOnAnotherObject = true;

	return droppedOnAnotherObject;
}

}

*** Drag Logic for Target Component ***

public class MyDragTargetBehavior extends EventDispatcher {

private var component:MyDragTargetUIComponent;

public function MyDragTargetBehavior(_component:MyDragTargetUIComponent) {
	component = _component;
	component.addEventListener(DragEvent.DRAG_ENTER, onDragEnter, false, 0, true);
	component.addEventListener(DragEvent.DRAG_DROP, onDragDrop, false, 0, true);
}

private function onDragStart(event:MouseEvent):void {
	if (DragManager.isDragging)
		return;

	var dragSource:DragSource = new DragSource();

	DragManager.doDrag(event.currentTarget, dragSource, event);

	component.removeObject(event.currentTarget);
}

private function onDragComplete(event:DragEvent):void {
	var dragObject:UIComponent = event.currentTarget as UIComponent;

	if (droppedOnDifferentObject(event))
		removeDragEvents(dragObject);
}

private function onDragEnter(event:DragEvent):void {
	DragManager.acceptDragDrop(event.currentTarget as IUIComponent);
}

public function onDragDrop(event:DragEvent):void {
	component.addObject(event.dragInitiator);
	dispatchEvent(new CustomDragEvent(CustomDragEvent.OBJECT_DROPPED, event.dragInitiator, component));
}

private function droppedOnDifferentObject(event:DragEvent):Boolean {
	var droppedOnAnotherObject:Boolean = false;

	if (event.relatedObject != component)
		droppedOnAnotherObject = true;

	return droppedOnAnotherObject;
}

public function addDragEvents(dragObject:UIComponent):void {
	dragObject.addEventListener(MouseEvent.MOUSE_DOWN, onDragStart);
	dragObject.addEventListener(DragEvent.DRAG_COMPLETE, onDragComplete);
}

public function removeDragEvents(dragObject:UIComponent):void {
	dragObject.removeEventListener(MouseEvent.MOUSE_DOWN, onDragStart);
	dragObject.removeEventListener(DragEvent.DRAG_COMPLETE, onDragComplete);
}

}

Separating Drag-and-Drop from View components

Wednesday, May 12th, 2010

I was recently working on a set of view classes that, among other things, implemented custom drag-and-drop behavior. As we all know, Drag-and-drop requires several different event handlers for the various drag events (DRAG_START, DRAG_ENTER, etc.). Adding these event handlers to your view class adds a lot of bulk to the existing view logic.

In addition to the issues that are directly related to the sheer size of the class, including drag-and-drop logic in a view can often be considered a 2nd responsibility of the view. I often find that, when I’m working on a view that contains drag-and-drop behavior, I’m typically doing one of two things:

  1. Working on the view (look-and-feel, data set-up, gathering results after user interaction, etc.).
  2. Working on the drag-and-drop behavior.

Thus, if I am working on the view, all of the drag-and-drop logic is excess clutter in my way (and vice versa).

This should actually be an immediate red flag. Good coding practice suggests that a class should have a single responsibility. The Single Responsibility Principle, a term coined by Uncle Bob, correlates a “responsibility” with “a reason to change”. In other words, a class should have only one reason to change. In this example, there are two reasons that I might change the view. Therefore, one of the reasons-for-change should be refactored out of this class. It seems silly to refactor the view out of the view class. So, my approach was to move the drag-and-drop functionality into its own class.

There are probably a number of ways to structure the association between the view and the class containing the drag-and-drop functionality. However, one simple method is to inject the view component into the drag-and-drop class. This will allow the drag-and-drop class to register for the drag events emitted by the view. To limit coupling, the injected reference can be typed as a generic UIComponent (to have access to the drag events).

private var viewComponent:UIComponent;

public function DragAndDropFunctionality(component:UIComponent) {
	viewComponent = component;
	viewComponent.addEventListener(DragEvent.DRAG_ENTER, onDragEnter, false, 0, true);
	viewComponent.addEventListener(DragEvent.DRAG_EXIT, onDragExit, false, 0, true);
	viewComponent.addEventListener(DragEvent.DRAG_DROP, onDragDrop, false, 0, true);
}

Some events, such as DRAG_DROP will require the view to take action (add the dropped item to the appropriate place). Again, we want to limit coupling in our communication. The standard technique to maintain separation is for the lower-level component (the drag-and-drop class, in this case) to dispatch events. Then, the higher-level component (the view, in this case) can listen for those events and take the appropriate action.

Once this separation is achieved, we will have classes that have (hopefully) a single responsibility. When you need to work on the drag-and-drop logic, you can go directly to the drag-and-drop class. I find it much easier to follow the drag-and-drop logic without the view code cluttering the class. Similarly, the view component is no longer cluttered with all of the drag-and-drop event handlers. The classes are smaller and more obvious. And, the two “responsibilities” can now be worked on by multiple developers without lock conflicts or merging, to boot.