11 ms·
The Composition over Inheritance Principle (2020)
- ravenstine 4y agoWhy 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.
- sevensor 4y ago> 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.
- formerly_proven 4y agoFunctions and lambdas are semantically identical, you don't need to use "lambda" to get a closure in Python.
- civilized 4y ago1. 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.
- irrational 4y agoYou forgot the /s
- radus 4y agoThey’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.
- formerly_proven 4y agoYou 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.
- andrekandre 4y ago> 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..
- michelpp 4y agoThe "Zope" interface model was proposed 21 years ago but not accepted: https://peps.python.org/pep-0245/ https://peps.python.org/pep-0245/
- andrekandre 4y agooh 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...
- formerly_proven 4y agoYou 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.
- jcheng 4y agoYes, since Python 3.8: https://peps.python.org/pep-0544/ https://peps.python.org/pep-0544/
- andrekandre 4y agonice!
- deleted 4y ago[deleted]
- kgeist 4y ago>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.
- deltaonefour 4y agoYou 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.
- goto11 4y agoThis 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().
- wizofaus 4y agoActually 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.
- BoorishBears 4y agoI 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.
- citrin_ru 4y agoLogging framework which is too flexible and too transparent for end users is one of the reasons behind log4shell IMHO.
- BoorishBears 4y agoAgain, 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.
- jcelerier 4y ago
- jayd16 4y agoHow about just abstract over passing the logger an output stream instead? Now any target works.
- xbar 4y agoThe author very clearly enumerates the trade-offs of choosing the if-statement solution and leaves it to the developer to decide.
- slt2021 4y agothe 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
- tgv 4y agoGenerally 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.
- galaxyLogic 4y agoThe 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.
- alexchantavy 4y agoThe 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?
- cabirum 4y agoDo 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?
- fouronnes3 4y agoSum types are superior IMO.
- jcelerier 4y agoSum types do not support loading new types at runtime which is fundamental in many GUI software
- cjfd 4y ago'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.
- zozbot234 4y ago> 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.
- CraftingLinks 4y agoVery insightful for beginners thinking about writing library code instead of application code. This was a great find!
- DeathArrow 4y agoI 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 https://www.manning.com/books/data-oriented-programming
- pharmakom 4y agoSounds like you are writing C# in F# style.
- spinningslate 4y agoInteresting. 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.
- codethief 4y agoTo 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 https://www.destroyallsoftware.com/talks/boundaries
- spinningslate 4y ago
- menotyou 4y agoYet another lengthy article about how to solve problems you would not have if you would never have started with OOP.
- banku_brougham 4y agoThis 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.
- birdfood 4y agoThis 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 https://gameprogrammingpatterns.com