Code != Clothing, or How to neatly structure your code

Having been worked with countless technologies , frameworks  and 3rd party libraries, I can say that I have seen almost every possible way of organising the code for a project. From the approach of putting every file in a single folder “just for now” (where “now” becomes “forever”, because moving things around is just too complicated), to the one of creating humongous, monolithic do-everything libraries (which I nicknamed “walls of code”), to have everything is in one place, to the theoretically more rational modular system, where files are organized in sub-folders.

The reason why I stress the word “theoretically” is that, while the idea is certainly good, it can still lead to a messy, hard to maintain mass of files. The key of everything, in this case, is finding what logic should be used to structure the code. It may seem a simple question to answer, but the way one answers to it can lead to nasty surprises.A common, yet, in my opinion, very inefficient way of organizing the code is what I call The Sock Drawer. I gave it such name because it reminds me of how most people manage their clothing. Let’s have a look at it in detail and see why I think it’s not appropriate for code.

The Sock Drawer

The Sock DrawerSince childhood, we are taught that keeping our room tidy is a good thing. The motto is “a place for everything, and everything is in its place”. If we apply this logic to out clothing, it seems obvious that the best thing to do would be grouping them together. Socks in a drawer, underpants in another, shirts in another. Trousers in yet another drawer, separating the good ones from the old, consumed ones we use when gardening, and, finally, shirts, jackets, suits and elegant clothing in the wardrobe (again, grouped by purpose).

Once finished, we can see how everything is tidy and organised. And it works perfectly, too! Time to dress up: open drawer, here’s a t-shirt. Open another, here are the underpants. One more drawer, here are the socks. Trousers, shirt, jacket, everything in its place. We only have to match the type (elegant/casual) and the colours, and we’re done! It’s such a natural thing, it only makes sense to apply it to our code… Or is it?

The Code in the Sock Drawer

