-
Notifications
You must be signed in to change notification settings - Fork 9.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Compilation enforces coupling to parent class Context contents #6114
Comments
I've had this problem as well. |
Solution: delete line |
I'm having this problem as well, but only happens when the mode is set to production. Agree with @fooman in #7659 that surprises in production mode are not welcome and @Vinai thank you for the proposed solution. Has anyone had success with workaround solution? (we installed Magento 2.1.5 via composer) |
Don't consider this as a coupling to a parent class. Actually there is no possibility for coupling, because Magento 2 encourages core developers as well as 3rd party developers to use private access modifiers for class properties (as Inheritance Based API is discouraged). So, in your specific case. Your child class constructor should look like: public function __construct(Context $context)
{
parent::__construct($context);
$this->redirectFactory = $context->getResultRedirectFactory();
} That will work, and you no need to pass additional dependency ( RedirectFactory $redirectFactory) into child's class constructor, which in fact already passed in the scope of Context object. The main idea behind this logic - is that we want to prevent classes from Dependency Hell (when amount of dependencies is constantly growing) and we have Here is the slide from presentation made by Jisse Reitsma (@jissereitsma on GitHub and twitter) on recent Meet Magento Croatia regarding this issue. Which he considers as a great feature added in Magento 2.1 |
@maghamed thanks for the explanation and example. I think the bigger issue is what @fooman brought up in #7659 as to why no exception is thrown in We are using a community module that has this context problem and is easily solved by the refactoring you provided, but as a module developer I can see how easy it is to overlook where context is passed in and what factories it provides and try to include my own redirect factory. And the example provided, the public function __construct(Context $context)
{
parent::__construct($context);
$this->redirectFactory = $context->getResultRedirectFactory();
} |
@maghamed Coupling to the parent class is not limited to usage of parent class properties, but any knowledge about parent implementation details is coupling. The only way to reduce coupling is to reduce the knowledge about other classes. The only way to avoid dependency hell is to create small, encapsulated, decoupled classes. The context class in fact increases coupling, so it paves the way into dependency hell. |
I really feel stupid not getting the point. Arguments for the context aggrigation check I've heard so far are:
I think both those arguments are nought, since 1) only hides dependencies. It doesn't really reduce the number of dependencies. It just tricks phpcs to not see a dependency. My arguments that the check is bad are: To explain argument a): Explanation of b): And finally point c): As I've said above, coupling can not be avoided. Otherwise no classes could collaborate. However, coupling can be controlled and minimized. Most importantly, coupling should only be introduced if we as developers get something in return. Maybe I'm missing something here, why it is a good idea to use dependencies from the context class. If so, please explain it to me. |
Thanks @maghamed for referring to my talk and thanks @Vinai for giving me a ping on this matter. To summarize my standing point: I don't think the concept of In my presentation, I also referred to Magento 2.1 enforcing the usage of this Because of this, I simply took the stand of Magento itself. If Magento comes up with a new thing, we should not be bitching about it: We should either adapt it, or work together to fix or improve it. That being said (and take note that I really don't see myself as a system architect), if a move is made to get rid of And on that matter, @Vinai makes a solid point that ignoring I'm not sure how this fits in with the plans of Magento to de-couple everything, and their vision on |
Thanks, @jissereitsma for joining this discussion. Another reason was Backward Compatibility we wanted to provide for 3rd party developers. When a 3rd party developer extends from Magento core class, his class starts to depend on constructor signature of extended class. Modification of core class constructor signature will break 3PD code. So, Magento core classes, that are intended to be extended (Controller, Block, Model, Helper), should have an abstraction layer, that will allow modification of their constructor signatures after public release in a backward compatible way. Context objects were introduced to address this problem as well. Here is an example, Context class implements Magento\Framework\ObjectManager\ContextInterface and has all dependencies of corresponding "extendable" class. Context class instance is passed as a dependency to "extendable" class instance. "Extendable" class retrieves all it's dependencies from context object instead of retrieving them directly through __construct. Only responsibility of context object is to retrieve all dependencies of corresponding "extendable" class and provide access to them through getters. So, summarizing all of the above - Usage of Context is a small hack (maybe this hack "smells" a bit) which provides an ability for Magento Core team to make refactoring of Abstract Classes (actually our desire goal is to eliminate God Block, Contoller, Model and Helper classes as soon as we would be allowed to introduce Backward Incompatible Changes). But we expect Magento 3rd party developers consider Context object as set of dependencies which are injected into parent class. And if you already have specific dependency in the parent class. And taking into account that child's constructor signature should be compatible with parent one. Also taking into account that two different external dependencies on the same instance of some class considered as logical issue. |
Thanks @maghamed for explaining things. It helps us understand why But 3rd party developers should also be allowed to refactor their code - and maybe even earlier than the Magento core changes. So the reason to duplicate the dependencies, is to remove the dependencies in the child class, so that the child class is up-to-date when the core is being refactored. @Vinai his main point was not about making this refactoring of child classes required. However, every 3rd developer should be able to determine whether he/she wants to use the |
Could you please bring more details on which particular problem this "feature" is trying to solve? Without it if you implement classes in Magento 1-style, your list of dependencies will grow. If you decompose logic into classes properly, list of dependencies WILL NOT grow. I see the value in such check for core classes, so that constructor signature is not changed until necessary, but for third-party code why enforce legacy-style?
If I remember correctly, the main reason was to avoid massive constructor signature changes when you change something in super-abstract class. This is NOT related to PHPMD checks for increase of dependencies at all, as usual way to "fix" violation was to add SuppressWarnings annotation. At least I do not recall a single case where such violation would result in some refactoring in order to have smaller classes.
Vinai had already explained this. We don't want to rely on parent class implementation details. We want to develop classes here and now properly so that we will not have to refactor later, when context object will change in backward-incompatible manner. I don't understand why inheritance-based approach for customization is generally discouraged but strictly enforced here. So, to summarize:
|
Thanks for the explanation @maghamed, I understand the benefits the context class gives for "extendable" parent classes. Those are valid as long as abstract classes with a But as I've tried to explain above, enforcing child classes to also use the context object for their dependencies is damaging (especially point c) in my comment above) in regards to the refactorability of the parent dependencies. It is true that coupling between the child class and the super class already exists. The child class code containing the class name of the context class in the constructor signature so it can pass it on to the parent constructor is one coupling point. The same is true for the child class coupling to the parent class. By extending it and overriding the constructor there is coupling. But with every method that is called on the context (or the parent class), or any property or constant that is accessed, the coupling increases, and the prospect for problems when the core classes are changed is increased. The code gets more and more rigid and fragile. Even though it isn't as intuitive as for example accessing a protected property, the same kind of coupling is introduced by enforcing the child class to use dependencies from the context class, instead of having it's own dependencies injected in the constructor. If a child class doesn't know anything about the contents of the context class and just passes it to th parent, the context class and the dependencies of the parent class can freely be refactored, as long as the class names aren't changed. But if a child class access the a dependency of the context class, then that dependency can not be removed from the context class without causing potential breakage in child classes, even if the parent class no longer requires that dependency! This kind of breakage is the same that is caused by refactoring protected|public methods or properties in a super class. This is the main reason why I believe the enforcing child classes the use properties from the context class needs to be changed. In summary: For all these reasons I ask you to please reopen and merge the PR #8955. I suggest investigating implementing the OPPOSITE of the current context aggregation check, that is, showing a warning if a child class access a dependency from an "extendable" class from the context. However, this should only be a warning and not prevent compilation. |
@jissereitsma Of course, refactoring is the only thing which could save us from the "code smell". But for now we can't just eliminate abstract god classes because that will introduce massive Backward Incompatible changes. The stories for elimination of Abstract God classes are at the top of our developer experience backlog and would be implemented as soon as we would be allowed to introduce such a drastic changes. But that will happen not earlier than in 2.3 - 2.4, because in 2.2 all abstract classes would be marked as @api We make some deprecation on the method level. For example, methods load/save/delete are marked as @deprecated in Magento\Framework\Model\AbstractModel because Magento provides an ability to get the same functionality using "right" way, not violating Single Responsibility. @Vinai , Inheritance is the strongest coupling which could exist between the objects. Because inheritance is the implementation of "Is-a" relationship. When a successor re-use implementation details of parent using a dedicated protected contract. If you would like to decouple class A from B - don't use inheritance at all and use composition instead. If you have an inheritance, and business logic uses child class by contract of its parent - your coupling is too high. And in any case you will need to make a complex refactoring when usage of Parent class would be deprecated. Regarding discouraging references the $context in child classes. If we will imagine that there is no any $context, but all the bunch of dependencies injected straight into the parent class. Should we ban the usage of these dependencies in child classes either? Should child class duplicate dependency which already exists in parent class, just to reduce coupling on parent class? Most of Dependency Injection containers (like, Unity in C#) implemented in a way which forbids multiple instances of the same type to be injected in specific class. Because that's another "smell" of bad design. |
"Unity does not allow this" is not related and could not be considered as a reason :) Please answer this question.
For any new dependency such property will be Possible rules:
Thoughts? |
@maghamed Inheritance is a strong source code coupling, but it by no means is the strongest. There can be very strongly coupled parent and child classes and there can be very weakly coupled parent and child classes, where the child knows only the parent's class name and nothing else. If the child access methods and properties and constants (it doesn't matter whether they are public or protected) the coupling increases. If the parent class is treated like any other dependency and the knowledge the child class has of the parent is kept to a minimum, then the coupling will be much lower, and thus both the child and the parent much simpler and easier to maintain. This issue now is confusing two things:
I don't think that getting rid of the context class is a good idea. It is a good workaround as long as inheritance is used. |
@Vinai , getting rid of the context class is definitely in our plans. But we will eliminate contexts in the same major release with decoupling and removal of god super class abstractions for Block, Controller and Model. Because if we will just eliminate contexts that will bring huge backward incompatible changes but we would not get to the desirable state for our code, because next release will bring another BiC changes for the same classes, so 3PD will have to refactor their code twice. I brought current conversation to Magento Architectural Council meeting yesterday for additional discussion.
It was agreed that current behavior, when exception is thrown at the time of DI compilation is incorrect. Because DI compilation is not considered as a test scenario. So, shouldn't produce such unexpected errors. Moreover it's not a responsibility of compiler to throw such kind of exceptions. class A
{
public function __construct(B $b, B $b2)
{
..
}
} So, this new static test has to check and prevent this situation. Based on the above can I ask you @Vinai to prepare this static tests and add to existing PR, so we can take and deliver them all together. |
@maghamed,
Sorry, I may be missing something, but how is that related to initial discussion besides your observation about Unity?
That's a quote from @Vinai and it seems totally valid to me. Even more, when some instance is Could you reopen #8955 or it can be reopened by PR author? Thanks for the quick response and no rigidity ;) |
@maghamed Great news, thank you for the update! I agree that injecting two instances of the same type is a code smell (even if Can you please reopen #8955 please? It would be great to get it into 2.2. |
[TSG-Commerce] Tests for 2.4 (pr16)
When running
bin/magento setup:di:compile
the process throws an error if in a class receives an instance of an object as a constructor argument that is already contained in theContext
class of the parent.This enforces coupling custom code to implementation details of the parent classes, however, parent dependencies should be encapsulated.
In my opinion (and I'm sure there are arguments to keep things the way there are, too), enforcing the use of objects from the parent
Context
increases coupling in a bad way.Coupling makes code brittle and rigid. Coupled code is more likely to break during changes (e.g. upgrades).
Even though inheritance is a very strong form of source code dependency in itself, all coupling to the parents implementation should ideally be avoided.
Treating the parent class in the same way as an external collaborator object helps to keep the code maintainable.
I think it would be better to display a warning if a class of a third party module access the
Context
object of a parent, contrary to the current compiler behavior.Preconditions
A Magento 2 instance (any version afaik) with a custom module.
Steps to reproduce
Create a module containing the following class and run the compiler:
Expected result
The compiler compiles.
Actual result
The compiler raises an error because the
RedirectFactory
already is contained in theMagento\Framework\App\Action\Context
instance.I suggest removing that check from the compiler. If it is required by the core team, it should be moved into a separate command or tool.
The text was updated successfully, but these errors were encountered: