This might be a good time to address the system architecture, so here goes...
At present, the architecture has only been broken down to the subsystem level, as described in architecture/Architecture Overview.txt file in version control.
Before we begin, it's important to remember that the primary goal of any architecture should be to minimise the complexity of the system. Taken as a whole, a software project is simply too big for any one person to deal with at one time. To quote Edgar Dijkstra, one of software eningeering's greats, "The competent programmer is fully aware of the limited size of his own skull. He therefore approaches his task with full humility, and avoids clever tricks like the plague.".
For that reason, software development is largely concerned with breaking down the program into smaller, more manageable, chunks. The first division is into subsystems. A subsystem is simply a group of classes and functions that all serve a common goal. Examples would be that part of the program that handles the interface, or the database, or external devices, etc.
We further divide subsystems into classes. Classes and object-object programming have revolutionised the way we write code. Simple put, an object in a program is analagous to an object in real life. For example, if you were writing a program that deals with cars, you would would have many types of "Car" object. This is where the distinction between a class and an object come in. A class is basically a template for objects; in this case you would have one "Car" class that describes what kind of attributes and a car can have - eg. it has a colour, a top speed, a fixed number of doors, etc. From that one Car class, you create multiple objects that represent different types of cars. You might create a object called "ferrari" of the type "car" that has red paint and two doors, as well as a object "mazda" with blue paint and four doors.
I'll go into objects in more detail later, but it's absolutely crucial that you understand the concept, so I very highly recommend reading the
Wikipedia article on object-oriented programming and the OOP section of the previously posted C++ tutorial if you are not familiar with OOP.
Classes are further broken down in to functions. Functions are small groups of code that have a single, well-defined, purpose. For example, a you might have a function called "showMainWindow" that will pop-up the main window of the program.
I'll explain objects and functions in much more detail once we get to those levels of architecture, but for now it's sufficient that you have the general idea of their purpose.
Managing Complexity
I cannot stress highly enough that managing complexity is software's primary imperative. If you don't make a dedicated effort to make the architecture as simple as possible, the software will eventually enter an entropy death-spiral - gradually becoming more and more complex, until no-one really understands what it does.
We have two primary means of managing complexity: Abstraction and Encapsulation.
Abstraction:
Abstraction is presenting a consistent interface to a class or a subsystem, so that a programmer can concentrate on one level of complexity. An example of every-day abstraction would be talking about a town. You know that a town is a collection of houses, but you can understand the concept of a town without knowing the detail of every single house. That is an abstraction. If you had to know all the details of every single house in order to understand what a town was, that would be a poor abstraction, because thinking about towns would become a very complex business.
From a programming perspective, the principle is the same. A prime example is accessing data from the database. At its lowest level, you access the data using raw SQL queries, which is quite a complex process, relatively speaking. However, often you don't want to know about that level of detail - you want to be able to just call the function
rather than
Code: Select all
findUser("SELECT * FROM users WHERE name='Judas'")
As you can imagine, if you had to constantly worry about building SQL queries as in the second example, it would unnecessarily distract you from your goal - the simple process of finding a user.
This is where we talk about levels of abstraction. Different levels of abstraction deal with the different levels of complexity, so you can address a problem at the right level of complexity.
Running with the above example, you might have to functions: executeSQL(), which executes a SQL query, and findUser(), which simply looks up a user. The findUser() function should handle all of the SQL internally, leaving you free to focus on the task at hand without having to worry about the much more error-prone task of pulling information out of the database.
Encapsulation:
Encapsulation and abstraction go hand in hand. Encapsulation goes one step further than abstraction: not only should you present a consistent abstraction so that the programmer doesn't need to worry about underlying complexity, but you should prevent the programmer from even knowing what underlying complexity is going on. When using the findUser() function, a programmer shouldn't know if the user's data is being retrieved from the database, from the network or just being made up on the spot. If it doesn't matter to the programmer using the function, he shouldn't need to think about it - freeing up the programmers valuable grey matter to focus on other, more important, issues.
The Subsystems
When designing subsystems, you want to keep them as loosely connected as possible. You should be able to pull out a subsystem and replace it with another one, or take a subsystem and use it in another program, with as little difficulty as possible. For that reason, the amount of communication between subsystems should be strictly limited.
For example, if subsystem A which communicates with systems B, C and D, it's going to be very difficult to remove or replace that subsystem - because you'll need to do something about all of those connections to other systems, or else you'll break the program. By contrast, if subsystem A only communicates with system B, it becomes much easier to disconnect the subsystem from the rest of the program.
At this stage of the architecture, we have these subsystems defined:
Databse:
The databse subsystem manages all low-level database operations, such as connecting, disconnecting, and performing queries. The database subsystem should not be able to communicate directly with any of the other subsystems.
Initially, we were going to use a dedicated network subsystem and a client/server model to handle networking. However, because the program will mostly be used on LANs or on VPNs, all of the terminals should have direct access to the database server, so it makes little sense to add the extra complexity. Rather, each computer running the program will connect directly to the database to get the information it needs.
Entity:
The entity subsystem contains all of the classes that map to real-world objects. For example, classes like "User", "Bill", "Order" and "Item" would be found here. They present a higher level of abstraction than the Database subsystem, because you can just search for a user and get a User object rather than searcing the database with SQL queries and getting simple data types.
Furthermore, the Entity subsystem hides the fact that information is coming from a database. When you call User::findUser(), you don't know if the data is going to come from the database, a flat file, or the network, because the entity subsystem will handle all of that internally.
The Entity subsystem is closely tied to the Database subsystem, and can communicate directly with the Database subsystem and no other. The reason for not simply including the Entity classes in the Database subsystem is so that, if need be, we can add a network subsystem later, and hide that from the rest of the program with the Entity classes.
Application Classes:
The application classes are the backbone of the system - they perform most of the logical processing, and control most of the program's operation. The Application Classes subsystem can communicate with the Entity, GUI, and Devices subsystems.
GUI:
The GUI subsystems controls the program's user interface. The main reason for making this a subsystem on its own is so that we can muck around with the UI as much as we like without disrupting any of the rest of the program. The GUI subsystem can't directly communicate with any other subsystems.
Devices:
The device-related classes - such as printers, fingerprint scanners, cash drawers, etc. are stored in their own subsystem because this will likely be the more volatile and complex part of the program. We definitely want programmers working on the application classes to be able to use printers, drawers, and other hardware without needing to worry about the underlying drivers, protocols, etc.
The communication between the subsystems could be summed up with this diagram:
I should highlight that the design is in no way fixed - indeed, this is about the sixth design that I've come up with
. So, if you have any ideas on how to make the design better or simpler, don't hesitate to say so!
'One will rarely err if extreme actions be ascribed to vanity, ordinary actions to habit, and mean actions to fear.'
- Friedrich Nietzsche
'Do not argue with Judas, nube, that would be foolish!'
- D3PART3D