By following the same principle we apply to our clothing, we come to the conclusion that code of the same type goes in the same place. Let’s take, for example, Magento, a popular e-commerce derived from Zend framework which follows the MVC principle and organises the code using the Sock Drawer technique. Here’s the typical structure of a plugin providing some functionality and requiring both a frontend and a backend interface:

  • /app
    • /code – All Model and Controller code goes in a sub-folder of /code
      • /local
        • /MyNamespace
          • /PluginName (this
            • /Block – Some Controller code goes here
            • /controllers – Some other Controller code goes here
            • /etc – Plugin’s configuration XML files go here
            • /Helper – Helper code goes here
            • /Model – Models code goes here
            • /sql – SQL code goes here
      • /core
        • [folder tree analogous to the one found in /local]
      • /community
        • [folder tree analogous to the one found in /local]
    • /design - Part of the View code goes here (PHP code)
      • /adminhtml
        • [several other nested folders]
      • /frontend
        • [several other nested folders]
    • /etc
      • /modules - Configuration files that enable plugins go here
  • /skin - Part of the View code goes here (CSS and JavaScript)
    • [several sub folders, each one containing CSS, images or JavaScript files]

Apart from the difficulties in following such structure, two problems arise from this layout:

  1. Development becomes significantly more complicated. While installation on a production site might be a simple matter of copy/pasting folders (if the structure is correct), Developers don’t copy and paste files every time they change, they use symlinks. This means creating a dozen or so symlinks (if not more), and recreate them if a file gets renamed. It’s definitely a mess. Put a link in the wrong place, and thing will suddenly break, apparently without reason.
  2. Uninstalling a plugin becomes a nightmare. Remove a folder here, another there, a file here, a file there… No, not that file. Wait, the other one.
OpenCart, another popular e-commerce, is also “guilty” of using such practice, with the difference that your plugin’s code is divided in files, each one going in a different folder inside the framework’s folder tree.

While initially it looked like a good idea, sorting the code by type revealed itself not to be such a great choice. So, how comes this works with socks? That’s because clothing is interchangeable, while code is not necessarily so. A pair of socks goes well with several trousers, which go well with several shirts, which go well with several jackets. You reuse small components and make an outfit out of them. On the other hand, the code for a plugin, library or framework is designed to go together and should stay together.

A better approach: the Uniform

Fortunately, there’s a simpler way to organise your code. Keeping the comparison with clothing, think of your plugins, modules or libraries as uniforms. You need shirt, jacket, trousers, socks, tie and shoes, and they must go together. You store them together, you use them together, you throw them away together. They all stay in the same storage bag.

This is the approach used by Vanilla Forums: a plugin goes into its own folder, inside which you’re free to structure the code as you wish. Here’s how the folder structure looks like:

  • /plugins
    • /MyPlugin
      • /SomeFolder – Name it as you like
      • /SomeOtherFolder - Name it as you like
      • class.myplugin.php

Sure, inside the MyPlugin folder one will still have “drawers”, but they will only contain resources needed by your plugin. All the pieces are well organised, but in one place. Let’s see how this compares to previous method:

  1. Development is easy. Just create one single symlink to your plugin folder, and you’ll magically have it installed.
  2. Deployment is also easy, just one folder to copy.
  3. Removal is trivial: delete one folder, plugin is gone. No files left around, no accidental deletions.

It seems clear to me which of the two methods is the most practical.

Bottom line

Clearly, I favour the second method over the first. Code is not clothing, and what works together must stay together. If two or more components need to share resources, move those into a separate component. Make your libraries live next to each other like good neighbours, but don’t let them become house mates, and you won’t have fighting for the TV.

Photo by Jeremy Beagle

flattr this!

Speak Your Mind

*

  Globals Profiler (1,474.80 ms) SQL (60 queries in 254.08 ms) Errors (14, 1!) Toggle Close
$_GET = array (
);

$_POST = array (
);

$_COOKIE = array (
);

$_SESSION = array (
);

$_SERVER = array (
  'SERVER_SOFTWARE' => 'Apache',
  'REQUEST_URI' => '/2012/10/03/how-to-neatly-structure-your-code/',
  'DOCUMENT_ROOT' => '/home3/bestmant/public_html/dev.pathtoenlightenment.net',
  'GATEWAY_INTERFACE' => 'CGI/1.1',
  'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
  'HTTP_ACCEPT_ENCODING' => 'gzip',
  'HTTP_ACCEPT_LANGUAGE' => 'en-us,en-gb,en;q=0.7,*;q=0.3',
  'HTTP_CF_CIP_TAG' => '0',
  'HTTP_CF_CONNECTING_IP' => '54.234.180.187',
  'HTTP_CF_IPCOUNTRY' => 'US',
  'HTTP_CF_RAY' => '73460c4ccbb020e',
  'HTTP_CF_VISITOR' => '{"scheme":"http"}',
  'HTTP_CF_WAN_ENCODING' => '0',
  'HTTP_CF_WAN_ID' => '0',
  'HTTP_CONNECTION' => 'Keep-Alive',
  'HTTP_HOST' => 'dev.pathtoenlightenment.net',
  'HTTP_USER_AGENT' => 'CCBot/2.0',
  'HTTP_X_FORWARDED_FOR' => '54.234.180.187',
  'HTTP_X_FORWARDED_PROTO' => 'http',
  'PATH' => '/bin:/usr/bin',
  'QUERY_STRING' => '',
  'REDIRECT_STATUS' => '200',
  'REDIRECT_URL' => '/2012/10/03/how-to-neatly-structure-your-code/',
  'REDIRECT_W3TC_ENC' => '_gzip',
  'REDIRECT_file_gzip' => '/ramdisk/cpud/status',
  'REMOTE_ADDR' => '54.234.180.187',
  'REMOTE_PORT' => '39161',
  'REQUEST_METHOD' => 'GET',
  'SCRIPT_FILENAME' => '/home3/bestmant/public_html/dev.pathtoenlightenment.net/index.php',
  'SCRIPT_NAME' => '/index.php',
  'SERVER_ADDR' => '66.147.244.108',
  'SERVER_ADMIN' => '[email protected]',
  'SERVER_NAME' => 'dev.pathtoenlightenment.net',
  'SERVER_PORT' => '80',
  'SERVER_PROTOCOL' => 'HTTP/1.1',
  'SERVER_SIGNATURE' => '<address>Apache Server at dev.pathtoenlightenment.net Port 80</address>
',
  'W3TC_ENC' => '_gzip',
  'file_gzip' => '/ramdisk/cpud/status',
  'PHPRC' => '/home3/bestmant/public_html/:/etc/php53/',
  'PHP_SELF' => '/index.php',
  'REQUEST_TIME' => 1369339705,
);

Profiler Initiaded 0.0000 ms 21541 kB
Profiler Noise 0.0319 ms 21542 kB
Profiler Stopped 1,474.7980 ms 51455 kB
0.7498 [ms]
show tables;;
0.5109 [ms]
SELECT option_value FROM wp_options WHERE option_name = '_wc_session_Yil6KG5jJrtzw4QC2BQH1SnAsmfBGMfd' LIMIT 1;
1.4369 [ms]
SELECT   wp_posts.* FROM wp_posts  WHERE 1=1  AND YEAR(wp_posts.post_date)='2012' AND MONTH(wp_posts.post_date)='10' AND
DAYOFMONTH(wp_posts.post_date)='3' AND wp_posts.post_name = 'how-to-neatly-structure-your-code' AND wp_posts.post_type = 'post'  ORDER BY
wp_posts.post_date DESC ;
0.6340 [ms]
select val from wp_wfConfig where name='liveTraf_ignoreIPs';
1.9281 [ms]
select val from wp_wfConfig where name='liveTraf_ignoreUA';
5.1420 [ms]
insert into wp_wfLeechers (eMin, IP, hits) values (floor(unix_timestamp() / 60), '921351355', 1) ON DUPLICATE KEY update hits = IF(@wfcurrenthits :=
hits + 1, hits + 1, hits + 1);
0.4771 [ms]
select @wfcurrenthits;
1.1208 [ms]
select val from wp_wfConfig where name='blockFakeBots';
0.9050 [ms]
select val from wp_wfConfig where name='maxGlobalRequests';
1.1821 [ms]
select val from wp_wfConfig where name='maxRequestsHumans';
1.1032 [ms]
select val from wp_wfConfig where name='liveTrafficEnabled';
2.7788 [ms]
SELECT post_id, meta_key, meta_value FROM wp_postmeta WHERE post_id IN (150);
5.0330 [ms]
SELECT * FROM wp_users WHERE ID = '1';
9.1488 [ms]
SELECT user_id, meta_key, meta_value FROM wp_usermeta WHERE user_id IN (1);
0.8440 [ms]
SELECT   wp_posts.ID FROM wp_posts  WHERE 1=1  AND wp_posts.post_type = 'safecss' AND (wp_posts.post_status = 'publish')  ORDER BY wp_posts.post_date
DESC LIMIT 0, 1;
1.7910 [ms]
SELECT wp_posts.* FROM wp_posts WHERE ID IN (188);
1.0641 [ms]
SELECT post_id, meta_key, meta_value FROM wp_postmeta WHERE post_id IN (188);
11.6720 [ms]
SELECT t.*, tt.* FROM wp_terms AS t INNER JOIN wp_term_taxonomy AS tt ON tt.term_id = t.term_id INNER JOIN wp_term_relationships AS tr ON
tr.term_taxonomy_id = tt.term_taxonomy_id WHERE tt.taxonomy IN ('faq_category') AND tr.object_id IN (150) ORDER BY t.name ASC;
0.4928 [ms]
SELECT option_value FROM wp_options WHERE option_name = 'text_direction' LIMIT 1;
6.5031 [ms]
SELECT t.*, tt.* FROM wp_terms AS t INNER JOIN wp_term_taxonomy AS tt ON tt.term_id = t.term_id INNER JOIN wp_term_relationships AS tr ON
tr.term_taxonomy_id = tt.term_taxonomy_id WHERE tt.taxonomy IN ('post_tag') AND tr.object_id IN (150) ORDER BY t.name ASC;
18.5900 [ms]
SELECT   wp_posts.ID FROM wp_posts  WHERE 1=1  AND wp_posts.post_parent = 150  AND (wp_posts.post_mime_type LIKE 'image/%')  AND wp_posts.post_type =
'attachment' AND (wp_posts.post_status = 'inherit')  ORDER BY wp_posts.post_date DESC LIMIT 0, 5;
43.1590 [ms]
SELECT wp_posts.* FROM wp_posts WHERE ID IN (151);
2.4660 [ms]
SELECT post_id, meta_key, meta_value FROM wp_postmeta WHERE post_id IN (151);
16.6481 [ms]
SELECT t.*, tt.* FROM wp_terms AS t INNER JOIN wp_term_taxonomy AS tt ON tt.term_id = t.term_id INNER JOIN wp_term_relationships AS tr ON
tr.term_taxonomy_id = tt.term_taxonomy_id WHERE tt.taxonomy IN ('post_format') AND tr.object_id IN (150) ORDER BY t.name ASC;
2.5098 [ms]
SELECT t.*, tt.* FROM wp_terms AS t INNER JOIN wp_term_taxonomy AS tt ON t.term_id = tt.term_id WHERE tt.taxonomy = 'nav_menu' AND t.term_id = 4 LIMIT
1;
4.6690 [ms]
SELECT tr.object_id FROM wp_term_relationships AS tr INNER JOIN wp_term_taxonomy AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id WHERE tt.taxonomy
IN ('nav_menu') AND tt.term_id IN ('4') ORDER BY tr.object_id ASC;
5.1100 [ms]
SELECT   wp_posts.* FROM wp_posts  WHERE 1=1  AND wp_posts.ID IN (10,13,68,89,93,94,182,183,200,389,391,457,829,838) AND wp_posts.post_type =
'nav_menu_item' AND (wp_posts.post_status = 'publish')  ORDER BY wp_posts.menu_order ASC ;
0.8352 [ms]
SELECT post_id, meta_key, meta_value FROM wp_postmeta WHERE post_id IN (183);
3.9032 [ms]
SELECT post_id, meta_key, meta_value FROM wp_postmeta WHERE post_id IN (13);
6.1240 [ms]
SELECT post_id, meta_key, meta_value FROM wp_postmeta WHERE post_id IN (68);
0.6819 [ms]
SELECT post_id, meta_key, meta_value FROM wp_postmeta WHERE post_id IN (89);
1.7340 [ms]
SELECT post_id, meta_key, meta_value FROM wp_postmeta WHERE post_id IN (391);
2.7480 [ms]
SELECT post_id, meta_key, meta_value FROM wp_postmeta WHERE post_id IN (93);
3.5591 [ms]
SELECT post_id, meta_key, meta_value FROM wp_postmeta WHERE post_id IN (94);
2.3201 [ms]
SELECT post_id, meta_key, meta_value FROM wp_postmeta WHERE post_id IN (10);
2.9690 [ms]
SELECT post_id, meta_key, meta_value FROM wp_postmeta WHERE post_id IN (829);
0.4449 [ms]
SELECT post_id, meta_key, meta_value FROM wp_postmeta WHERE post_id IN (182);
0.3560 [ms]
SELECT post_id, meta_key, meta_value FROM wp_postmeta WHERE post_id IN (200);
0.3772 [ms]
SELECT post_id, meta_key, meta_value FROM wp_postmeta WHERE post_id IN (389);
6.3539 [ms]
SELECT post_id, meta_key, meta_value FROM wp_postmeta WHERE post_id IN (838);
0.4151 [ms]
SELECT post_id, meta_key, meta_value FROM wp_postmeta WHERE post_id IN (457);
2.3229 [ms]
SELECT   wp_posts.* FROM wp_posts  WHERE 1=1  AND wp_posts.ID IN (11,7,181,158,439) AND wp_posts.post_type = 'page' AND (wp_posts.post_status =
'publish')  ORDER BY wp_posts.post_date DESC ;
2.3251 [ms]
SELECT t.*, tt.* FROM wp_terms AS t INNER JOIN wp_term_taxonomy AS tt ON t.term_id = tt.term_id WHERE tt.taxonomy IN ('category') AND ( t.term_id = 12
 OR t.term_id = 13  OR t.term_id = 59  OR t.term_id = 20  OR t.term_id = 21 ) ORDER BY t.name ASC ;
18.4941 [ms]
SELECT t.*, tt.*, tm.*  FROM wp_terms AS t INNER JOIN wp_term_taxonomy AS tt ON t.term_id = tt.term_id LEFT JOIN wp_woocommerce_termmeta AS tm ON
(t.term_id = tm.woocommerce_term_id AND tm.meta_key = 'order')  WHERE tt.taxonomy IN ('product_cat') AND ( t.term_id = 22  OR t.term_id = 83 ) ORDER
BY tm.meta_value+0 ASC, t.name ASC ;
3.3619 [ms]
SELECT woocommerce_term_id, meta_key, meta_value FROM wp_woocommerce_termmeta WHERE woocommerce_term_id IN (83);
1.2059 [ms]
SELECT woocommerce_term_id, meta_key, meta_value FROM wp_woocommerce_termmeta WHERE woocommerce_term_id IN (22);
3.7279 [ms]
SELECT * FROM wp_posts WHERE ID = 11 LIMIT 1;
2.5721 [ms]
SELECT * FROM wp_posts WHERE ID = 7 LIMIT 1;
0.5939 [ms]
SELECT * FROM wp_posts WHERE ID = 181 LIMIT 1;
5.0449 [ms]
SELECT * FROM wp_posts WHERE ID = 439 LIMIT 1;
1.6341 [ms]
SELECT t.term_id FROM wp_terms AS t INNER JOIN wp_term_taxonomy AS tt ON tt.term_id = t.term_id INNER JOIN wp_term_relationships AS tr ON
tr.term_taxonomy_id = tt.term_taxonomy_id WHERE tt.taxonomy IN ('category') AND tr.object_id IN (150) ORDER BY t.name ASC;
7.8139 [ms]
SELECT t.*, tt.* FROM wp_terms AS t INNER JOIN wp_term_taxonomy AS tt ON tt.term_id = t.term_id INNER JOIN wp_term_relationships AS tr ON
tr.term_taxonomy_id = tt.term_taxonomy_id WHERE tt.taxonomy IN ('category') AND tr.object_id IN (150) ORDER BY t.name ASC;
4.4172 [ms]
SELECT * FROM wp_comments  LEFT JOIN wp_posts ON wp_comments.comment_post_ID = wp_posts.ID  WHERE comment_approved = '1' AND comment_post_ID = 150 AND
 wp_posts.post_type NOT IN ('shop_order')   ORDER BY comment_date_gmt ASC ;
0.9720 [ms]
SELECT t.*, tt.* FROM wp_terms AS t INNER JOIN wp_term_taxonomy AS tt ON t.term_id = tt.term_id WHERE tt.taxonomy IN ('link_category') AND ( t.term_id
= 3 ) AND tt.count > 0 ORDER BY t.name ASC ;
7.9570 [ms]
SELECT *    FROM wp_links  INNER JOIN wp_term_relationships AS tr ON (wp_links.link_id = tr.object_id) INNER JOIN wp_term_taxonomy as tt ON
tt.term_taxonomy_id = tr.term_taxonomy_id WHERE 1=1 AND link_visible = 'Y'  AND ( tt.term_id = 3 ) AND taxonomy = 'link_category'    ORDER BY
link_name ASC;
1.1439 [ms]
SELECT   wp_posts.ID FROM wp_posts  WHERE 1=1  AND wp_posts.post_type = 'post' AND (wp_posts.post_status = 'publish')  ORDER BY wp_posts.post_date
DESC LIMIT 0, 5;
0.6762 [ms]
SELECT wp_posts.* FROM wp_posts WHERE ID IN (807,756,727,689,658);
7.2720 [ms]
SELECT t.*, tt.*, tr.object_id FROM wp_terms AS t INNER JOIN wp_term_taxonomy AS tt ON tt.term_id = t.term_id INNER JOIN wp_term_relationships AS tr
ON tr.term_taxonomy_id = tt.term_taxonomy_id WHERE tt.taxonomy IN ('category', 'post_tag', 'post_format') AND tr.object_id IN (658, 689, 727, 756,
807) ORDER BY t.name ASC;
1.0538 [ms]
SELECT post_id, meta_key, meta_value FROM wp_postmeta WHERE post_id IN (658,689,727,756,807);
1.0049 [ms]
SELECT t.*, tt.* FROM wp_terms AS t INNER JOIN wp_term_taxonomy AS tt ON t.term_id = tt.term_id WHERE tt.taxonomy IN ('post_tag') AND tt.count > 0
ORDER BY tt.count DESC LIMIT 45;
Notice Undefined variable: gasp_check on line 16 in file /home3/bestmant/public_html/dev.pathtoenlightenment.net/wp-content/plugins/growmap-anti-spambot-plugin/growmap-anti-spambot-plugin.php
Strict Redefining already defined constructor for class reCAPTCHA on line 17 in file /home3/bestmant/public_html/dev.pathtoenlightenment.net/wp-content/plugins/wp-recaptcha/recaptcha.php
Strict Redefining already defined constructor for class WPPlugin on line 28 in file /home3/bestmant/public_html/dev.pathtoenlightenment.net/wp-content/plugins/wp-recaptcha/wp-plugin.php
Strict Redefining already defined constructor for class MailHide on line 15 in file /home3/bestmant/public_html/dev.pathtoenlightenment.net/wp-content/plugins/wp-recaptcha/mailhide.php
Notice Undefined index: _wfsf on line 383 in file /home3/bestmant/public_html/dev.pathtoenlightenment.net/wp-content/plugins/wordfence/lib/wordfenceClass.php
Strict Non-static method Jetpack_Subscriptions::init() should not be called statically on line 512 in file /home3/bestmant/public_html/dev.pathtoenlightenment.net/wp-content/plugins/jetpack/modules/subscriptions.php
Strict Non-static method Jetpack_Notifications::init() should not be called statically on line 181 in file /home3/bestmant/public_html/dev.pathtoenlightenment.net/wp-content/plugins/jetpack/modules/notes.php
Strict call_user_func_array() expects parameter 1 to be a valid callback, non-static method Jetpack_Post_By_Email::init() should not be called statically on line 406 in file /home3/bestmant/public_html/dev.pathtoenlightenment.net/wp-includes/plugin.php
Strict (12) Only variables should be passed by reference on line 42 in file /home3/bestmant/public_html/dev.pathtoenlightenment.net/wp-content/plugins/widget-logic-by-path/widget_logic_by_path.php
Strict (12) Only variables should be passed by reference on line 49 in file /home3/bestmant/public_html/dev.pathtoenlightenment.net/wp-content/plugins/widget-logic-by-path/widget_logic_by_path.php
Strict Non-static method Jetpack_User_Agent_Info::is_ipad() should not be called statically on line 17 in file /home3/bestmant/public_html/dev.pathtoenlightenment.net/wp-content/plugins/jetpack/modules/custom-css/custom-css.php
Strict call_user_func_array() expects parameter 1 to be a valid callback, non-static method GoogleSitemapGeneratorLoader::Enable() should not be called statically on line 406 in file /home3/bestmant/public_html/dev.pathtoenlightenment.net/wp-includes/plugin.php
Warning opendir(/home3/bestmant/public_html/dev.pathtoenlightenment.net/wp-content/cache/db/000000/options_comments/): failed to open dir: No such file or directory on line 82 in file /home3/bestmant/public_html/dev.pathtoenlightenment.net/wp-content/plugins/w3-total-cache/inc/functions/file.php
Strict Only variables should be assigned by reference on line 514 in file /home3/bestmant/public_html/dev.pathtoenlightenment.net/wp-content/themes/genesis/lib/classes/breadcrumb.php