Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
The Composition over Inheritance Principle (2020) (python-patterns.guide)
130 points by radus on June 24, 2022 | hide | past | favorite | 102 comments


I am using a more data oriented approach [1],even if I mainly work with C# which is an object oriented language.

I use inheritance scarcely and I do not use encapsulation.

I separate the code from the data. I have data classes and code classes which do not hold state, and I treat those code classes similarly to modules in other languages.

Now I don't even need classes to hold the data since the introduction of records which are immutable containers of data.

[1] https://www.manning.com/books/data-oriented-programming


IMHO, encapsulation is a principle that can also be applied to non-traditional OOP or even FP, even when there is no mutable state.

It's mostly about limiting the knowledge that certain parts of the program need to have about the internals of certain other parts of the program. For example, if I want to use Redis as a cache, I can make sure to only do so via a specific set of functions and data structures from a particular class/module/... that don't expose any lower-level details.


Agreed. Having worked in F# for a few years, I still find encapsulation extremely useful for things like the logging problem discussed in the article. (Another advantage is the ability to specify configurations via IoC frameworks or bespoke options).


Indeed, encapsulation is probably better referred to a minimal coupling. Near zero knowledge interaction. It's a variability principle.


Data-oriented is the way to go IMO. I am using the latest version of C# and our current domain models use zero inheritance. We also do not allow for any sort of complex type nesting so composition is also constrained. In order to relate complex types together, foreign key properties and dedicated relation types are used.

Every domain type we have can be mapped to & from a relational table without any difficulty. This is important because we use SQL for certain parts of the product. Turns out this kind of compatibility has tons of other benefits that you may find value in (we certainly did).


Interesting. Can you expand on how this is similar to/different from a functional approach? i.e. using types & values to define and hold data respectively, and functions to define the logic/transformations that apply among types.


To me this sounds very much like functional programming as well despite the fact that in OP's link it is said that

> Distinguish data-oriented programming from functional and OO programming

I mean, if code & data are separate and there is no state, there's no other option left in the code separation vs. stateless/stateful (<> mutations) 2×2 matrix other than functional programming. The 2×2=4 options are: procedural (data and code separate, mutations allowed), object-oriented (data and code not separate, mutations allowed), functional (data and code separate, no mutations/state), and a fourth option (data and code not separate (i.e. still classes), but no mutations/state) which Gary Bernhardt jokingly calls FauxO, see minutes 10:21-13:31 of his talk about Boundaries: https://www.destroyallsoftware.com/talks/boundaries


> To me this sounds very much like functional programming as well despite the fact that in OP's link it is said that

>> Distinguish data-oriented programming from functional and OO programming

Yes, that was the basis of my question. The book description also states:

> In DOP, you maintain application data in persistent generic data structures separated from the program’s code. You use general-purpose functions to manipulate the data without mutating it.

Which seems a lot like functional programming. The use of 'persistent' is perhaps interesting, though there's nothing inherent in FP that excludes persistence obviously.


Could you expound on why you don’t use encapsulation? I get the criticism against inheritance but encapsulation seems like good practice—it’s systematically making it impossible to introduce bugs by disallowing clients of the code to see and misuse details that they should not be seeing/misusing.


Not the OP but typically what people mean when they say they don’t use encapsulation is that they don’t use it to hide state, but they will still use accessibility modifiers to limit the scope of an api.

If your state is all largely records or stdlib datastructures that you are passing to functions that only operate on state passed to them, then hidden state is just a complexity without much value.


Incredible, your views are very close to my own. I slightly disagree on encapsulation in that my view is more like, “don’t let it get in the way”.

I think this view on encapsulation makes much more sense from the data-oriented side though.


> and I do not use encapsulation

works well for small stuff you can keep in your head, don't know how well that will scale :)

> I use inheritance scarcely and I do not use encapsulation. I separate the code from the data

"It looks like you are writing Pascal. Would you like some help with that?"

> Now I don't even need classes to hold the data since the introduction of records which are immutable containers of data.

Uh, how is a record not just a class with a few nice properties such as immutability, which, BTW you can have in C# anyway albeit with some extra boilerplate (annotating every class property with 'readonly' (i think it is) means it can only be set in the ctor. I'm not sure what to make of your post. Perhaps I should read the book.


Sounds like you are writing C# in F# style.


How does this apply to problems like filters and emitters for logging as discussed in the article?


Sounds like an anemic domain model.

OOP is orthogonal concept to data-driven programming.


