Designing the architecture of software is very important phase is the software lifecycle.
Rules of thumb:
A good piece of software would follow several rules of thumb:
- Reusability – The piece of software should be as reusable as possible (unless it is applicative), and reuse other components as much as possible.
- Extensibility – The piece of software should be as extensible as possible – i.e. have enough places which other developers (or the original developer in a later time) can use to add more functionality.
- Functionality – The piece of software should make the life of its user easier. The 80-20 rule would fit here (at least 80% of the time…)
- Order – The piece of software should be organized for easy finding of each piece of code, and easy thinking of a good place for a new piece of code.
- Traceability – The piece of software should log its actions in details, preferably in different levels of detail. Usage of extensible logging libraries (such as log4net) are welcome.
- Security – The piece of software should not reveal security leaks, that would allow malicious code to abuse (I am talking about things like code injection, unauthorized privileges modification, system denial of service, etc.)
- Portability – The piece of software should be as portable as possible. Not necessarily between operating systems, but generally between computers. Zero installation (aka -xcopy deployment) is always welcome.
When writing software, the reusability factor is crucial, especially when you have deadlines. Splitting your code into several well-defined libraries, that each can be used in other projects, would usually yield lower cost factor in terms of time,effort and money when several projects are involved.
If you only have one project, you would still want to reuse. You should still split reusable code to a library of its own (or at least isolate it, so it could be split into a library at a later time). You might also would like to look for libraries that are already out there.
When not to reuse
One case where reuse is not welcome is a very specific case of the NIH - “Not Invented Here” syndrome. This is the case where the potentially reusable code from an external source does what your business is all about. Joel Spolsky has a blog post about it.
Another case, where reuse should be postponed, is the lack of understanding and/or foreseeing of the correct way to build the reusable code. This would usually be after the relevant code was only written once or twice. I’ll explain: You write a piece of software that does a certain task, and you feel this task might repeat itself in the future. You go and export it to a library. Then, after a while, you want to use it again, somewhere else, but you see that it is not extensible enough, portable enough, answer your functionality requirements, etc. You still have to write your own code. Or you have to fix the library to fit the two projects, then go and fix the old project that already uses it. Fixing the library at this point would be hard, since the new project’s requirements were not taken into consideration in the original design. When a 3rd project would need it, this might repeat itself again.
It is usually a good idea to wait until you have requirements from at least three different projects before you decide to make a piece of code a reusable library.
Extensibility allows adding new functionality in the future. There are several types of extensibility: External extensibility points, and internal extensibility points.
External extensibility points are the most important. The developer of a reusable piece of code should take into account the need of another developer to add functionality, or do things a little different from his own point of view.
For an example, suppose you have a class that contains a user control, and that user control is visible to the end-user. You could wrap it completely, only supplying the functionality you think relevant. But in some cases, a developer might want to tweak that control, and you might have not given him the option to do his little tweak (say, change the font or color). Exposing the wrapped control itself would allow the programmer to tweak it.
Extensibility Best Practices
Another extensibility rule of thumb would be to always expose functionality through interfaces, and if you want the user to be able to implement them in introduce the created objects to your library, use the factory pattern, and allow the user to register his own types. This allows you to replace the code behind the interface as much as you want without the user needs to wary about it, and also allows the user to introduce his own types.
Generally, prefer to introduce interfaces rather than base classes. If the user need to integrate your code into a current project, a base class might prevent him from doing so, as he might need to add functionality to a current class he already has, that already inherits another base class. With interfaces, this is always possible.
If you have a method that returns a result, prefer creating a specialized result class and fill it with the relevant data. If you’d like to add data in the future, this would be as easy as add fields to the already existing result class. In .Net this is also true for EventArgs – create your own even if they are empty. They might get filled later on.
When writing software, make sure each portion of the software does what it needs to do, and does not do what it doesn’t need to do.
When thinking where to place each method that implement some of the functionality required, try to group together methods that have related functions. Try to avoid duplicating functionalities, just because of small differences. If you must, do it in smart way, reducing code duplication to the minimum necessary. Use helper class or helper methods as possible, and make them static, to reduce class state artifacts.
The exposed functionality should fit the user needs. In reusable code, don’t try to cover 100% of the cases. At least 10% of them are so rare you won’t even get to think about. Make a good, thorough research and find the 80% case that is relevant to you. Don’t look at other unrelated businesses research. They do things differently since they have different audience. Look inside your own market. Even in your own business. You’ll find plenty of information there. If you don’t – it only means this might be a functionality you don’t need to reuse. Make it applicative, and save others the burden of understanding why this is needed.
Don’t use intrusive technologies in your code, and if you do, document them and the reason you use them. For instance, don’t do communication for traceability. The containing project might be affected by it. If you must, make sure everything is configurable, including the traceability feature itself.
When building your project tree structure (in terms of code files), make meaningful separation by logic, not by physical terms. Think where a new-comer would look for a certain file. If you can’t – ask a new-comer to find that file for you just by looking at the tree structure.
The same goes for using the “region” directives. I see endless “public methods” and “private properties” regions. Where would I find the method that performs the layout? or the methods that respond to user input events? I don’t know. In either of them. Better regions would be based on functionality groups. Region such as “User Interaction”, “Layout Logic”, “Construction and Initialization” are better.
The software should log its actions and its errors. This logs are later used to analyze software usage for performance, errors, misbehavior, etc.
Use a common logging method for all parts of the application. A highly configurable and extensible logging method is preferred, because in the horrible case of mismatching with another logging facility used by some 3rd party component, it can be configured or extended to match it, so everything behaves like one logging method.
Security is a very important issue, especially when the software is exposed to a large user base. You don’t know which of them might want to harm you, or your client.
When making database queries, make sure to sanitize the queries to prevent malicious SQL injection, that can result in loss of critical data.
When asking for input of any kind from any source (and this one is especially relevant in native environments, like C and C++), make sure you have enough room for it, to prevent code injection.
A portable software is much better that a software you need to install. Copying the files to place is the easiest thing to do, and it doesn’t mess up the registry. In .NET all you need to do is make sure all the required DLLs are there, near the main executable.
You might want to use tools like ILMerge to reduce the number of DLLs in your distribution.
When writing .NET software that should run on Linux, use MoMA to make sure your application and dependencies are portable.
When writing a portable application that need to be run from a portable memory device (such as a disk-on-key), make sure to write the relevant data next to the application itself.
That’s it for now about writing quality software, a quick review of architectural design. I’ll try to add more about that subject later on, and write about other aspects of writing quality software, such as user interface design, testing your application and modules, etc.