jeudi 29 janvier 2015

How can I avoid tight coupling when practically every decision-logic has to check lots of distributed state?


As the senior developer in our company, I am currently starting to move our commercial php-mysql e-commerce solution (which takes data from a specific ERP-system) from procedural-functional spaghetti-code which uses globals for pretty much everything and has very little separation of concerns to OOP.


The title describes the basic problem rather well - almost every user-interaction will have to check lots and lots of things (state), and do different things based on that. How can I avoid bloating my controllers & services or coding structural knowledge about everything into my classes (constructor over-injection, service-locator, law-of-Demeter violations etc)?


More information about the current situation:


I have already successfully developed a basic architectural style and implemented an RMA-module with this design.


A brief summary: For every basic business-entity (the rows of our mysql-tables), I have a class for data-storage and retrieval, as well as for getting NULL-versions of that object, a configuration-class that holds information about which fields are mapped from db, what a potential alternative unique key besides the db-id is, as well as the name of the table (or view) to get data from. Furthermore, I have specific collection-classes which ensure that they only contain instances of the data-classes and specific repositories also implementing the identity-map pattern which manage both basic retrieval & persistence as well as entity-specific methods. When non-persistent business-entities are needed, they can be stand-alone, in a dyad with collection or a triad with collection and repository.


On top of this, I have quasi-"front controllers" for modules which use services for processing-tasks. In case of multi-step processes (currently RMA, later also user-account, order etc), the module "front controller" checks the current step (action) & user-input (delegate validation to services where sensible).


If multiple actions are possible on a view, and an action only changes data within the view, and has no potential to change which view has to be chosen - the "front-controller" will pass that action to a presenter for the view. If the action can change what page is displayed (re-routing on validation error, progressing to different view on successful input-validation), the business-logic is done by the module "front-controller" itself with the help of a service or services.


As a rather standard php-application, the application is created with user input via URL, POST & GET, has this input from the beginning, constructs a response, outputs it and then is destroyed. New user input will create the entire application anew - with the exception of one or two places where Ajax-code is put into the site by the application, which is basically just creating a mini-application and integrating its output into the browser-window, and isn't an interaction with the application-instance that created the page. So the application cannot update views during its lifetime - actually creating a view in the UI is the end of the application lifetime, meaning actual MVC / MVVM / MVP implementations as originally conceived are not strictly possible... only partial mappings are. I have tried a modest implementation as described above.


RMA is working nicely, and has already achieved huge increase in separation of concerns and thus "debuggability". Using traits for fields and field-related methods that different classes need to fulfill common interfaces (the business-entity specific collections, configs, data-objects and repositories/identity-maps), I was able to avoid code duplication quite neatly.


After many months of reading all about best practices, patterns and anti-patterns, architectural styles etc - I'm still left with some rather pressing problems.


The coupling between the classes related to a single business-entity (data-object, config, collection and repository) is acceptable, in fact quite beneficial: in virtue of this tetrad-pattern and the use of traits, new business-entities are added (and provide extremely powerful methods for handling them) in a matter of minutes. Schema-modifications merely necessitate changing the respective fields in the data-object and their description in the config (two lines of code for every field, plus a potential further two if the non-id unique-key changes... which does't happen).


What's bothering me mostly is that I have found no way to avoid falling prey to at least one of the set of bad practices related to constructor over-injection, service locator plus law-of-Demeter violation and tight coupling (too much knowledge) in general for the controllers and services.


That's mainly because pretty much every single controller will have to check with huge amounts of distributed state... mainly, what should be done and displayed on a certain URL with a certain set of POST & GET data depends on a so many things: Is the shop b2c, b2b or salesperson? Is the visitor logged in? Does the form-data identifying a record to act upon match the records company-code, shop-code, shop-language-code? If we are in b2b, does the user have permission for the module? Does the user have permission for the record? What is the content of the text-snippet with the given code in the local language?


All this information is distributed in different model-entities. Wherever a procedure concerns only the data of the object itself, it is in the data-class or (when meta-information is needed) in a service that knows about the data-class and its config. When it contains nothing but operations on a given collection/aggregate of a business-entity, it's in the collection-class, if it concerns retrieval, it's in the repository/identity-map.


But since practically every module and nearly every action the user can perform with respect to a module is dependent upon so many different things, I currently can't seem to find a way to avoid either bloating objects and their constructors beyond the pale, or using (as I do now with the RMA-module) Configuration-Objects such as "CurrShopConfiguration" which contain the current shop, shop-language, a provider of local text-constants, the current visitor, user and customer and a "god"-like helper-service for a controller which has the CurrConfig-Object and implements all procedures the "front-controller" needs to perform beyond choosing actions, models, views and linking them.


I would very much like to write more SOLID code - yet can't see how, since the essential decision-logic of the modules and actions have so many necessary dependencies.


Perhaps you have some ideas on how to approach this issue?





Aucun commentaire:

Enregistrer un commentaire