Objects are a marriage of logic and data. You can certainly implement a data-driven system design on top of an object oriented language, but you pretty much have to just make your classes into simple data repositories, and not take advantage of the main aspect that makes OOP interesting, dynamic dispatch.

Most of my coding is done in Ruby, and when I'm doing data processing stuff, I don't bother making any classes. I break out the class keyword when my workflow stops resembling a pipeline, when I'm grabbing all kinds of info from different places and I don't always know how I'm going to be using it. This moves it away from a data approach and more towards a, well, object approach rather than a data approach. Where the behavior of the program is dependent on the metadata just as much as the data.


Why even use a class in any of those cases? The if-statement example demonstrates precisely how OO classes can make code more complicated than it needs to be.

Just make a log() function that takes a target (file, socket, etc) and a message. Unless your logger needs to be so flexible as to work in 3rd party apps, why can't it be just that simple?

Although the author's take on composition is agreeable, it's things like logging where I think custom classes usually don't make things better in the first place. Just writing a function with conditional logic gets the job done and is plenty maintainable.


This is a general problem with code examples. You want the example to simple enough to easily understand. But many features only really makes sense in larger programs, so in a minimal example they seem overly complicated.

You don't want the code which logs a message to be coupled to the logging mechanism. You should be able to transparently change the target (file/socket/database or a combination) without having to change code and dependencies every place a message is logged.

If you don't need any of that, you just use print().


There's also the question whether side effects (IO) are involved in the logging or not. Can it fail (silently)? Is it synchronous ? Does the source care about all of this? Damned, decent logging can be hard ;) The use of print was also endorsed by the twelve-factor methodology[1] and for simple cloud based applications, it could be as simple as that.

[1] https://12factor.net/logs


Actually there's a decent argument you shouldn't allow arbitrary logging targets, especially anything that requires network calls. Writing to a file that's then monitored and uploaded to a database or other managed logging service is more robust. But at least allowing console output, file or in Windows OutputDebugString output is definitely needed.


I think that's a pretty uncharitable reading. Saying network target implies eventually. the data ends up on the network, not that this person is proposing blocking execution to make API calls on logging.


Logging framework which is too flexible and too transparent for end users is one of the reasons behind log4shell IMHO.


Again, being pretty uncharitable. There is a vast and mighty ocean between "allow configurable targets" and intentionally supporting querying LDAP via string substitution.

The kitchen sink approach is its own poison.


If your library does not allow logging to network it does not mean that things won't be logged to the network, just that someone somewhere will have an additional script that netcats the local output, overrides your function calls with LD_PRELOAD to inject the network calls or something like this. Preventing features from existing never makes things better.


There's always that risk, but there's not much worse than a logging framework that makes it's far too easy to affect system behaviour in unpredictable ways (looking at you log4j), or be unreliable in a way that you have no source of what's happened when it falls down itself.


Having said that, I know of at least two legitimate use cases for allowing arbitrary logging targets, both because of annoying limitations of the hosting environment. One being certain unit testing frameworks (e.g. Xunit in the .NET world requires you write to a custom destination for log messages to be captured by the test runner) and the other being AWS lambdas - where you don't even have the option of a separate process reading the log files for further processing. Ideally it would always be sufficient just to write to stdout or stderr.


Sure, but this choice shouldn't be the concern of the code writing the log message.


No, but it might be an argument for not using generic interfaces where an implementation that literally does anything at all might be passed in. In C# and similar languages this could be done with an abstract base class with an internal constructor for instance. No idea if Python supports anything like that.


Isn't this the approach the article is recommending against? Prefer composition over inheritance.


The logging target may be a property that is part of the composition of the logger itself, but it would still need to support interface- or abstract-"inheritance" (even if via duck-typing) to be useful.


I still don't know how people get non trivial experience with kubternetes from the very beginning. Nearly all the code examples of it on the internet are these tiny contrived examples where ultimately for that example use case something other than kubternetes would have been a bad choice.


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

Example:

    writeStringToWrappedSocket(s: WrappedSocket, s: str)
    writeStringToFile(f: File, s: String)
    print(s: str)

    generateLogString(s: str) -> str
    generateFilteredString(s: str, match: str) -> str
    capitalize(str) -> str
    appendSuffix(s: str, suffix: str) -> str
    generateCapitalizedFilteredString(s: str, match: str) = capitalize(generateFilitedString(s, match))

    logCapitalizedFilteredStringToSocket(s: str, match: str) = writeStringToWrappedSocket(wrappedSocket, generateCapitalizedFilteredString(s, match))

    logFilteredWithDateToFile(s: str, match: str, f: File) = writeStringToFile(appendSuffix(generateFilteredString(s, match), time.date), f)
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?


