9 ms·
Hm. Not sold. Sure, you can write terrible code with classes. But that also works great without classes. > I’m terrible at making good abstractions with classe
by fuzzy2 7y ago
Hm. Not sold. Sure, you can write terrible code with classes. But that also works great without classes.
> I’m terrible at making good abstractions with classes.
Then don’t. Or only do it after everything is done and you have explored the domain. Don’t try to be clever from the start. Unless you have understood everything, it will backfire.
> But as other classes extend and override those methods to address their version of the problem space, they usually end up having some other responsibilities in addition to the original task.
Inheritance gone wrong. If a derived type behaves differently, it should not be a derived type. See Liskov substitution principle. Just because a hash map is somewhat like a list of key/value pairs doesn’t mean it should derive from one.
It could also be a problem of missing separation of concerns/responsibilities, which is entirely unrelated to OOP.
> Secondly, I’ve noticed that classes have a tendency to grow large.
Yes, I did see many big classes. They were always the result of... missing separation of concerns/responsibilities. The fabled god objects.
The section about state management appears to be very much React-specific, too. So, not about classes.
- cryptica 7y agoLarge classes are not always a bad thing. It's better to have a large class with 1K lines which you can break up at a later date than having lots of small classes which have unclear or overlapping responsibilities and which will be difficult to reorganize later. It's much easier to split up and categorize parts of a large blob of code than to re-categorize and merge bits and pieces of logic that are scattered all around.
- mark-r 7y agoWhat kind of coding do you do where a 1K class is considered large?
- cutemonster 7y agoWhat do you consider large? I think classes without internal state, can be large and still simple to understand. But if many mutable fields and internal constraints, then things can get complicated. Better keep such classes smaller
- mark-r 7y agoI would never argue smaller isn't better. But the code I work on every day tends to have much larger classes than that.
- fetbaffe 7y agoYes, key point is to separate classes if they have state or not. I have a simple convention in my code base, a repository does not have state, a service depends on repositories & can have state, thus a repository can have lots of methods (different ways of retrieving data) & a service is specialized & only have small number of methods. If an existing services does not match exactly what you need to do, you write a new service and reuse the same repositories.
- dnautics 7y agoI used to be an oo programmer. Now I'm fully FP, and I'm never going back. When you say "not sold", I guess my question is, have you tried it?
- swish_bob 7y agoI am and have done both. I tend to prefer a functional style, but even then I'm going to be using classes (or a similar construct) for quite a lot of problems.
- sojournerc 7y agoporque no los dos? I find some problems are solved better in an OO way such as interacting with a relational database. Others like data processing, are better done in a functional pattern. I look for the right tool for the job rather than some idealistic purity
- dnautics 7y ago> I find some problems are solved better in an OO way such as interacting with a relational database. I've switched to functional interactions with a database, (Elixir/Ecto) and really, it's much better. In particular the explicit and declarative nature of the database interactions combines the convenience of an ORM with "not hiding important gotchas from you". I'm not arguing for pure functional systems (which I can see going very poorly with relational databases). There are some things that objects are good at like caching state as a model. But even here the FP systems typically do it better via FP actors and (again, impure) message passing, which enforces no-shared memory, and at least in erlang virtual machine systems, couples segregated state with limiting blast radius for your systems failure domains, which is 150% the "right thing to do". Of course if we're getting pedantic, this is even close to OO the way that Alan Kay imagined it, than what the OP and GP talk about, and I don't consider that to be OO in the contemporary, colloquial sense of "everything that happened after Bjarne Soustroup invented C++" I highly recommend this video for a more philosophical take on how to do "impure FP" without OO that is informed by a decade of industry experience: https://www.youtube.com/watch?v=yTkzNHF6rMs https://www.youtube.com/watch?v=yTkzNHF6rMs which is by Gary Bernhardt (of WAT fame) > I look for the right tool for the job rather than some idealistic purity This is all about right tool for the job. IMO, FP code is easier to read, easier to reason about, easier to maintain. I think it's a shame that FP gets this reputation for being 'about ideological purity'. I like using "working programmer's FP", and the FP that I use are built on designs that are driven by real, customer-use-case driven concerns and real-world constraints, and how to make life easier for programmers and operators.
- swish_bob 7y agoI came here to talk about Liskov, but since you beat me to it I'll mention SOLID principles of class design instead, which it really sounds like the OP isn't aware of or just isn't following and it feels like is the source of a lot of their class based woes.
- Spivak 7y ago> Then don’t. Or only do it after everything is done and you have explored the domain. I think this really hits the nail on the head. Classes are the ultimate DRY tool but it seems like much of our industry is realizing that it's rare for any project outside of small self-contained libraries to reach the point where you know enough about your problem domain to ever start meaningfully factoring code out like this. Instead having "here are the different shapes of state you might see floating around" and "here are some functions that can do things with certain shapes" becomes really powerful when you need to be nimble with features/requirements. Encapsulation is beautiful when the scope is specified completely up front but it I've found it leads to more complicated code and mental overhead because you create a world where the least cost path to doing what you want does a run around with types.
- hombre_fatal 7y ago> Then don’t [use them, it's that simple!] The problem with classes in languages that use them as a main abstraction (Java, Ruby, etc) is that people reach for them for all abstraction and software design as their Day 1 tool. All program state and behavior is now immediately fragmented from Day 1 in arbitrary buckets and my experience some moss quickly grows on them and things are never changed for the life of the program. Just seems your post doesn't offer much insight beyond the classic OOP advice I've heard for years when people realize it's not that great: oh no, you're doing it wrong, don't do the bad things, just do the good things and ensure everyone else is doing them too. I don't see how that's actionable nor useful. Limiting class use as much as possible and using functional module pattern as TFA does, on the hand hand, is actionable advice for these common issues with classes.
- cle 7y agoI just don't see much of a practical difference between these two approaches. From a functional perspective, objects are fundamentally a set of namespaced functions that implicitly take the same state/map/JSobj as an argument bound to "this". You can pass that state in explicitly and it just becomes what the author is talking about. And you can have the same state-management nightmares with that approach, because nothing is fundamentally different. Instead of obj.method(arg1) you have namespace::function(obj, arg1). I've worked in a very large frontend codebase using this technique and it's just as much of a pain as implicitly managing state with objects, if not more of a pain b/c it's no longer idiomatic. State management is hard. Changing how you refer to the state isn't going to make it any easier, just different.
- AnimalMuppet 7y agoThe difference with this approach is that any function can take obj as an argument. If it's pass by reference, any function can mutate obj. If obj winds up in an inconsistent state, any function in the entire codebase could have done it. With a class, however, you normally make the data members private; that is, only the member functions of the class can change them. If the data gets mangled, you now have a very small set of places to look. That difference matters when debug time comes around.
- majormajor 7y agoBig classes or no, I've found the key is to have good tests around your public facing contract. IME most unit tests are poor and too fragile because they treat methods as the "unit", even helper methods (or mock out calls to their own internal helper methods, if they're the public-facing methods). So if you want to move stuff around, you find your tests aren't testing for the true behavior/side effects, but just that your previous method heirarchy is in-place. But when your public facing methods are big blobs, they tend to have lots of params that cause fairly different thing to happen, and it gets much harder to have testing confidence around that than if you had a bigger set of smaller public-facing operations.
- bcrosby95 7y agoYour thoughts on this reminds me of: https://martinfowler.com/articles/mocksArentStubs.html#ClassicalAndMockistTesting https://martinfowler.com/articles/mocksArentStubs.html#Class... It's a basic philosophical difference in what a unit test should be doing. It goes back over a decade.
- majormajor 7y agoThanks for the link, I hadn't seen those terms before, but yeah, I have been debating it for a while with coworkers. I'm a bit in between the way described there, though, since I'll often happily leave other objects mocked as long as I know that my contract successfully called that object. I don't need to test all the way through their own implementation - there should be sufficient testing on that object to drive a refactor of it. I just want to support a refactor of me. My view is basically: "we're gonna have to restructure some of this code as future requirements come in, so let's make sure our tests ensure that the things we really want to happen do happen" (like an update being sent to the database or other backend service layer). I have similar hesitancy about applying a lot of design patterns early. Most of the places I've worked have been in the experimentation and rapid feature development stages of the business, with all development being internal development (rather than providing an SDK to others), so being able to change the code is more critical than being able to treat the code as more of a framework.
- crimsonalucard 7y agoYou can write terrible code with nulls and without nulls, but when all else is equal, code without nulls is less terrible. Same with code without classes.
- AnimalMuppet 7y agoBut if I'm the kind of programmer who's going to write terrible software, why would you expect me to be able to choose a good paradigm? For that matter, why would you expect me to be able to be good enough to avoid nulls?
- kova12 7y agoEvery "problem" that the post brings up is a consequence of incorrectly using good programming practices. Solution? Not use the good programming practices at all if we can't get it right. Seriously, just hire a good senior software engineer
- lmm 7y ago> Inheritance gone wrong. If a derived type behaves differently, it should not be a derived type. See Liskov substitution principle. Just because a hash map is somewhat like a list of key/value pairs doesn’t mean it should derive from one. The trouble is that "extends" conflates three different things, and many languages don't offer an easy way to do those things separately: 1. Ad-hoc polymorphism: this thing offers the same interface as this other thing, but with a different implementation 2. Composition: this thing contains this other thing, and uses it to implement its own functionality 3. Delegation: this thing implements this interface by forwarding it to this thing that it contains (possibly overriding some parts) So you end up with things that need to share implementation accidentally exposing a common interface, or vice versa. Thankfully we're seeing more and more languages offering the option to do delegation more explicitly (Kotlin/Rust-style delegation, Haskell/Rust-style derivation), and to do ad-hoc polymorphism without needing to share implementation (Java-style interfaces, Haskell typeclasses / Rust traits).
- b2gills 7y agoIn Raku those things are also separate. 1. Polymorphism class Example { method be-nice () { 'be nice' } } class Bill-and-Ted is Example { method be-nice () { 'be excellent to each other' } } 2. Composition role Salaried { has $.salary; } role Employee { has $.title; } class Manager does Employee does Salaried {} 3. Delegation class Price { has $.cost; } class Product { has Price $.inner handles( 'price' => 'cost' ); } Any call to the `.price()` method on `Product` will delegate to the `.cost()` method on the inner object.