Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

>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.

    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 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.

With stateless functions it's trivial.

   add(x, y)
   sub(x, y)
   mult(x, y)
   add5minus6andMult10(x) = mult(sub(add(x, 5), 6), 10)


>Let me illustrate:

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.




Consider applying for YC's Winter 2026 batch! Applications are open till Nov 10

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: