>Just make a log() function that takes a target (file, socket, etc
You'd need to find or store the socket everywhere where the function is used, which is cumbersome and leaks implementation detail in every client. A class allows to store this dependency (socket) internally. Another approach would be to create a lambda which stores the socket as a captured variable.
You are getting side tracked. Implementation details have nothing to do with it.
It's about state and functions. He is saying rather then have an entity manage both state and functions, Just have a function take in state. The implementation details are besides the point. Why? Because you can arbitrarily wrap that socket up with ANY type of wrapper if you don't like "implementation details". Wrapping it up in a lambda is probably the worst thing to wrap it up in.
He is saying that the trade off of a slightly larger function signature that takes in an extra "wrapped socket" is WORTH higher modularity and composability and lower complexity.
Boom look at that! How many different types of logging functions can you produce simply by composing some primitives together? I have achieved greater compositional flexibility then OOP in MUCH fewer lines of code and significantly less complexity. With OOP you need an ENTIRE ARTICLE to explain strategies trying to get around a problem caused by over complicating things.
There is a cost to just using normal primitive functions and avoiding classes. What's the cost? The function signature needs to take in a wrapper to a socket while the OOP version doesn't? No that's not much of cost. The actual cost is more subtle. The version I present here is propagating logical management of the lifetime of WrappedSocket to be handled by the parent context. It's not really a "cost" per say, it's just shifting the problem somewhere else. This is much less of a concern for most popular languages where garbage collection handles the lifetimes. Which is another way of saying if you're not using C++ the above method is the better way to go then OOP. And if you're using OOP, then the above is a library that can ONLY be used in the context of the class as some destructor has to call file.close().
Note I am not saying that OOP is bad. It works for many use cases. But it is also in general one of the LEAST modular and reusable patterns in programming. It is also over used and over popular. You use it when you have no choice. I am also NOT promoting functional programming.
Sorry, I didn't understand your example. As a client, I want to say log("some error happened") without caring how it's done because it's not my responsibility. How would you do it without classes or closures?
In your example, logFilteredWithDateToFile accepts a file. That means, if a function wants to log an error, it needs to accept a file somewhere to pass to the log function (for example, we want the sockets/files to be opened once and be shared across several calls). So if function A depends on function B which depends on function C, then function A will need to explicitly state all the dependencies of B and C in its function signature. We end up with a very complex function signature of A where we have to list all possible dependencies of every function it makes use of recursively (also, some of the dependencies may not always be needed depending on control flow). As a client, all I wanted was to ask a function to do one simple thing but now I have to manage files, sockets and god knows what else just because somewhere deep inside someone uses them. And then in a new version we decide we don't want to log to a file, we want to use a third-party library. Now we need to change signatures of A, B and C, and modify our code at every call site. Is this what you are proposing?
People interpreted my initial reply as arrogance. While it is a bit arrogant, there's a reason for the condescension. Because your flippant reply, implied you didn't even read the code. Any programmer can OBVIOUSLY read the code. You didn't and claimed you didn't UNDERSTAND it. Well here now, you have a full retort and explanation... so were you lying when you said you didn't understand it? Obviously you were. So my equally flippant response was appropriate.
But let's get that out of the way. I appreciate your second reply here.
>In your example, logFilteredWithDateToFile accepts a file. That means, if a function wants to log an error, it needs to accept a file somewhere to pass to the log function (for example, we want the sockets/files to be opened once and be shared across several calls).
This is the intention of how the library works. It actually does promote the sharing of files and sockets. See trivial example below:
s = WrappedSocket(...)
logStringToSocket(s, "hello")
logStringToSocket(s, "world")
See. Socket is shared across several calls. The only thing that's a bit of the eyesore is that extra socket being passed around. But this is a problem that can't be gotten rid of. You either pass the socket all over your code or in the OOP example you pass an instance of a logging class around.
The only benefit of OOP is that the logging class manages the lifetime of the socket. If calls socket.close() for you during destruction. That's it.
>So if function A depends on function B which depends on function C, then function A will need to explicitly state all the dependencies of B and C in its function signature.
No you don't. This is not mandatory. I expose only parameters that are needed by the user and the API. If I want to give the user the ability to filter I SHOULD expose two string parameters, s: str, match: str. because without it the user can't do anything. But I don't have to, see below:
appendSuffix has two parameters required. But I expose zero parameters.
>We end up with a very complex function signature of A where we have to list all possible dependencies of every function it makes use of recursively
There's no recursion going on in my example and none of the logging functions manage any state. Again the function signature is as complex as you want it to be. If there are OTHER required parameters like "socket" and you want to reduce the complexity of the parameters coming in, you can wrap say a socket and a file in a single struct. Not a huge deal either way... Often this variable is called ctx or context, you see it in languages that tend to be more against OOP like golang.
For C++ you can have state managers. Often this exists in the form of things like ThreadPool or **Manager objects. These manager objects are more of a necessary evil though because they lack the huge compositional ability of my example due to the fact that they unionize state and function into a single primitive. Managers overall segregate state away from the rest of your program such that you can have huge compositional ability in the rest of your program as shown in the example above.
>As a client, all I wanted was to ask a function to do one simple thing but now I have to manage files, sockets and god knows what else just because somewhere deep inside someone uses them.
Yes as a client you do need to manage this. I mentioned this problem with the lifetime of socket or file. From the perspective of say a programmer who uses garbage collection it's the same amount of crap being passed around.
Either you're passing a socket everywhere or your passing the log manager everywhere. If you want something that logs to both socket and a file (multiple dependencies) simple package both of those dependencies in a single struct. This struct can be used for both the OOP version and the non-OOP version. So from the perspective of a programmer who uses garbage collection, the problem presented by both examples here is the SAME, but the 1st example is superior because it is MORE composable and modular.
>As a client, all I wanted was to ask a function to do one simple thing but now I have to manage files, sockets and god knows what else just because somewhere deep inside someone uses them.
Either you're managing a bunch of LogManagers with all the dependencies, Or you're managing a struct with all the dependencies. The problem is the SAME.
>And then in a new version we decide we don't want to log to a file, we want to use a third-party library. Now we need to change signatures of A, B and C, and modify our code at every call site.
Explain this problem with an example. It's too vague. What is the nature of the third party library, what is it replacing?
>Is this what you are proposing?
I've hit all your points pretty thoroughly sentence by sentence. The only thing I haven't resolved is the third-party library thing. Feel free to explain that part and also offer counter arguments to my arguments.
>Socket is shared across several calls. The only thing that's a bit of the eyesore is that extra socket being passed around. But this is a problem that can't be gotten rid of. You either pass the socket all over your code or in the OOP example you pass an instance of a logging class around.
The concept of a "logger" (defined as an interface, for example) isn't tied to any particular implementation. Imagine you have 1000 files that pass sockets around (a plausible scenario because logging is essential). The requirements change and we want to write to a file or use a third-party library. You will have to change 1000 files. If I use an interface, I only touch the initialization code (often 1 single file) by plugging in a different implementation - all 1000 files immediately start using the new implementation with no effort. That initialization could be a dependency framework of your choice, or manual initialization. In OOP, you usually don't pass dependencies around as function arguments, you inject them in constructors once somewhere at startup time and forget about it. So no, it's not only about calling the destructor. If you emulate this behavior with structs, it's still OOP, just without the syntax sugar.
>The concept of a "logger" (defined as an interface, for example) isn't tied to any particular implementation. Imagine you have 1000 files that pass sockets around (a plausible scenario because logging is essential).
Interfaces are a separate topic. In my example, I log to TWO sockets. If you want to imitate the same thing with a log manager you need TWO instances of LogManager.
If you have 1000 files you either pass the socket around to ALL 1000 files or you pass the LogManager around. Same thing. The logManager is simply a wrapper around the Socket.
>If I use an interface, I only touch the initialization code (often 1 single file) by plugging in a different implementation - all 1000 files immediately start using the new implementation with no effort.
Interfaces are a different topic for a different use case. But your point also doesn't fly with me. Because you can REDEFINE a function as well. You define the function in one file and it's used in 1000 files. You redefine it in one place it's redefined in everywhere.
>That initialization could be a dependency framework of your choice, or manual initialization.
So? I can initialize dependencies and pass it around as well. Same thing as passing a log manager everwhere.
l1, l2, d1, d2, need to be passed Everywhere your program is used. There is no getting away from this. Doesn't matter if you use OOP or not.
But you'll note one thing. d1 and d2 are reusable. and modular. I can reuse sockets and files with other things. With a logManager I don't even have access. Which is my point.
deleteContents(d1)
>If you emulate this behavior with structs, it's still OOP, just without the syntax sugar.
I am not emulating this behavior with structs. Just because they tell you in C++ 101 that a struct and a class is the same thing doesn't mean it actually is in a general sense. A struct has no methods in general. It's just raw data. A class is more of a mixture of data AND methods. OOP is the later not the former.
>In OOP, you usually don't pass dependencies around as function arguments, you inject them in constructors once somewhere at startup time and forget about it.
You aren't hearing my argument. Injecting the dependency into a class necessitates the ENTIRE class to be passed around. You either pass the dependencies or you pass around the wrapper class they are injected into. Let me give you an example with a higher level of scope.
Virtually no difference. Think of LogManager as SocketWrapper. Just putting a socket in a wrapper doesn't change the fact that you have to pass it around all over the place.
>I am not emulating this behavior with structs. Just because they tell you in C++ 101 that a struct and a class is the same thing doesn't mean it actually is in a general sense. A struct has no methods in general. It's just raw data. A class is more of a mixture of data AND methods. OOP is the later not the former
I think we should focus on semantics here rather on syntax only.
Objects are dividable in roughly two categories: entities/values and services. If your project prefers the "anemic model" then your entities/values will be "raw data" (anemic model) but your services will be proper classes that contain both data (dependencies) and behavior (methods). In our case, a logger is an instance of a service. I myself prefer this approach where class-based services operate on what is essentially structs (it's very common in the field I work at). So this approach is very much procedural-like in a way and deviates from pure OOP but the way dependencies of services are managed and passed around, by reusing OOP primitives, is already far more flexible and modular (i.e. we have a dependency tree of services which communicate via interfaces initialized at construction time) than what you are trying to propose (concrete structs, passing dependencies via arguments).
>I think we should focus on semantics here rather on syntax only.
We are focusing on semantics. But your definition of OOP is shaped by enterprise style syntax and languages. I'm using very general notions of OOP and general psuedo code and a general notion of what a struct is outside of something like c++.
All programming domains outside of OOP use concepts like polymorphism, inheritance, interfaces and such; they just have different names. The only thing that is exclusively a unique feature to OOP is that methods are unionized with data in scoped sectors called objects and this model is used as a base primitive of a program.
Also OOP and procedural are orthogonal. Most OOP languages are procedural. As long as you have instructions executed as a list of procedures you are doing procedural programming which is 99 percent of what's out there. FP programming is the opposite counterpart to procedural. If your program can execute on a single line as a single expression then you are not executing instructions procedurally... it is now functional. That is the essence of FP.
The strange thing though is FP is not orthogonal to OOP. Most versions of OOP involve setters. Which are methods that mutate state. That in itself makes it so no OOP program following that model can be FP.
>Objects are dividable in roughly two categories: entities/values and services.
Entities and values aka structs are not a concept from OOP. The very general notion of this concept is that these are called Product types with named access to members.
OOP is style of programming that uses Product types such that the entire program lives as part of a Product type and that product type contains State, and methods, and other Products as part of it's structure. This is what you call services. Services are basically a synonym for your standard OOP object.
>If you have 1000 files you either pass the socket around to ALL 1000 files or you pass the LogManager around. Same thing. The logManager is simply a wrapper around the Socket.
Not quite the same thing. If we suddenly want a file instead of a socket, we need to replace all instances of "socket" in all 1000 files with "file", while LogManager stays LogManager no matter what the internal implementation is - I don't have to modify a single file (ecxept for LogManager itself). Your code isn't flexible when it comes to refactoring. And it's not a synthetic example - we've had real cases like that in real projects.
>Because you can REDEFINE a function as well. You define the function in one file and it's used in 1000 files. You redefine it in one place it's redefined in everywhere.
If you pass dependencies via function arguments, your new implementation of the function will most likely need a different signature (as it requires new dependencies) - thus you will also need to patch all call sites in all 1000 files to conform to the new signature. With interfaces, I don't have to do it.
>So? I can initialize dependencies and pass it around as well. Same thing as passing a log manager everwhere.
In this example, you're basically reinventing OOP with structs, just without the syntax sugar, such as polymorphic functions. If you use that approach properly - dependency injection in constructors with clean method signatures - I agree it's the exact same thing as OOP so I'm not sure what we're arguing about here.
>Injecting the dependency into a class necessitates the ENTIRE class to be passed around
>Think of LogManager as SocketWrapper.
Usually in real code it's not "the entire class", but an interface, which is just a lightweight implementation-agnostic protocol which announces what kind of functionality is available. Your SocketWrapper is indeed an example of passing an entire class around. Readability is also lacking: "Logger" tells exactly what it does: "I can log". If I see SocketWrapper or WrappedSocket, it's not immediately clear what "wrapping a socket" means. And your example passes the logger as a function argument - that's not how properly designed object-oriented code works - you inject your dependency in the constructor of your class as part of the dependency tree which is initialized somewhere once. Of course if you pass it as an argument every time there's no difference.
>Just putting a socket in a wrapper doesn't change the fact that you have to pass it around all over the place.
See my argument above that mentioning "socket" everywhere isn't as flexible when it comes to refactoring.
>d1 and d2 are reusable. and modular. I can reuse sockets and files with other things. With a logManager I don't even have access.
A socket instance can be injected in the logger's constructor and shared by other classes as well, I'm not sure why you think OOP is less modular in this regard.
>Not quite the same thing. If we suddenly want a file instead of a socket, we need to replace all instances of "socket" in all 1000 files with "file", while LogManager stays LogManager no matter what the internal implementation is - I don't have to modify a single file. Your code isn't flexible when it comes to refactoring. And it's not a synthetic example - we've had real cases like that in real projects.
The only problem with my code and I mentioned it before is that annoying extra variable. To solve your refactoring problem you write an interface to a single class then implement that class how you like.
To solve my problem you write an Interface to both the Function and the dependency. That's it. Same problem as I stated above with the annoying extra variable.
Let me illustrate:
FunctionType: (d: DependencyInterface, s: str) -> void //This is defining a type alias similar to interfaces for classes but this is for functions.
logFile(d: FileImplimentation, s: str): FunctionType = ...
logSocket(d: SocketImplementation, s: str): FunctionType = ...
generalLogFunction(d, s): FunctionType = <logFile or logSocket>
doStuff(d: DependencyInterface){
...
generalLogFunction(d, "hello world")
...
}
Now if you want to change generalLogFunction just create a new implementation to the function type signature and the dependency interface. Same thing has to be done for the class version of the interface
My example is ALSO real world. Many variables that are externally managed are passed around and function signatures take type these parameters as interface parameters. Golang is an popular example with their CTX that they pass all over the place.
>In this example, you're basically reinventing OOP with structs, just without the syntax sugar, such as polymorphic functions.
No. The delta between OOP and other forms of programming is that OOP scopes methods and data together. There isn't an instance where I do that. Sure you can call it syntactic sugar in the sense that python is syntactic sugar for assembly language, It's all turing complete anyway.
>Usually in real code it's not "the entire class", but an interface.
I am aware of what an interface is. In real code an interface doesn't actually exist. It's just type checking guidelines. The actual data moving around in real code is instances of instances of these interfaces.
>Your SocketWrapper is indeed an example of passing an entire class around.
So? You want me to generalize it? It's an example. Generalizing it doesn't prove my point.
>Readability is also lacking: "Logger" tells exactly what it does: "I can log". If I see SocketWrapper or WrappedSocket, it's not immediately clear what "wrapping a socket" means.
It's just an example. LoggerSocket is equivalent to WrappedSocket. That's the point I'm getting across, they are the same concept. But it Looks like you wanted to talk in terms of interfaces. Well I explained how that works in this comment.
> And your example passes the logger as a function argument - that's not how properly designed object-oriented code works -
There is no properly designed OOP code. Everyone has opinions, you come from the enterprise sector where you guys take certain design patterns from people like Martin Fowler as axiomatic. No man... that's just one flavor of programming, EVEN from the OOP perspective. There are other flavors... see Alan Kay.
>you inject your dependency in the constructor of your class as part of the dependency tree which is initialized somewhere once.
Doesn't this change the signature of the class and therefore break the interface? Well if you make the constructor of the class accept an Interface for a dependency, well then problem solved. But isn't that exactly how I solved it with my function example above? Boom.
>See my argument above that mentioning "socket" everywhere isn't as flexible when it comes to refactoring.
I already countered this point above. For functions you can solve this problem by using an interface for the dependency. If the dependency must be injected anyway then it must be in itself an interface to the interface. If you don't do dependency injection well, this is the only advantage OOP has which I conceded to in my first post: No annoying extra variable. But the Refactoring problem is effectively identical.
>A socket instance can be injected in the logger's constructor and shared by other classes as well, I'm not sure why you think OOP is less modular in this regard.
Again, Doesn't that break the interface?
A logger is a bad example for where OOP breaks down actually because all log functions are IO based and return void so they can't be composed anyway. A better example is this:
class IntegerAdditionOperations:
int i = ..
void add(x: int){i += x}
void minus(x: int){i -= x}
...
class IntegerMultOperations: ...
int i;
void mult(x: int)
How would I create a class using ONLY the primitives above to add 5 minus 6 and multiple by 10 with a member variable? Kind of hard to compose these things.
I don't follow, is DependencyInterface a proper interface (as in OOP)? If so, then, the code appears more complicated compared to just implementing two classes SocketLogger and FileLogger which implement LoggerInterface (by providing log(...) method) and accept Socket and File in their constructors respectively (at least 1 method each) - 7 concepts so far (if we count the methods, too). You have 2 implementations of DependencyInterface - FileImplementation and SocketImplementation, they must have at least 1 method each, then you have 2 functions logFile and logSocket, 1 FunctionType, 1 global log function - at least 8 concepts, with no immediately clear relation between them (syntax doesn't help us). We are also forced to pass DependencyInterface through all functions explicitly when we could just create a new dependency tree for our request.
If DependencyInterface is a function pointer then it's not clear how it actually retrieves its socket without resorting to casting from void* (unsafe) or using global state (not thread-safe).
In any case generalLogFunction is global state. What if I want to use a different implementation for a different use case? As a client, I can't use this global function anymore => leaking abstraction. In OOP I'd be happy to work with whichever logger was injected into my instance.
What will happen if the current global function is SocketLogger but I pass a file as a DependencyInterface?
What if the log function needs a second dependency? You have to add another argument to doStuff and anywhere where the log function is used.
I see no benefits here, it's more complicated and more code.
>Golang is an popular example with their CTX that they pass all over the place.
You lose static typing with this, because key/values stored in a context have to be cast from interface{} to the target type at runtime and there are no compile-time guarantees the value is present or of appropriate type. Usually contexts are used for cancellation.
>you inject your dependency in the constructor of your class
>Doesn't this change the signature of the class and therefore break the interface?
It doesn't break the interface because interfaces don't have constructors. We do have to change the signature of the concrete class' constructor when dependencies change but method calls are far more common than constructor calls. When we change code due to new requirements we have to change something by definition, what we do is trying to minimize the amount of code which has to be modified. A method call is a more common case than an object construction.
>I don't follow, is DependencyInterface a proper interface (as in OOP)?
Interface is not part of OOP. C++ doesn't have it. It's still OOP. Interfaces are a language implmentation detail with a vague definition. In general an interface refers to a general concept called a type of types. An interface is a type of types.
But following your intent. Yes. It is an interface in the way you think of it.
>If so, then, the code appears more complicated compared to just implementing two classes SocketLogger and FileLogger which implement LoggerInterface (by providing log(...) method) and accept Socket and File in their constructors respectively (at least 1 method each) - 7 concepts so far (if we count the methods, too).
It's less complicated. LoggerInterface isn't a concept that exists in my example. Actually you can adjust my example for it to be more simple. Only impliment generalLogFunction. Then impliment the interface of DependencyInterface for socket and file. 3 concepts.
For oop you impliment two constructors, two log methods as well. That's 4 concepts.
>If DependencyInterface is a function pointer then it's not clear how it actually retrieves its socket without resorting to casting from void* (unsafe) or using global state (not thread-safe).
It is not a function pointer. This is isomorphic to the command pattern. Don't need to get into the specifics other then the fact that it's a awkward way to pass state.
>In any case generalLogFunction is global state. What if I want to use a different implementation for a different use case? As a client, I can't use this global function anymore => leaking abstraction. In OOP I'd be happy to work with whichever logger was injected into my instance.
A function definition is global state? A class is global state too. No difference dude. The only thing injected here is a dependency that is agnostic to logging. The socket itself is an interface to IO, but we use it here to differentiate between files and network, so can't talk about it in those terms but Socket is indeed an interface in itself.
>What will happen if the current global function is SocketLogger but I pass a file as a DependencyInterface?
generalLogFunction should handle it as a DependencyInterface. You need one implimentation of it, and you don't need logSocket or logFile. Sorry my example up there wasn't fully correct. The difference between how a file and a socket should be handled should not exist in the log function but in the implimentations of the DependencyInterface.
>What if the log function needs a second dependency? You have to add another argument to doStuff and anywhere where the log function is used.
This changes the signature of the interface.
>I see no benefits here, it's more complicated and more code.
If you don't understand my description then at least my example above sould display significantly HIGER compositional then OOP. I can construct more different types of entities from primitives with my approach then OOP.
>You lose static typing with this, because key/values stored in a context have to be cast from interface{} to the target type at runtime and there are no compile-time guarantees the value is present or of appropriate type. Usually contexts are used for cancellation.
It's not a key value store. It's indeed used for cancellation and the interface is created for such a purpose.
>It doesn't break the interface because interfaces don't have constructors. We do have to change the signature of the concrete class' constructor when dependencies change but method calls are far more common than constructor calls. When we change code due to new requirements we have to change something by definition, what we do is trying to minimize the amount of code which has to be modified. A method call is a more common case than an object construction.
Then do dependency injection on the method. The constructor is cheatcode that will cause a type error somewhere else. The dependency interface gets rid of this problem EVEN for the OOP version.
Where? I need you to spell out the line for me where I used the same symbol twice, because I don't see it. The post is well past the time limit for editing; so whatever it is you're talking about it's there forever.
I also likely have dozens of errors in my psuedocode.
If my arrogance pisses you off you should just take me down by proving my point completely and utterly wrong.
The problem is, you can't. So you resort to being rude and pointing out trivial errors in the code. I'm pretty sure you don't have any argument here.
I've already told you what the error is and where it occurs. If that isn't enough information for you then I am sorry to tell you, you don't have the necessary skill set to work as a programmer.
Do you realize why I said that to the other person? There's a good reason why I was rude. Why don't you read the thread.
There's no good reason for you to be rude here. Unless you're an ass hole. Not saying you are an ass hole. But that's certainly the only reason why you'd act the way you are.
So, not only is your arrogance out of control, you're fragile too. So fragile that echoing your own intemperate words back to you triggers you.
In future, remember that you're on Hacker News, not Reddit. If you can't be civil to people who ask you to clarify your points then perhaps this isn't the site for you.
Bro, you're the one being uncivil. Don't make me laugh. I may be arrogant. But being arrogant isn't uncivil. When you insult people and try to "trigger" them that's what's uncivil.
Also that guy I responded to, wasn't asking for clarification. He was dismissing my comment without reading it.
>If this is written in the library, that is 2^n combinations of file output functions
This is a feature you want. When you code you want to build things in a style that's modular like legos such that you have the ABILITY to build 2^N or more combinations. Any style of coding that restricts this ability is a detriment. Don't get confused, we're talking about the term "modularity" here. Modularity is a feature you want. Who wants a library/framework/programming language that DOESN'T give them ability to be modular or have the capability of building 2^N combinations? It's about ability not actuality.
Not every high level function needs to be implemented. Like any framework you should be given the tools to build whatever you want. If you want somewthing simpler, that's ok too. Then you wouldn't expose all the core low level functions of the library. You would expose higher level functions in your api and make the primitives private. But to build those high level functions, the way to go is to modularly construct them as I did in the example above.
Maybe they don't need logFilterWithSuffixToFileAndSocket. Then don't create it. You just need the foundational primitives such that if I wanted to create it, I CAN. And you can also expose primitive functions through the API such that users CAN too. That is the core philosophy of programming.
Yeah, nobody's going to use those logging functions. They're too cumbersome and take too much effort. And they're completely unsuitable for library code, because the logging target (stdout/stderr/file/socket) and formatting are fixed and not flexible.
Imagine 1000 call sites use 1 function. What would you choose: 1 slightly more complex function which is easy to use by 1000 callers (because the function signature is clean and the dependencies are already injected somewhere in the constructor so they're hidden from the callers) or 1 simple function which is hard to use by 1000 callers (because they need to pass implementation details around)? A function is defined once but used N times, so it makes sense to optimize for the more common case (usage).
I addressed most of this in our thread. You might as well go back to that. Let me converse with this guy on his own terms.
Anyway in response to your reply. It's the same thing. Maybe another renamed example will help you understand. A LoggerSocket class is identical to a WrappedSocket class. It's a wrapper around a socket which is basically similar to a socket itself, so let me call it such.
You see the equivalence? The amount of code is the same. The reuseability of the code is not the same as WrappedSocket is more general then LoggerSocket, but BOTH need to be passed everywhere, and editing the core methods/definitions of log() or LoggerSocket has basically the same effect
You're doing it wrong: you pass LoggerSocket as a function argument. You're trying to use it in a very procedural way: you create a logger of a concrete class, you pass it to a function which expects a concrete class, you call a function on it. Of course it will be the same, and we have none of the benefits of OOP in this example. In a real project, the logger will already be preconstructed somewhere else (DI framework, for example, but manual initialization somewhere at startup works as well) and stored as an object field of the current class (in a functional language that would be a captured value). So at most of the call sites it's going to be just this.logger.log("some error happened") without any mention of sockets or any other kind of boilerplate/implementation details. Our client class has a single responsibility by delegating additional work to a dependency defined as a simple protocol ("interface") in its constructor, which is useful for readability and modularity (you can inject anything you want, provided it implements the protocol), and in case implementation details of the dependency change, we don't have to modify every call site. Another thing is that your code is not testable because you can't swap the socket with a mock (at least, in languages with strong/static typing). You can implement all this with structs and function pointers in something like C, the difference here is that object-oriented languages have nice syntax sugar for it.
>You're doing it wrong: you pass LoggerSocket as a function argument.
This is because I'm too lazy to write out a full class definition for DI. The intent of my example still stands. I'm writing a lot, and a lot of examples too, so forgive me if I don't write out the entire DI pattern of passing ownership of the Dependency to a class.
>In a real project, the logger will already be preconstructed somewhere else (DI framework, for example, but manual initialization somewhere at startup works as well) and stored as an object field of the current class (in a functional language that would be a captured value).
Bro, just pretend that the doStuff function is a constructor and that it also happens to do more stuff then I wrote, like maybe assign it to a member variable if that helps you understand better.
>So at most of the call sites it's going to be just this.logger.log("some error happened") without any mention of sockets or any other kind of boilerplate/implementation details.
I would say the logger instance is a boiler plate detail. It's a wrapper around all the dependencies. WrapperSocket/LoggerSocket etc. Ideally what we want is just log(string). Too bad.
>Our client class has a single responsibility by delegating additional work to a dependency defined as a simple protocol ("interface") in its constructor, which is useful for readability and modularity (you can inject anything you want, provided it implements the protocol),
The client class has more then one responsibility. It has to log something AND manage dependencies. That's two responsibilities. You can argue for the interpretation of responsbilities, but think about it like this. Compared with a stateless function that doesn't MANAGE ANY STATE, a stateless function is even more "single" in terms of responsibility.
I'm going to stop responding to this thread here. Most of the points are in the other thread, pointless to have two. Just go back to the other thread, state your points, and I will respond.
You'd need to find or store the socket everywhere where the function is used, which is cumbersome and leaks implementation detail in every client. A class allows to store this dependency (socket) internally. Another approach would be to create a lambda which stores the socket as a captured variable.