What I wrote is much simpler then ANYTHING in the article. If you can read the article you can read my code. If you can't then I'm sorry.
What I wrote is much simpler then ANYTHING in the article. If you can read the article you can read my code. If you can't then I'm sorry.
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:
logHelloWorldWithDate() = print(appendSuffix("hello world: ", time.date().to_string()))
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.
s1= socket()
s2=socket()
logWithSocket(s1, "hello")
logWithSocket(s2, "world")
versus the oop version l1 = LoggerThatOwnsASocket()
l2 = LoggerThatOwnsASocket()
l1.log("hello")
l2.log("world")
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.
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.
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.
d1 = DependencyManagerSocketFilesEtc()
d2 = DependencyManagerSocketFilesEtc()
logStuff(d1, "blah")
logStuff(d2, "blah")
versus: l1 = LoggerWidthDependenciesFilesSocketsEtc()
l2 = LoggerWidthDependenciesFilesSocketsEtc()
l1.log("blah")
l2.log("blah")
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.
s1 = socket()
s2 = socket()
doStuff(s: socket) {
...
logToSocket(s, "blah")
...
...
}
doStuff(s1)
doStuff(s2)
versus: l1 = logManager()
l2 = logManager()
doStuff(logWithSocket: logManager) {
...
logWithSocket.log("blah")
...
}
doStuff(l1)
doStuff(l2)
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 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).
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.
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.
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 interfaceMy 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.With stateless functions it's trivial.
add(x, y)
sub(x, y)
mult(x, y)
add5minus6andMult10(x) = mult(sub(add(x, 5), 6), 10)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.
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.
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.
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.
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.
Also that guy I responded to, wasn't asking for clarification. He was dismissing my comment without reading it.
You use parameter s as both a WrappedSocket and as a str, minor detail for pseudo code.