The blockchain is immutable. However, we don't necessarily want our code to be immutable, since we like to be able to fix it up. So, this is done with proxies - a contract that sits in front of our implementation, which is accessed with a delegateCall() to share storage.
There is a problem though: the constructor only runs at the deployment of the contract. So, what if we want to have similar logic then we are going to need to write this ourselves with the same constraints as a constructor. In Open Zeppelin, this is called the Initializer pattern.
Open Zeppelin implements the initializer() modifier for us. This will allow the initialize function to be called once and only once during the lifespan of the proxy.
Although the initializer() is implemented with the functionality, this will not automatically call the children constructors like a real constructor will do. So, we must do that manually. To prevent these from being called by ours, we can use the onlyInitializing() modifier. There is another function called reinitializer() that can be used to allow for initializations after the initial one.
There are a few no-nos within these because of Solidity internal restrictions. First, it's a bad idea to set static values within field declarations. Why? This is equivalent to setting it within the constructor so it doesn't work. To solve this issue, use a storage slot instead. Immutables and constants are fine to upgrade since these are actually stored in the bytecode used but great care should be taken while doing this.
Second, the implementation contract needs to disable initialization patterns. This is because we don't want an attacker to initialize the contract, have a delegateCall() occur to trigger a self-destruct.
Storage changes must be done very carefully as well. Changing slots, types and whatever else is absolutely terrifying, since the code semantic meaning can change.
Overall, an interesting pattern that is necessary for proxies that should be scrutinized carefully.