When entry-level developers are assigned a business problem, they tend to latch on to the first solution that comes to mind. Not only does this have a tendency to lock in an inferior solution too soon, but it may even cloud thinking to the point of missing some important requirements.
Listening for Clues
Telltale signs of this mistake are in the developer’s vocabulary, so mentors should pay attention. When the rookie attempts to gather a program’s requirements, does he or she jump straight to using programming terminology in describing those requirements, like hash maps, FIFO queues, reference pointers, and LIFO stacks? Or, does he or she carefully stick to problem-domain terminology like definitions, breakdowns, groups, relationships, andpriorities?
If your rookie is indeed having trouble distinguishing the problem-domain from the solution-domain, then the following techniques may help.
Encouraging a Test-First Approach
There are certain practices that can encourage developers to keep an open mind. One such practice is to use a tool that supports behavior-driven development (e.g. Cucumber). It forces the developer to begin with an acceptance test (that is expressed in plain English) and that focuses solely on the problem domain. Only after the acceptance test is fully defined does the developer move on to implementing the solution — or even multiple different solutions that can be swapped in and out and measured against performance benchmarks, placed in A/B testing trials, etc.
Distinguishing Form vs. Function
Function refers to the purpose of a problem, what the program is trying to achieve. Form refers to the format, layout, or organization of the problem, i.e. how it is presented. The problem domain will always have elements of both. In fact, different aspects of the problem may each have their own forms and functions.
For example, a genealogy (family tree) program will primarily take the form of a tree with the function of recording information about people. (Secondarily, it could also have an aspect of a timeline, where a chronologically linear form has the function of recording information about events, but let’s ignore that for now.)
Each node (person) within the tree will have data about that person (name, birth date, etc.) in accordance to the function. Each node will also have metadata that refers to its position in the tree (parents, siblings, children) in accordance to the form.
Good design allows for the form to stand on its own as a general-purpose framework upon which to hang the function. So, in this case, we’ll want to start with a solid, general-purpose graph structure of nodes and edges, where the nodes can each have multiple parents, multiple siblings (that do not necessarily share the same parents), and multiple children. It shouldn’t matter what values happen to be attached to each node. Instead of people, they could be parts in a manufacturing assembly, for example. (A good Java developer, for example, would know to turn to something like JGraphT, JDLS, or neo4j which already provides such a framework.)
Commonality/Variability Analysis
Commonality/Variability Analysis (CVA) is another technique for organizing elements of a problem domain. It organizes them into layers of abstraction.
One simple way to do CVA is to use sticky notes with a white board. Write down the problem domain elements each on their own sticky note and tack them to the white board. Try grouping them together in different ways until patterns start to emerge. After settling on a primary grouping organization, draw Venn diagrams around the sticky notes to represent primary and secondary groupings. Here, then are your clues for how to design the software using abstract classes with polymorphism (probably on the primary groupings), and/or interfaces and their implementations (on the secondary groupings).