Taken from my writeup on ES in response to a question on courses for the non-CS person.
Tough question. Software design, from my experience, comes down to learning how to apply methodologies and techniques based on the problem. The application is often as much art as it is a science so most academic courses appear useless until experience is gained. A bit of a catch-22 but the coursework is often useful later on when it finally dawns on you what the point was.
If I were to suggest something, it would general introduction to software engineering and object oriented design. The latter doesn't always fit the bill for software. However, the techniques are useful for taking system requirements and dividing the system out into discrete components no matter what the final decision is on implementation.
My general thoughts on the topic include:
Before coding, I highly suggest a written description with a length matching the complexity of the system. It should not contain any discussion of how it works ... just what it needs to do. Then start listing the requirements of the system -- what it should do and what it should not do. Both are very important. For multiple authority levels such as globals, dividing the list into discrete sections for mortals and staff/wizards/etc will help
After you get that written, hand it off to others to review if you can. Since it is nothing more than a description you can give it to anyone -- even those that don't code. Use people with a good eye for details especially for the requirements review. Including the end-users of the system will greatly aid in the design process. They may run off on a tangent but hopefully they'll identify any weaknesses in the description or requirements you overlooked. Incorporate all the relevant comments and update the document. Repeat the process if significant changes are introduced.
To demonstrate the usefulness of doing up front preliminary design, jot down what you'd do for a common +who or +where command. Compare it to an operational system. Those commands are not complex systems but do contain significantly different functionality depending on who executes the command -- a player or a staffer.
After the basic design is complete, the detailed design begins. The system is subdivided into smaller elements or modules of functionality. This step is the beginning of the code but it is not functional code...it is pseudo-code -- somewhat descriptive and somewhat code. List every element of the system and describe precisely how it will work. Refer back to the preliminary design to validate that it does what is intended and doesn't do things that wer not intended. Watch for functionality that is repeated in several areas and move anything that is reused into a discrete function.
Write a detailed specification for every command based on its intended purpose and restrictions it imposes. Document any and all shared functionality ... these bits, even if trivial will become the building blocks of the system. Learn to build complex functionality based on smaller elements.
Detailed design is often skipped by experienced coders because they can mentally assemble what is needed and how it will work. It is key to learning how to divide code into necessary elements and shared functionality. Making the decision to divide shared functionality into discrete elements is experienced based. If you see the same code more than once in a system, it is better to make it a discrete element.
Consider something as benign as check to see if a user of a command is a wizard. While it is trivial code, if that function is separated tomorrow you can expand the code to check if the user is a wizard, or has the royalty or staff flag without much effort. A simple function to check for wizards is better off as checking for an authorized user rather than assuming wizards will be the only users of the functionality.
Pick a style. Stick with it. Never deviate from the style within a system. Many articles have been written on this topic. If modifying a preexisiting system, match the style of the original coder to the best of your ability to keep the system cohesive for the person after you.
Verbosity in naming is your friend. It is tedious when writing the initial code but is immensely useful when you need to fix something in a few months. Consider a function named like FN_USER_OK vs FN_AUTHORIZED_USER. The former might be a validation check or an authority check. The latter makes it clear what the function does.
Always prefer to refactor code as you write it. If you write the same block of code more than once or see an opporunity to cut and paste....move that code into its own function -- and FIX the original block to use the new function. Do not allow yourself the luxury of cut and paste. Keep all code short and simple.
Along the same lines, if there are minor differences between several similar functions, consider how you can make the basic code more generic to support all the cases. Identify if passed arguments are moot are unique. Fix the earlier code.
Document the code to the best of your ability. It is difficult inline with MUSHcode but can be accomplished by using good names for attributes. Consider utilizing one of the many offline formatting techniques for larger systems. Do NOT mix online coding with offline coding. Eventually you will forget to merge a change and chaos will ensue.
Prefer to write obvious code rather than intricate optimizations unless absolutely necessary. Code that is readable is supportable. Most optimizations are neither necessary or supportable when you think of them as you code. If the code is well written, optimization can be done as needed for scalability. The key is to make it work in a fashion that anyone can read it. Premature optimization is most likely to make it less maintainable by you and by others.
Do not make assumptions. If you do not know what the answer on a requirement, find it before you code it. Avoiding assumptions makes the system flexible, maintainable and hopefully scalable. If you must make an assumption, parameterize that assumption independently of the code.
Test both code and the system. I tend to write code in increments and poke at functions and commands as they are completed. That ensures I have functional code that does what I expect but it does not ensure the code is correct for the system.
Incremental testing is extremely useful and avoids a lot of code problems. Test every bit of code you write. Make sure it does what it expected but also test with garbage to make sure it handles that as well.
Then test with the expected and unexpected. I find the latter hard to do but there are many individuals who are skilled at it. Ask other staffers and trusted players to do their best to break it. Let them find nuances you didn't expect.
Rinse, Wash, Repeat
None of this comes overnight. The capability increases with experience. Even after years of experience, you are unlikely to be skilled in all areas. You'll do better in all of them but will probably find yourself lacking in one or more. That's okay. Concentrate on improving that area and ask for help if you discover you aren't very good at it.