If you can't read my example I am sorry to tell you, you don't have the necessary skill set to work as a programmer.

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.


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:

     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.


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


You should probably tone down your arrogance given that the very first example in your list has an error in it.


It's written in psuedo code. And what error.


You've used the same symbol twice in the parameter list. I'm surprised a genius like you can't spot it.


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.


>writeStringToWrappedSocket(s: WrappedSocket, s: str)

You use parameter s as both a WrappedSocket and as a str, minor detail for pseudo code.


How can I extend this version to multiple loggers, like the article does? Do I need to write in my application code:

  writeStringToFileAndWrappedSocket(w: WrappedSocket, f:File, s:String) = writeStringToFile(f,s) and writeStringToWrappedSocket(w,s)
If this is written in the library, that is 2^n combinations of file output functions - even worse than the n*m the article complains about.


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


So you'd rather use the example in the article that's 5x longer? Makes no sense.


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.

In your method it's:

     wrappedSocket1 = LoggerSocket()
     wrappedSocket2 = LoggerSocket()

     doStuff(wrappedSocket: LoggerSocket){
         wrappedSocket.log("blah")
     }

     doStuff(wrappedSocket1)
     doStuff(wrappedSocket2)
in my example it's:

    wrappedsocket1 = Wrapper(Socket())
    wrappedsocket2 = Wrapper(Socket())

    doStuff(wrappedsocket: Socket){
        log(wrappedsocket, "blah")
    }

    doStuff(wrappedsocket1)
    doStuff(wrappedsocket2)
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.


> Just make a log() function that takes a target (file, socket, etc) and a message. Unless your logger needs to be so flexible as to work in 3rd party apps, why can't it be just that simple?

Of all the ways this could have been done, it seems like they chose the most complicated.

Python's lambdas get a lot of grief, but

    def my_logger():
        log1 = lambda msg: log(sys.stderr, msg)
        log2 = lambda msg: log1(filter_function(msg))
        return lambda msg: log2(other_filter_function(msg))
seems like an entirely sane and uncomplicated way build up a logging function that does what you want it to. Sure, it's not going to be high-performing code (these are Python lambdas after all), but it's not like the article's version is going to take home any prizes in that department either.


Functions and lambdas are semantically identical, you don't need to use "lambda" to get a closure in Python.


  > The if-statement example demonstrates precisely how OO classes can make code more complicated than it needs to be.
does python have something like interfaces/protocols?

i feel like something as easy as multiple loggers should just be as you say, a simple lambda, or an interface with a single function `log(string)`, then you don't need all those annoying if statements..


Given that python is entirely dynamically typed... it's completely unnecessary, python has been able to replace implementations of X with Y for essentially forever.

Yes, there are tool-assisting type hint systems. But they're not part of the runtime. At runtime, if it quacks like a duck, it's a duck, and it matters not one bit what brand the duck is or how much of its innards match what you think ducks have inside them.


Yes, since Python 3.8: https://peps.python.org/pep-0544/


nice!


The "Zope" interface model was proposed 21 years ago but not accepted:

https://peps.python.org/pep-0245/


oh man...

  Rejection Notice

  I’m rejecting this PEP. It’s been five years now. While at some point I expect that Python will have interfaces, it would be naive to expect it to resemble the syntax in this PEP. Also, PEP 246 is being rejected in favor of something completely different; interfaces won’t play a role in adaptation or whatever will replace it. GvR.
---

hmm, maybe someone should dust this off and give it another try...


You can totally still use the zope.interface module or how it's called. But this was part of a phase in the late 90s / early 00s were Python coders from the zope-sphere of influence tried to imitate Java as much as possible, which is not where the community settled.


They’re using the requirements of the python standard library logging module as an example to explore different patterns to accomplish the desired feature set. This module does need to be flexible for obvious reasons.


You need to go deeper. Why does logging look and work like it does? Ah, because its API is a copy of log4j. This is a popular thing to do, but it is not obvious at all that it's a good thing to do.


The author very clearly enumerates the trade-offs of choosing the if-statement solution and leaves it to the developer to decide.


How about just abstract over passing the logger an output stream instead? Now any target works.


1. To make code Good, you have to use Design Patterns.

2. To use Design Patterns, you have to make classes.

3. Therefore, to make code Good, you have to make classes.


You forgot the /s


The article compares multiple different programming mechanisms for implementing a solution to a single problem. But, we can not really draw many general conclusions based on a single example problem.

For some problems inheritance is better, for others composition, or traits, or if-statements.

