3 ms·
I actually like "dumb" service classes with 1 method. Usually that method may require tens of dependencies to function (db connection, authorization logic, etc.
by kgeist 3y ago
I actually like "dumb" service classes with 1 method. Usually that method may require tens of dependencies to function (db connection, authorization logic, etc.), which in turn have their own dependencies recursively, and your choices are: 1) use global variables/functions for dependencies, 2) pass all dependencies as arguments to your function, 3) inject dependencies in the constructor of a service class/functor. Global state is a no-no in multithreaded code and is poorly manageable (side effects and all), while forcing a client of your function to pass all required dependencies manually as arguments is cumbersome and basically leaks implementation details (hard to refactor). So for me, service classes are the best option. It's kind of equivalent to a closure (object fields are basically captured variables), just written using the familiar OOP syntax (OOP syntax != OOP semantics). In the context of web, I tend to prefer stateless architectures (because they are easily scalable and less error-prone), and coupling behavior with state often goes in the way, in my experience. So my favorite architecture is a network of service classes whose dependencies are injected in their constructors and which are basically "smart" functions (not "objects"), they have no state, and they operate on domain objects which have no logic other than basic self-validation during mutation (to uphold invariants). I know it's a heated topic (rich model vs. anemic) but I've seen projects naturally, through evolution, end up being more anemic than rich. Pure OOP, where state and behavior are entangled, feels very cumbersome to me, it's harder to refactor when requirements change every day, and having too much state is pretty error-prone. YMMV.
- moron4hire 3y ago"A closure is just a method where the instance members are implicit. A method is just a closure where the closed over state is explicit." I agree with you about service classes. I think one of the things that service classes do well that regular functional code does not is indicate to the consuming developer which parameters are meant to be provided by the environment and are largely static, and which are meant to vary per invocation. Lisp does this instead with dynamic scope. But dynamically scoped dependencies felt too much like global variables to me. The service class instance has its scope bound to it and it won't change. But dynamically scoped functions always felt like the ground could get ripped out from under me at any time.
- zero_shift 3y agoIt sounds to me like you're using classes to implement curried functions. In this case with the IO / DI context as the curried parameter. Working in functional langs I just do DI with higher order functions.
- slively 3y agoExactly! I find myself writing out the same code with classes and functions to show this to folks. I still prefer using classes as it’s easier for me to see at a glance that constructor = DI/curry vs a function returning an object of functions or something. So a class is just a way to communicate a pattern.
- galbar 3y agoI have definitely had the whole "what is the difference between this class <class with constructor and one method> and this function that receives the same as the class constructor returns a function implementing the method" conversation with my teammates before!
- galbar 3y agoI do this and advocate for this in the teams I work in. In my experience, it works well. Service classes are "configurable functions", as I tell my team. With the advantage that you don't have to monkey-patch an import in another module to do a unitary test of the function, but rather you can inject mocks! One thing I do insist on is that a service class either does something (calculate some value, retrieve some information, etc.) xor it coordinates service classes (parse value, then call the validator and, finally, persist value). This aims to prevent deep call chains where every function in between does a little bit and calls someone else, which can be hard to reason about.
- user9925 3y agoThis is exactly how I write code. I wonder if there is a name for this pattern? Something like "functional dependency injection"?