new GTC Architecture
Graphic::Toolkit::Color 1.9 brought several big new features which I will write about when 2.0 comes out - just to sum up what changed since 1.0. This time I want to describe the internal changes, since this release completed an in-depth rewrite. So this will be about software engineering, architecture and coding style. TLDR: simple, clear, DDD, OO by composition and arg and a color space DSL!
GTC is a compact but not too small project (~180kB pure code) without dependencies - my chance to create something perfect - right ? At least I could test my ideals, which are:
Architecture made of few ideas and pieces, that can contain further substructures.
compact file form: small, single purpose packages, methods, blocks; files structured from general to the specific, starting with one line description
telling, consistent names and naming schemes across all types of identifiers and package.
These three points support and reinforce each other. For instance:
- If you start with a simple structure of dummy packages and give them good names at the beginning many of the other important names for methods and variables will reveal themself early -> clearer code and much less refactoring.
- It is easier to move around smaller packages in the hierarchy to find their best spot.
- It is also easier to find a fitting name for a small, single purpose unit.
3. Naming Scheme
And once you settled for a name (and written it down in a project wide glossary, which is documentation) you use this name for that thing every in all categories of names (packages, methods, vars, handles, comments, documentation). They instantly start to cross reference each other and are much more understandable and way more helpful. (You ever wished the code talk to you in the same language as the documentation?). And if you code start to look like a textbook for children - good. You show how much you are a genius by having little trouble with you code. To make this clear here some examples from GTC (which is not perfect - yet):
For instance: a variable containing a color space name is always named "space_name" - in any method - in any package. And there is a class: G.::Toolkit::Color::Space, which has a method called "name", which returns the same thing a variable "space_name" contains. Makes sense?
Second example: Since color is the main topic of the library its kinda implied as a context everywhere unless stated otherwise. That is why i call everywhere a tuple (ARRAY ref) with the values of one color "values" because these are the color - values. This might sound like a bad idea since its too broad of a term. But I never use it anywhere for anything else. It is a stable marker and it refers the the package GTC::Values the home of the values of one color.
2. Discipline
Also having short well structured files, with short methods turned out to be very helpful. I already posted about it elsewhere and will be also here. I found to be a sure sign for code smell if there is a lack of focus in a method. The content has to reflect the name and vice versa. If you have always a stern eye on that, you will catch bad code very early. Plus you make it easier to write libraries one layer above, when they combine functions with a clear purpose. Sounds obvious - but your only good if you are actually doing it. The other nice side effect of small and clear methods / subs is: much less dirty corners where bugs can hide. We all heard the spiel during conference talks: if you practice TDD it will cost time - much less time than hunting bugs. Guess what - same principles apply here - so no lame excuses: "my boss doesn't give me the time to make it pretty". And in contrast to TDD looking into nice code is good for mental health. In contrast tests are like an attic, mostly seen from the outside.
1. Architecture
GTC has three important API - the whole structure is just a byproduct of them.
The first is of course the package Graphics::Toolkit::Color itself. It is just the public facing API presenting all the functionality in a compact, perly way (GTC = swiss army knife for color computing). I hide a lot of the power by having not that many methods and reusing some argument names (in case they mean the same) throughout the methods (small footprint in the mind). Just by combining arguments you get what normally different methods do. There are entire modules on CPAN doing as much as one argument of one method in GTC. As Mark Overmeer says: small modules are incomplete. I go a step further by proclaiming: if you are able to stack methods, choose and combine their arguments, you can write one liners that are immensely powerful (just like perl). This works best if you have only one kind of color object, that has all the methods - hence the unified public API. There we have a lot of documentation (in POD and return messages) and the argument cleaning in the GTC main package. This is all the knowledge about communicating with the outside world in one place - good for maintainance and already more than enough, while try to follow the policy: small files! . So all the real functionality is in the iceberg below, which can be chopped in handy slices.
The most important way to slice complexity is to separate out all the color space specific code: each space into one file. GTC supports 15 different color space and with version 1.93 it will be 18. Some module authors prefer to offer a plugin API for that, but i don't think this is the best idea. Writing code for a color space is fire and forget, since color space definitions almost never change. So why creating the burden for 10 people to maintain 20 modules (because CPAN or perl requirements do change over time). They would have to maintain distributions without touching the productive code. I went the other route and made it as easy as possible to write the code for a color space, which will become part of the normal code base (all bases belong..). You can write a patch with 10 or 20 lines of perl (if it gets complicated) and never have to think about it again.
The class Graphics::Toolkit::Color::Space provides basically a DSL that lets you define everything you need. The name, an alias, axis names, value ranges, value precision, axis types (linear or angular), converter code (between normalized values), additional space constraints, additional formats, value formats. You can even add a value converter. We needed that for the NCol color definitions, which are very friendly to the human eye - but you still want to calculate with the values, so you need a translation between both. So you get every option you would dream of to tweak the space behaviour - but still keep the common problems already solved and handled inside the GTC::Space class.
Because this class is so big - it clearly violates the small files policy. But no problem - I cut it in 5 parts. One package has the utilities I need also elsewhere. Space::Basis handles the axis names, axis short names, looking if a tuple has right amount of values or a HASH color definition has the right value names to fit this space. Then we have the Space::Shape, which cares about the value ranges, normalisation, rounding, clamping of values and so on. And finally there is package with the IO for all the supported formats. The latter three objects are just attributes of the color space object which mostly only contains the delegation to the attributes and the conversion stuff. OO by composition for the win!
The particular color spaces are then instances of the Color::Space. Even the file/package name tells you that: GTC::Space::Instance::CMYK. Such a package has code that runs on start time, creating one GTC::Space object, giving him all the data and algorithms and return it as the return value of that package (instead of the notorious 1). These instances are held by the GTC::Space::Hub (Thank Ovid for the inspiration to the name!). The Hub has not much code either, because it is a glorified iterator over all space objects, well and it tracks dependencies for multi hop color conversions. Well there is another side pocket in the architecture: the name space: GTC::Name which translates values into color names and vice versa.
But generally speaking GTC::Space + Space::Hub is the mid level API. All packages that actually calculate colors are not concerned by the gory details of a color space or how to convert something, which makes them too more readable. And in case you missed it, the low level API (for those gory details) is the mentioned color space DSL, which is AFAIK complete now - hence we approaching 2.0 and I am happy. Hope sincerely you too.
Leave a comment