If we are to compare 5 different programming approaches, I think we should compare them all for solving at least 5 different problems. Of course we need to start somewhere so this article is a great start.


Yet another lengthy article about how to solve problems you would not have if you would never have started with OOP.


This really blew me away, what a spiral through beckoning madness. Glad there was a clear winner among possible solutions, but coming away I feel that OOP has serious problems.

FWIW this kind of thing is handled so nicely in Julia, via the type system and multiple dispatch.


This was a good, concise explainer. Thanks!

I've worked on a python code base that used a lot of deep, multiple inheritance (using mixins) and it was a nightmare to add or change behaviour, so I know the pain.

I think the design patterns book that I've got the most out of is Game Programming Patterns by Robert Nystrom. While putting all your eggs in one ethos is seems to be in vogue, reading this book was refreshingly _balanced_, highly recommend.

https://gameprogrammingpatterns.com


the standard Pythonic if approach+composition using kwargs seems to be more superior than Java-like zoo with classes, subclasses, interfaces, etc.

that's the beauty of python, add one line and you add new feature/behavior, comment out one line and you disable a feature. all code is on one screen and I dont have to scroll or navigate class hierarchy to understand code author's "design".

when I work in large Java repo, I need to read a thousand pages of doc just to understand what each class is doing and how they work together in a 1000+ class call stack.

Adapters, Bridges, AbstractBeanFactories, and other stuff still causes me nightmares


Generally speaking, kwargs in dispatch functions is a dangerous software engineering practice, IMO. I've debugged more function calls than I can remember, just because someone had mixed up functionality.

> Adapters, Bridges, AbstractBeanFactories, and other stuff still causes me nightmares

The "Javanic" approach is dreadful, I agree, but adapters and bridges (or really: using interfaces) can give you a clean and small design. I think that's what you're trying to achieve with "Pythonic if approach+composition using kwargs".

But as usual, what's the best implementation depends on the context.


Very insightful for beginners thinking about writing library code instead of application code. This was a great find!


The examples seem to rely on duck typing which is pretty cool, but can this work well with parameter type hints or will I just need to end up `Union`ing all input param types, or will I need to get rid of type hints?


Do we need inheritance at all? It only leads to complicated class hierarchies and leaky abstractions. Are there cases/problems where inheritance approach is superior to composition, interfaces, etc?


'Need' is a big word. One 'needs' very few things. Software has actually been written in assembly, so what do we actually 'need'?

Answer to both questions is yes. When I read something like this I find the 'one size fits all' dogmatism disturbing. So, maybe someone somewhere created a class hierarchy that was needlessly complex. Now apparently the pendulum needs to swing all the way to the other side and all of inheritance is bad. I hear that lots of people are cutting themselves incidentally with knives. Shall we forbid them? In programming people don't seem to be able to settle on the middle ground, the pendulum always needs to swing to the extremes apparently.

An example where implementation inheritance is beneficial? I have a calculation that depends on quite a few parameters. The calculation can be done from the application code but part of the calculation is rather expensive and is only done once every so often. So, I put the parameters of the calculation in a class, inherit another class from that the the application uses and that can do cheap calculations. I inherit another class that does the complicated part of the calculation. All in all three classes so not very complicated. The disadvantage with making the parameter class a member is that the code gets littered with indirection like 'data->param1' all over the place. This is much nicer.


> The disadvantage with making the parameter class a member is that the code gets littered with indirection like 'data->param1' all over the place. This is much nicer.

Isn't that a rather trivial concern? Implementation inheritance is not "much nicer" than delegation, the opposite in fact is the case. It adds a dispatch step to all calls to virtual methods (including calls that are private to implementations at any level of your 'hierarchy') that is not what you would want in most cases. Which in turn means your entire class hierarchy has to be analyzed as a single, highly-coupled program module; it's quite literally impossible to understand portions of it in isolation.


I find readability one of the most important properties of code. As the saying goes 'code is more often read then written'. Also, we have all seen where disregarding readability leads us, namely the java language, where one gets the impression that every java programmer is being paid in proportion to the number of characters they type. Any single step that leads us from readable code to typical java code may seem like a minor issue but in the end one ends up with code where the championship of large distance scrolling can be attained just in order to find out what is in a class.

