-2

The usual approach is to do what libraries do: group your (single file) classes by component where folder = namespace, right? That makes sense for code that is mostly meant to be used by other projects.

But wouldn't it be better to structure your actual application by inheritance? The major benefit would be that just by looking at the folder structure, you'd already know what inherits from what. In single inheritance languages like PHP, it's certainly possible.

After all, what is the benefit of structuring your actual, own application folders like libraries? So let's assume a simple MVC structure, something like this:

— classes
  |— controller
     |— IndexController.php
     |— PostsController.php
  |— extends
     |— MySqlDatabase.php
     |— HtmlTemplate.php
  |— Controller.php
  |— Database.php
  |— Template.php
— interfaces
  |— extends
     |— HttpControllers.php
  |— Controllers.php
— traits
  |— Cacheable.php

This would be combined with an autoloader that additionally looks for classes in an extends sub-folder inside any specified path, if it could't find one there already. The specified paths would be: /classes;/interfaces;/traits;/classes/controller. The last one is a special case, because I felt like grouping all controllers in an extra folder, so I had to explicitly add that one.

Creating those is also required, if you want to further inherit from the classes in there, because the autoloader wouldn't look for an extends/extends folder. A neat side-effect would also be that going crazy deep on inheritance is kinda discouraged.

What do you think? Which immediate flaws can you depict from this? I guess the additional file system access due to checking paths that may not exists is one.

5
  • 2
    One of the main principles of OOP is that the user of a base class shouldn't necessarily know (or need to know) the implementation of the base class - Under this premise, that setup doesn't make a lot of sense.
    – tofro
    Apr 29, 2021 at 7:47
  • 1
    If your particular application design relies heavily on a particular inheritance tree and the tree is complex and deeply nested and it tells the story of your model well, by all means reflect it in your file structure. But most applications do not meet the named conditions so making it the new usual approach is probably not a good idea. Apr 29, 2021 at 10:50
  • In which folder does a class belong that implements multiple interfaces?
    – Polygnome
    Apr 29, 2021 at 11:30
  • I don't use inheritance in my PHP projects, so I'm already a little bit irritated by creating an entire project structure around that concept.
    – Hans
    Apr 29, 2021 at 11:54
  • 1
    @Polygnome: I surmise the folder structure that is being suggested only accounts for inheritance, not interface (or trait) implementation. Notice in the example that the controllers' filepath doesnt specifically indicate which controller interface is being used.
    – Flater
    Apr 29, 2021 at 12:50

1 Answer 1

4

Whenever someone thinks of a new and improved way of doing something, I am reminded of the relevant XKCD:

enter image description here

I urge anyone to really consider whether after they've created their standard, if they think the total number of actively used standards will in fact decrease. Because if it stays the same, you've not changed anything, and if it increases, you've added to the problem.

In other words, your new standard must out right replace at least TWO existing standards. Therefore, a good measure of the value of your standard if to find two existing standards which would be significantly improved.


Parts of your idea match up (for coincidental reasons) with how things are structured today.

  • MVC controllers are generally put together and also tend to have shared inheritance, i.e. deriving from the same class
  • It's not uncommon for specific implementation of a base class to be one namespace level up from where its implementations are. Especially for tests, I've commonly seen them share a base class fixture that is found closeby to the derived test classes.
  • Base(r) classes tend to be more broad-sweeping, (more) derived classes tend to be more specific. Top-level directories tend to be named/structured after the broad architecture, while deep subfolders tend to be niche and specific.
  • Contracts are commonly kept in a little bag of their own, much like you have done with your interfaces and traits (but not base classes) here. While contracts are more commonly interfaces than base classes, it's not unheard of for some base classes to be part of a publically exposable set of contracts.

But there's also a whole lot of conceptual conflict going on here.

  • Developers don't always know the entire inheritance tree. For example, your controllers may all inherit from a premade Controller class from your framework, but do you know what Controller inherits from, and how deep that rabbit hole goes? Because if you don't then how will you be able to define your top level directory (= the "basest" class, if you will)
  • Are you including TPL base classes into the folder structure? If I inherit from a TPL's BaseThing class, should I use a /BaseThing folder level?
    • If yes, then your codebase is structured based on external libraries, which is going to bite you when your dependencies change.
    • If no, since you claim that the file structure would help with revealing the inheritance structure, then you don't really know what MyDerivedThing derives from, do you?
  • You seem to think about a project as being a single file tree. The predominant majority of professional software projects have multiple projects/layers, which immediately breaks your single-tree structure. In other words, your system would need to enforce a single-project approach in order for your system to make any sense.
  • I am struggling to see how you would be able to sanely implement inverted dependencies, though this may just be because it's tied in to the broader "no multiple projects" problem with your approach.
  • All static classes would have to live in the top level directory with no possible subdivision, since static classes can't inherit.
  • While there are differences between them, on a polymorphic level, base classes and interfaces are very closely tied together and used in the same way, but your system very specifically ostracizes the interfaces to their own folder whereas the base classes belong to their own folder. This forces a two-pronged approach for what is essentially much of the same thing.
  • Although I suspect you've already realized this, interfaces would become rogue agents as their implementations can be anywhere, without any way to group them in the same inheritance directory.
  • You would lose the ability to define your contracts in a separate assembly to ensure that it can be shared (e.g. if part of your codebase is a public package)
  • You would force the developer of a base class to also have to be the one developing the inherited classes, or at least have access to and work in the same source code. While this is commonly the case for developers to work on the full stack of their project, it would preclude things such as third-party libraries which provide a base class for you to extend.
  • I'm no PHP dev but what I surmise from how traits work, much of what I said about interfaces applies to traits as well.
  • If during a change, a base class is now made to derived from another (more baser) class, you would have to move your entire ancestry down one level. While that may not be very hard to do in a file explorer, it's at the very least going to cause a massive commit.
  • I've worked in teams where most developers only had read access to the contracts, so that they could only implement the existing contracts and not modify them. This was done because the implementation developers were not intimately aware of the framework in which their implementation would run, and that framework needed to rely on its contracts being honored and not altered willy nilly. This becomes increasingly hard to do when your base classes are peppered across the project file tree instead of kept separate.

The major benefit would be that just by looking at the folder structure, you'd already know what inherits from what.

Proper naming and a basic understanding of the (contextual) problem domain already tend to have you covered there. There are hardly any, if any cases whereby at the same time:

  • The base class and derived class names are not thematically linked
  • The naming is considered good

After all, what is the benefit of structuring your actual, own application folders like libraries?

Created software remains successful for as long as it is usable. Successful software tends to grow and people try to find new applications for the software, which means that there is a general push towards increasing the size of the codebase, or reusing it across platforms.

In both scenarios, you would've pre-emptively painted yourself in a corner if you had been using a single-project approach where part of the contracts (i.e. the base classes) is peppered in with the implementations. It makes it harder to separate the resuable business logic from the application-specific view logic.

Even in your own example, you already stumbled on this:

The last one is a special case, because I felt like grouping all controllers in an extra folder, so I had to explicitly add that one.

You're already needing to make exceptions for common cases just because "you felt like it". That is indicative of your proposed standard either needing to be commonly circumvented, or that the proposed standard very quickly starts feeling too constrained, which highly negates its value as a broadly used standard.

A neat side-effect would also be that going crazy deep on inheritance is kinda discouraged.

Quite the contrary. In any enterprise grade application, the depth of inheritance tends to be incredibly shallow compared to the breadth of the codebase (i.e. how many non-inheriting classes there are that are genetically unrelated to one another).

All of these non-inheriting classes would live in the same folder, which would become a maintenance nightmare. People would really want to subdivide them further, into logical groups that make sense to the humans working with the code (like you've already done yourself by making a special group for controllers).

But since your proposed system demands that the file tree and inheritance tree are the same, this wrongly incentivizes developers who want to subdivide your non-inheriting classes to invent "fake base classes" just so they can get away with sorting things neatly into folders. In other words, you're going to end up with more inheritance than you did before.

Much like your single-project attitude, the idea that you think that having a wide-but-not-deep file structure (i.e. shallow inheritance leads to a wide, not deep, folder structure) makes me suspect that you are currently working in relatively small and lightweight codebases where it's easy enough to keep the whole codebase in view.

Because you're working in a lighter codebase, any flaws of your approach don't weigh very heavy. You're able to mentally cope with the drawbacks, and some of the benefits make sense from your perspective.

But this is a biased view. Most projects are not like yours.

I can tell you now, hand on heart, that I would quit my current project if I had to see our 3700+ base classes in both the same project and directory folder (I just did a search through our codebase for non-inheriting classes across the solution). And keep in mind that this is not a spaghetti codebase, but an above-averagely clean architecture with a company which very much prioritizes a clean, loosely coupled and long term sustainable approach, instead of banging things out and cutting corners for short deadlines to please their client.

1
  • Thanks for the comprehensive answer. Lots of things for me to think about. You wouldn't have to create "fake classes" though. You could always introduce more folders anywhere. You'd only need to add those paths to the autoloader.
    – DanMan
    Apr 29, 2021 at 17:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.