Hello guys. Anyone can recommend some reading material on how to refactor an application towards DI?

Hello guys. Anyone can recommend some reading material on how to refactor an application towards DI?

I did read some stuff from Mark Seemann (blogs and currently reading his book), Misko Hevery, Stefan Glienke​, and some others that can't remember

Fairly enough, I understood the fundamentals of the pattern. Problem is how to deal with tightly coupled code when lots of classes are involved. Some parts of the code is very very DI unfriendly (not even method/property injection), and some other parts do nicely constructor injection but the way the classes are instantiated it's "old fashioned" using Create and not factories.

I don't pretend to use a DI container, so I'm going with pure DI. I've started by pushing the composition root to the DPR file (seems logical since that's the place were all implementation units are already used) and I'm defining lots of factory interfaces that are passed to classes so they can get their dependencies. If a factory becomes to "big" I wrap it with a facade (basically a composition of factories to hide the "dependencies dependencies")

Any tips, tricks and insight is welcomed

Comments

  1. Personally speaking, I'm not sure there's a definitive answer to this. It's really going to depend on your code, how tightly-coupled it is, as well as how extensive the use of global variables and cross-unit coupling exists.

    (I've been messing with some code lately, and when I started to look at this one form, it had a couple dozen global vars at the top of the unit. Why? Who knows. Luckily none of them were referenced outside of the unit; so I moved them into the object because they weren't singletons and didn't need to persist beyond the lifetime of the object. The code looked a lot like C# code; maybe that explains it.)

    I look at things from a "logic vs. plumbing" standpoint. There tends to be a lot of this jumbled together, so start by separating it out.

    The original computing model I learned was "load, process, store". This is really clear with assembly language because you're stuck with a small finite resource called "CPU registers", and you can't afford to let dozens of variables hang around. So you load a few ingredients into CPU registers, mix them up, and then stuff the result away for use later. Modern languages have made us lazy and we don't pay much attention to how much code ends up being used to shovel data from one place to another.

    Objects were supposed to simplify things. But in my experience, they've only made them more complicated. Because people ignore the plumbing.

    For example, it's fabulous when you can use a single assignment statement and get 174 data elements copied from object-A to object-B. But if there are any pointers involved, whoops, there goes the neighborhood! When you start unravelling this wad of code, you begin to see how much of a mess is being made of shallow-copy vs. deep-copy operations, and where they took shortcuts to save a little time and avoid deep-copy plumbing.

    (In Delphi, we only really have the Assign method, a virtual function intended to make it easy to copy objects. In C++, you've got the copy initializer; the default constructor; any number of constructors with different argument lists; and the assignment operator. People actually implement those things in C++ ... but in Delphi, most classes I see that have been created don't even have Assign defined. And we wonder why there's so much plumbing code all over the place!)

    The nice thing about functional programming is that you can cook your chicken where it falls, instead of having to go to the hassle of cleaning it, packing it up, shipping it to multiple destinations following rigorous health (ie., security) protocols, and ultimately into a pot (CPU) on your particular stovetop, just so you can combine it with other ingredients and spices that came from a dozen or more other sources, only to divvy up and move the whole mess into more containers that get stored in other places for a while.

    Yep. It's all about PLUMBING.

    Here's where you start: begin by unravelling the plumbing and isolating the logic. Lay things out so there's a nice straight line in the dataflow (or a few independent lines), like beads on a string. Refactor whatever you need to accomplish that. Your DI is most likely to be the string that everything is threaded upon.

    Logic operates on data. Either you bring the data to the logic, or you apply the logic to some data. With objects and DI, you can "inject" some context (think spices for your chicken) at the point of creation (or very shortly thereafter), and simplify the overall prep time significantly. And remember that you can inject both data and logic.

    ReplyDelete
  2. DI is just another form of plumbing. It's simply a way of getting the spices you want into the chicken at the farm before it starts it's trip to your stovetop. Which means, if you do it right, you can pick your chicken before it hatches. And when it arrives, all you need to do is throw it into a pot or the oven (load), turn on the heat (ie., process -- apply your logic), and wait until it's ready to eat (store). The only thing better would be to apply the logic while it's en route to your house so it's ready to eat when it arrives! (That's what fast-food delivery services are for, I guess. Only that's more of a Factory pattern, and DI is left to the vendor.)

    ReplyDelete
  3. "Working Effectively with Legacy Code" and "Refactoring: Improving the Design of Existing Code" comes into mind.

    Don't try too much at first. The biggest mistake is to turn your working application upside down just because you read or heard people talk about DI and how awesome it is (well, it is!) and then be stuck somewhere in the middle or introduce complicated workarounds because you try to press the existing architecture into the DI.

    As for going with pure DI - good choice. Once you master that you can go a step further and let the DI container do all that for you.

    ReplyDelete
  4. Stefan Glienke I'll have a look when i can to those books too.

    I'm not going very hard at first. In fact, I know it's hard (Seemann says so in his book, going from legacy app to DI is hard and mundane) but the thing I decided to go for it is that I've coded a subsystem of the app using pure DI and I just feel very comfortable with it: code it's easier to deal with, easier to mantain, and changes don't make you hack tons of clases to get it right. It's just amazing the fact that you only need to re arrange some logic-less 2 o 3 line factory code to create a "different application".

    My app today at least meets the "code to an interface" part of the story (I would say almost 100% of the app got it right) but the bad thing is, how do I grab a hold on those abstractions? Using class constructor. And forces me to pass that classes their dependencies. So I had two choices: create the dependencies within the class (bad) or ask for those dependencies (bad). I think I realized that's a bad smell as well: thinking of it like Law of Demeter (don't talk to your friends friends) --> don't ask for dependencies you don't need just because some other needs them (Am I wrong on this??)

    That leaded to try to keep factories with parameter less methods if possible. The only obvious exceptions are those things that depend on run time evaluation (ie Owner: TComponent; User: IUser; and so on) is that ok? I think the correct approach is that the factory internal implementation should pass the respective dependencies if needed (that's when "factory composition pattern" come to my mind)

    I'm really grateful for your help guys

    ReplyDelete
  5. Usually the "I need to pass something that is not a direct dependency (1) but something that my dependency (2) needs" problem arises when the dependency (2) is not properly passed via DI because then it would have gotten dependency (1) closer to the composition root.
    Funny enough this question often is asked by people fairly new to DI "but then I have sooo many arguments on my ctors because I need to pass everything in there" - well, no, you don't because you just pass direct dependencies which themself ask for their dependencies and so on - just like a Matryoshka doll (most outer doll does not know about the most inner one)

    And yes, factories and other patterns are very important in the DI toolbox.

    ReplyDelete
  6. Loose coupling is hard to achieve in Delphi way of coding. Bad smells and bad practices everywhere: each view (form) of my apps is a global var; when I share data (mostly arrays and records) that should have the same lifetime as the app, well I never know who should be owner of their structures, and so on...
    I put DI and loose coupling where I can, trying to keep the code cleanest as possible. I usually realize DI in a "sacrified" unit (kind of factory), where dependencies are hard coded. My other units only depend on interfaces, so when I need to change an implementation it can be done in a single place.

    ReplyDelete
  7. I have lots of forms and not a single global form var. In fact I defined a IView interface hierarchy which is a proxy for TForm

    Things that must have the same lifetime that the app can be managed easily if you have the factories. You can have a "singleton" (not Singleton), the factory can easily enforce the "only one instance" and manage the lifetime of that given instance

    ReplyDelete
  8. Sébastien Paradis When using DI in a classic VCL application you have let go some of the RAD ways of doing a VCL application - like the autocreation of forms that are done via these global variables inside the form units as well as the cross referencing of forms and datamodules via the designer/object inspector.

    ReplyDelete
  9. How do you achieve this Stefan Glienke? Like in Java, in a main method? (= in dpr Delphi file?)
    I also develop in C# and Java but I'm not sure how to reproduce that in Delphi. Any link to online resources about that switch from RAD to MVC in Delphi?

    ReplyDelete
  10. Sébastien Paradis the problem is that the IDE has a fixed and very limited way of doing things. You don't have to use it, but if you do, you cannot change the basic mechanism that's used. There are several different ways of creating instances of forms. You need to come up with an approach that works for your needs. Using what the IDE offers gives you nothing but a single immutable parameter that's not usually helpful.

    In working with forms, you need to: (1) create the form; (2) initialize properties in the form object (if needed); (3) show/process the form; (4) hide the form; (5) retrieve property values from the form (if needed); and (6) free the form (if desired). You can manage all of this yourself and delete the global vars in each of the form units. They're only there because of how the IDE does things.

    Initializing property values can be done through one or more different constructors, or via property injection after creation. Sadly, a lot of people leverage the fact that components on forms are all publicly-accessible variables on the form and fiddle with them directly; ideally, you should define public properties for accessing the ones you need and use them instead.

    If you take this approach, you probably cannot use the IDEs mechanisms because they'll conflict with your approach. It would be nice if the IDE was "upgraded" to support better interface idioms that have evolved since Delphi was first released in 1995 when the IDE's interfaces were designed to support the needs of the IDE rather than the application's designer.

    ReplyDelete
  11. You can put the composition root on the DPR file or on a dedicated unit. Once you wire everything you must include the 3 Vcl required magic lines using the application object: initialize, CreateForm and run. There is only one form that is required by Vcl to be created by the Application CreateForm method: the main form. If you use DI container then you can use delegateTo and CSL that method. You then inject the container into the main form and now you are ready to call resolve when required

    ReplyDelete

Post a Comment