In cases I was describing readability really takes a bit hit. It is a bit like writing the abc-formula and having to write (- data->b +/- sqrt(pow(data->b, 2) - 4 * data->a * data->c ) / (2 * data->a). Being able to write (- b +/- sqrt(pow(b, 2) - 4 * a * c) / (2 * a) is so much nicer.

Actually, the three classes I described by themselves do not need any virtual methods. But I was skimping on details a bit. There actually is a fourth class that is being used to mock the application facing class and that causes some virtual methods to be necessary.


By the way, Jai (which is a language currently being developed and is beta) has a “using” keyword which allows you to import the scope of the struct in normal functions, which looks like:

do_something :: (using data: Data, arg: int) { field1 = field2 + arg … }

Which lets you get rid of the annoying data.field1 and data.field2 indirection.

I really wish more languages implement this feature…


Common lisp has had that for ages too, though you specify which slots you are interested in and possibly rename them (useful when you have multiple instances of the same class or classes with common slot names).

http://clhs.lisp.se/Body/m_w_slts.htm

http://clhs.lisp.se/Body/m_w_acce.htm#with-accessors


Need? No. Everything beyond machine code is sugar.

Can certain things be solved neatly with inheritance in a given language? Sure.

Is it a hammer for every nail? No.

For example, at work we use Delphi. Delphi has fairly excellent support for COM-like interfaces[1]. I prefer using interfaces, and I prefer to use composition where it makes sense, since Delphi has support for delegation[2].

But when implementing certain interfaces I might use inheritance through a base class with one or two abstract methods.

Could I have done the same through composition? Absolutely, but sometimes using inheritance leads to a smoother implementation. To me this is a happy middle ground, in Delphi. The resulting hierarchy is very shallow, almost never more than two levels. And the users are shielded from this implementation detail through the interface.

[1]: https://docwiki.embarcadero.com/RADStudio/Sydney/en/Object_I...

[2]: https://docwiki.embarcadero.com/RADStudio/Sydney/en/Using_Im...


You probably want to be able to inherit behaviour. So e.g. in a language with interfaces you want interfaces to have inheritance but perhaps not concrete types.

You definitely don't want multiple inheritance of concrete types. I wrote a bunch of Python without ever realising it had multiple inheritance and I feel like that's a lucky escape, inheritance on concrete types causes problems, but multiple inheritance causes exponentially worse problems. Scenarios people have shown me of "good" multiple inheritance in Python all looked like regrettable mistakes.


> Do we need inheritance at all? It only leads to complicated class hierarchies and leaky abstractions. Are there cases/problems where inheritance approach is superior to composition, interfaces, etc?

No, but since Python allows multiple inheritance, one can use parent classes as traits for composition without having to write so silly boilerplate.


1. The production of “frameworks” that provide default behavior that can be extended or changed.

2. The modeling of taxonomies (Aristotolian).

3. Modeling fixed generalization-specialization relationships.


I agree with 1 and 3, but IME using inheritance to model taxonomies is usually a big mistake. It can work well for things inherent to the system (Iterable -> Sequence) but it falls apart when you're trying to model the real world (Mammal -> Dog). It's essentially about whether you're making stuff up, and can choose to operate within the bounds of the type system to get its benefits (the first case) or you're trying to describe something real and any trade-off is actually bad (the second case). It doesn't help you to be unable to express a portion of the problem, but in exchange you get something that doesn't apply.

Commentary on an excerpt from an interesting blog series: https://softwareengineering.stackexchange.com/a/361176


I somewhat agree, but note that I qualified point 2 with “Aristotelian”. The alternative “prototypical” view (which is what most want to use inheritance for) is unsuited.

Regarding the link, I remember Eric Lippert’s series of blog posts and his struggle to (initially) model a prototypical type problem using inheritance. I remember thinking at the time that he was limited in his object modeling experience to only the constructs provided by the C# language. There are no language constructs that specifically deal with rules relating to (optionally) collaborating objects, as he discovered.


In terms of keeping enterprise devs employed forever to fix the generated spaghetti code, inheritance is definitely superior to composition


You can't really have State pattern w/o inheritance. At very least it becomes very awkward.


This page of Rust's Book shows how to implement exactly the State pattern without inheritance, and it feels pretty natural. Rust's traits do have inheritance but that isn't used here - at first there is one trait, State, but by the time we're writing idiomatic Rust there are no traits at all.

https://doc.rust-lang.org/book/ch17-03-oo-design-patterns.ht...

We end up with three types of post, Post, DraftPost and PendingReviewPost, and our state pattern is implemented by the type transitions implemented in these three types. When you call Post::new() you get a DraftPost, and the only way to get a Post from that is to request the review, obtaining a PendingReviewPost and have somebody accept it to get the Post from there.


Sum types are superior IMO.


Sum types do not support loading new types at runtime which is fundamental in many GUI software




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: