No OOP please .. use SelfContained modules

Posted on Jan 21, 2026

Other people have done a good enough job of explaining why OOP is a solution to which we are always asking: “Wait, what was the question again?”

See also https://alexanderdanilov.dev/en/articles/oop and others like https://www.yegor256.com/2016/08/15/what-is-wrong-object-oriented-programming.html

It basically boils down to:

  • Inheritance is a anti-pattern that will give other problems down the road
  • Mixing data and code is a fundamentally bad idea
  • OOP is rich in boilerplate, but low in actual code
  • Concurrency and Multi-threading become almost impossible in OOP

But maybe the biggest problem is that:

code is made to be read and understood by humans

Why do we not program in ASM? because we need to be able to read and understand code. By spreading code out all over the place, we undermine the readability of the code. Nobody gives a hoot if a computer can read the code .. a computer can read anything. The whole point of code is that humans can quickly read and understand the code. OOP creates code that can only be read by recursively going down all classes, which make it difficult or even impossible to understand what is going were.

The argument that an IDE can help people understand the code ignores the fundamental reason why we want to make code readable at al.

Then what is the way forward? The remaining use case of OOP is that all code in a class is self-contained. It means that people can mess around with classes, with no impact to the other teams/programmers. In practise this does not work, hence all the refactoring which breaks backwards compatibility. Is it possible to actually have a sane separation of concerns?

I can’t tell what other people should do (OK, I can, but I don’t feel like telling other people what to do), but I can tell what works for me, and could for you.

I call this approach “SelfContained modules”. Of course this approach allows a separation of concerns.

Lets see a very simple class:


  class Requests_Response extends Requests {
    public $body = '';
    public $raw = '';
    public $headers = [];
    public $status_code = false;
    public $protocol_version = false;
    public $success = false;
    public $redirects = 0;
    public $url = '';
    public $history = [];
    public $cookies = [];

    public function __construct() {
        $this->headers = new Requests_Response_Headers();
        $this->cookies = new Requests_Cookie_Jar();
    }

    public function is_redirect() {
        $code = $this->status_code;
        return in_array($code, [300, 301, 302, 303, 307], true) || $code > 307 && $code < 400;
    }

    public function throw_for_status($allow_redirects = true) {
        if ($this->is_redirect()) {
            if ($allow_redirects !== true) {
                throw new Requests_Exception('Redirection not allowed', 'response.no_redirects', $this);
            }
        } elseif (!$this->success) {
            $exception = Requests_Exception_Http::get_class($this->status_code);
            throw new $exception(null, $this);
        }
    }

    public function decode_body($associative = true, $depth = 512, $options = 0) {
        $data = json_decode($this->body, $associative, $depth, $options);
        if (json_last_error() !== JSON_ERROR_NONE) {
            $last_error = json_last_error_msg();
            throw new Requests_Exception('Unable to parse JSON data: ' . $last_error, 'response.invalid', $this);
        }
        return $data;
    }
  }

What does this provide to a programmer, and how should we rewrite it?

It provides a namespace .. we can call Requests_Response::is_redirect()

We can actually provide the same kind of namespaces with: (other programming languages have other names, such as modules,packages, etc)


  namespace Requests_Response { 

    function data() {

	$foo = struct { 
    	   $body = '';
    	   $raw = '';
    	   $headers = Requests_Response_Headers::data();
    	   $status_code = false;
    	   $protocol_version = false;
    	   $success = false;
    	   $redirects = 0;
    	   $url = '';
    	   $history = [];
    	   $cookies = Requests_Cookie_Jar::data();
	}

	return $foo;
    }

    function is_redirect($struct) {
    	return in_array($struct->code, [300, 301, 302, 303, 307], true) || $struct->code > 307 && $struct->code < 400;
    }

    function throw_for_status($struct,$allow_redirects = true) {
        if (Requests_Response::is_redirect($struct->code)) {
            if ($struct->allow_redirects !== true) {
                throw Requests_Exception->foo('Redirection not allowed', 'response.no_redirects', $this);
            }
        } elseif (!$struct->success) {
            $exception = Requests_Exception_Http::get_class($struct->status_code);
            throw $exception(null, $struct);
        }
    }

    function decode_body($struct,$associative = true, $depth = 512, $options = 0) {
        $data = json_decode($struct->body, $associative, $depth, $options);
        if (json_last_error() !== JSON_ERROR_NONE) {
            $last_error = json_last_error_msg();
            throw Requests_Exception->exception('Unable to parse JSON data: ' . $last_error, 'response.invalid', $struct);
        }
        return $data;
    }
  }

