Bjarne Stroustrup, the creator of C++, is believed to have said that programming in C++ involves writing libraries and then using them. Having worked with C++, Perl, PHP, Python and Javascript for the past ten years, I'm inclined to think the principle applies to more than one language. Especially the object oriented variety.
The idea makes sense. Large problems need to be broken down into building blocks. Then you must first build the building blocks before you use them to build the building. Build the building blocks wrong and the building comes out wrong.
How do you go about designing the building blocks? Based on your building's requirements, of course. Strangely though, in the world of computing, some programmers (including my past self) tend to forget this basic principle and try to stand the process on its head.
Bottom-up
Bottom-up API design, at least the variety I have seen, tend to go like this:
Programmer needs a User class, whose basic responsibilities include storage, retrieval, authentication and access control. He starts implementing the following functions, sometimes in an almost knee-jerk fashion:
// NOT in any particular language class User { function User(username, fullname, password, role) { this.username = username; // etc. } function insert() { // insert this object's attributes to database } function update() { // update row with this.username with this object's attributes } static function find(username) { // return object with data selected from row with this.username } }
Now he starts using it in his application. Any functions that he realizes he's missed out, he has to implement while writing the application. If the function names, parameter order, scoping and other syntax looks odd in actual usage, tough luck. Either he has to live with it or go back and fiddle with the innards of his "library". Doing so is not all that different from developing monolithically.
Top-down
I have not developed anything in this way in years.
The reverse process has worked well for me over the years and with several languages and for both web and enterprise applications ranging in size from 10,000 lines of code to several hundred thousand lines of code. Still, that does not mean it will work for everyone.
I start by imagining that the perfect API already exists and starting implementing the application using it:
e.g.
// in create handler User user = new User(context['username'], context['fullname'], context['password']); if (!user.insert(db)) { // handle error return false; } // in the login handler User user = User::find(context['username']); if (user == null) { // handle error return false; } // etc.
This allows you to chose the optimal grammar for your library before you start implementing. It also allows you to implement only those functions you need, saving time, which is often at a premium for enterprise projects. You also know the error handling behaviors expected by the application.
There is another, more important advantage: when you're designing the library, you are in an application developer mindset than a library developer mindset. You automatically adhere to certain principles which library developers have to continually remind themselves of.
Notable examples:
There is another, more important advantage: when you're designing the library, you are in an application developer mindset than a library developer mindset. You automatically adhere to certain principles which library developers have to continually remind themselves of.
Notable examples:
- Never force the application developer to do something the library can do itself (avoid boilerplate application code)
- Favor convention over configuration, except when you want to override (provide defaults, minimize configurations)
- Do not do two things in one function
- Do not create unnecessary states
- Favor functions for logic and objects for state; avoid "services", "handlers", "managers", "factories", "adapters", "validators" etc.
- Favor encapsulation over abstraction for dealing with complexity
- Use short function and attribute names based on context (e.g. user.insert(db) or db.insert(user) better than db.insertUser(user) or user.insertIntoDB(db))
- If you start tightly coupling your libraries together, what you will end up with is a framework (e.g. if the database handler is retrieved inside the User::insert function, you can no longer supply an alternative database handler)
That said, no matter how well you follow the rules, you are unlikely to get the most optimal solution the first time. Be prepared for rework. At least implement several alternative application examples before you settle on a library interface to implement.
Like it is in the literary world, so it is in programming -- the essence of writing is rewriting.
Like it is in the literary world, so it is in programming -- the essence of writing is rewriting.
No comments:
Post a Comment