4 ms·
Ok but under-engineering can also be a problem. Say you and your team are going to build a REST server that will serve hundreds of endpoints. Nothing complica
by gerbilly 10y ago
Ok but under-engineering can also be a problem.
Say you and your team are going to build a REST server that will serve hundreds of endpoints.
Nothing complicated, just a bunch of CRUD code with some business logic.
So here is the simple way to write each endpoint function, or method.
RESTResponse getUserPrefs(Userid id)
{
return DB.getUser(id).prefs;
}
except you need security and code to setup and teardown DB the transaction.
RESTResponse getUserPrefs(Userid id)
{
if( !Security.checkAllowed(id) )
return REST.403;
RESTResult result;
DB.getConnection();
DB.beginTransaction(true /*Readonly*/);
RESTResult result = DB.getUser(id).prefs;
DB.endTransaction();
DB.closeConnection();
return result;
}
Now imagine giving this example to a team of devs to develop those hundreds of bitty REST endpoints.
Also as you can imagine, the boilerplate introduced here may have to change per endpoint. There might also be other boilerplate that may need to be added for object level security, authentication, error logging etc ...
So when you throw this code at your developers, it means that you'll end up with hundreds or thousands of little methods all similar but all different.
They are going to cut and paste this example and modify as they go. They may come up with better ways to structure the boilerplate, leading to some old code mixed in with code written in a newer style.
Good luck working with that after a few years.
All these encomia for simple code usually considers only a single small program, with one person working on it.
Once code is loosed in the wild for a team of maintenance programmers to work on, you have a whole new problem, and lack of structure form the simple approach will cause weeds and vines to choke your codebase.
- acveilleux 10y agoGenerally, it's much easier to add complexity than remove it once it has been added. > So when you throw this code at your developers, it means that you'll end up with hundreds or thousands of little methods all similar but all different. If that's what happens with your developers, you need new developers or new management, clearly the ones you've hired are either incompetent or their management is forcing them to suck.
- gerbilly 10y ago> If that's what happens with your developers, you need new developers or new management I don't think so, it's just human nature and other pressures besides the purely technical ones: like the pressure to just get a task done, or to get the job done as easily as possible, and so on...[1] As programmers we tend to think the the biggest challenges are technical, but they aren't: the social problems are a way bigger factor. Also the problem I'm describing here applies to large low-complexity codebases, to be worked on by large teams. These are common in 'enterprise' settings, which is why I think design patterns have so much appeal there. [1] https://en.wikipedia.org/wiki/Tragedy_of_the_commons https://en.wikipedia.org/wiki/Tragedy_of_the_commons
- junke 10y agoI couldn't resist making a Common Lisp version. DB, REST and SECURITY are imaginary packages. CLSQL is an existing library for interfacing with databases. You define the meat of your code with a primary method: (defmethod get-user-preferences (id) (user-preferences (db:get-user id))) Then, you add security checks with an AROUND qualifier; CALL-NEXT-METHOD is a little like super() in Java, but with multiple-inheritance and method qualifiers (around, before, after, ...): (defmethod get-user-preferences :around ((id integer)) (if (security:allowedp id) (call-next-method) rest:+FORBIDDEN+)) You can add logging, executed before the primary method, after security checks. (defmethod get-user-preferences :before (id) (log "Getting user preferences for id ~S." id)) Likewise, you could log the outcome of the security check; (defmethod security:allowedp :around (id) (let ((result (call-next-method))) (log "Access ~:[denied~;granted~] for id ~S." result id) result))) Also, you can wrap the call to db:get-user with the appropriate dynamic context where a database connection is open (it could be from a pool, or something else) within a transaction. (defmethod db:get-user :around ((id integer)) (clsql:with-database (db *database*) (clsql:with-transaction (:database db) (call-next-method)))) I did not find an equivalent for the read-only flag. If more methods need to follow this pattern, it might be preferable to define them directly with a macro which always open the connection and the transaction. The WITH-X macro ensure that transactions and connections are properly handled when unwinding the stack.
- gerbilly 10y agoAOP huh? Nice. The scenario i described above was from a real project I was on a few years ago. The way I handled it (partially) was via the use of AOP. I also made use of dependency injection. These two approaches combined managed to reduce the boilerplate to near zero. Of course, in Java AOP means you have to take care to enable it in each part of the toolchain (build system, CDP, IDE ...) but it wasn't too bad. I'm not sure which gang of 4 patterns I may have been using, and I don't care, but I do know that leaving it simple would have led to chaos, because I'd seen it happen before.