What is the difference would you say? The difference is that is if possible to call any function in this namespace without initialization.

It is possible to directly call: Requests_Response::is_redirect(‘code’ => $code), without initialization. It means you don’t do:


  $foo = new Requests_Response(); // call the __init__ / __construct function of the class
  $foo->is_redirect();            // short for Requests_Response::is_redirect($this), but with pointers provided by the data structure $foo

But instead you do:


  $data = Requests_Response::data();     // get the data structure that the functions in the module are useing 
  $data->body = get_body();              // optionally set data 
  Requests_Response::is_redirect($data); // call the function with the explicit passing of the data-structure if required

In this way of working, data is a normal struct, and this struct can be used indepent of the functions in the module. Functions in the module can be called without haveing to use this structure if the params are simple. In this way of working you never have to guess what function is being called.

This not only means that you know exactly which function has been called, but you can also call directly Requests_Response::is_redirect( ‘code’ => $code)

It provides a way to specify a data-structure that other functions in the namespace can use, but it does not force it upon the code. Therefore, no locking problems, and direct readability. You don’t have to guess what function is being called.

In most cases you don’t even have to bother with requesting the data structure. Most class functions simply require a simple param. The last example can be simply rewritten as:


  Requests_Response::is_redirect('body' => get_body()); 

Why bother with the extra code? Why 2 or 3 lines of code when 1 line is sufficient?

Its also allows passing arguments as reference if you are sure that its threadsave. You simply have more choices and flexability.

What about inheritance?

Simply don’t do it. It always gives problems in the end. I have spent years of my life fixeing broken applications because of broken backwards compatibility of modules (looking at you Python and NodeJS). Stop creating problems for others. This is not controversial, since most OOP languages nowadays promote composition, which is still a patch.

Instead of:


  class Requests_Response_Headers extends Requests_Response { 

	// extra code 
  }

do:


  namespace Requests_Response_Headers { 

    function data () {
        return Requests_Response::data();
    }

    function is_redirect($struct) {
        return Requests_Response::is_redirect($struct->code);
    }

    function throw_for_status($struct) {
       return Requests_Response::throw_for_status($struct);
    }

    // extra code 
  }

This allows refactoring without breaking everything all the time. It is actually what OOP does under the radar, but also allows you to mix functions from different namespaces. Use the lowest possible function that applies, don’t call a function that only calls another function.

So if Requests_Response changes to much, you can (instead of building extra logic in the Requests_Response_Headers class, simply use the function in Requests namespace.

Conclusions:

  • Most OOP code is syntactic sugar for really simple primitives, but you are better of useing those primitives directly than useing OOP directives
  • It is possible to have a self-contained code that other teams can use without side-effects
  • It is possible to reuse functions without inheritance, and actually be more safe and/or flexible in refactoring
  • This actually reduces problems with multi-threading apps and also improves readability
  • You don’t need a IDE to keep track / being able to find out what code is actually called if you use code oriented namespaces.

What about other people’s code setting malicious values in a data struct?

Most OOP code has a simple ‘$this>->set($data)’ wrapper which does no checking at all. In short: use strong data types && check the input. Also: developers really hate to directly manipulate internal data structures. Maybe provide a API function? As a last ditch resort to the data be do a pointer / key for the code to reference private data which is not accessible to other code.

And if the plugin/external code is really malicieus: Why would you allow this code to run in your memory space in the first place? If you have malicieus code running in your memory, you have a big problem. Also: OOP provides a compile-time check. No OOP constructs provide run-time security. Any code can always read and write all data in your code, stack and data segment. OOP “security” is not real security, and trival to bypass.

Stop thinking OOP provides security. It does not. It creates barries to legitimate programmers, but does nothing to stop bad actors. Security-researchers have been telling developers for the last 40 year to do input validation. Maybe start doing this?