7 ms·
People might be afraid of types because in OOP land there's the idea that types aren't mere containers for data. You have to use encapsulation, inheritance and
by DeathArrow 2y ago
People might be afraid of types because in OOP land there's the idea that types aren't mere containers for data.
You have to use encapsulation, inheritance and polymorphism. Fields and properties shouldn't have public setters. You assign a value only through a method, otherwise you make the gods angry.
You have gazillions of constructors, static, public, protected, private and internal. And you have gazillions of methods, static, public, private, protected and internal.
You inherit at least an abstract class and an interface.
You have at least some other types composed in your type.
Unless you do all this, some people might not consider them proper types.
I had my engineering manager warn me that data classes are "anemic models". Yes, but why? "We should have logic and methods to set fields in classes". "Yes, but why?" "It's OOP and we do DDD and use encapsulation." "Yes, but why? Imagine we have immutable records that hold just data and static classes as function containers, and those functions just act on the records, return some new ones and change no state. This way we can reason with ease about our goddam software, especially if we don't encapsulate state and mutate it all over the place, Uncle Bob, be damned." He shook his head in horror. He probably thinks I am a kind of heretic.
- bluGill 2y agoThat is what happens when all too commonly people treat OOP as their religion. Prefer encapsulation to inheritance is good advice. Interfaces are very useful if you have more than one implementation (mocks/fakes do not count), but otherwise pointless. Having private data is always a good idea - no matter what your code style is you don't want to have to look over all 10+million lines of code anytime you need to change data. (there is no clear answer to how the size of a circle should be stores - radius, diameter, circumference, area - given one you can easially calculate the others, but there are implications to whatever choice you make and sometimes you will discover late that you made the wrong choice). > we [snip] use encapsulation." "Yes, but why?" Simple: because when your program gets large you cannot know everything needed to modify the code. You need places where you can say "something really complex happens here and I must trust it is right" (not to be confused with it is right, only that right or wrong is irrelevant) OOP is a great answer to the problems of large complex programs. It is not the only answer. It sometimes is the best and sometimes not. Applying it like a religion is wrong.
- dfee 2y ago> Interfaces are very useful if you have more than one implementation (mocks/fakes do not count) Why not, again?
- bluGill 2y agoBecause of the cost. Performance (if you optimizer is any good) will be at most a few thousand CPU cycles and so only rarely worth worrying about, though even this can add up if everything is an interface. The larger cost is maintenance. Every time you want to change/add/remove something you now need to touch not only every place that uses the thing in question, but also all the tests. That is by mocking your are tightly coupling your tests to how your code is implemented instead of the code should do. If there is more than one implementation this is okay as the code can do different things in different situations anyway, but if there is only one then you are adding coupling that isn't useful. If you have never worked with non-trivial code that is more than 10 years old the above won't make sense. However as code ages you will discover that the original beautiful architecture ideas where always wrong for some need that happened since and so you have had to change things. Mocks make all those changes much harder.
- feoren 2y agoYou contradict yourself here. Interfaces are useful for the same reason having private data is a good idea: separating the contract from the implementation. Interfaces are a description of what the consumer of the interface needs, not a description of some commonality between implementations. Interfaces can be useful even if you have no implementations.
- turdprincess 2y agoIn that setup, how do you write a test which acts against a mock version of your API client (a static function in your proposal?)
- beders 2y agoyeah, I wouldn't recommend trying to do this with pure Java but you could pass around method handles for that purpose. You certainly would want to use an `interface` and that means you need an object. It could be an object that has no fields though and receives all data through its methods. But it does go against the spirit of objects: You want to make use of `this` because you are in the land of nouns.
- MrDarcy 2y agoIn Go at least you simply define an interface and use it as the receiver then inject whatever mock object you want. The mock itself may simply be a new type defined in your test case.
- nrook 2y agoRun a fake version of the API server, and then connect the real client to it. It is usually a mistake to make the "unit" of your unit test too small.
- kdps 2y agoIn this case, API does not refer to client/server. The API of the aforementioned static class is the set of its methods and their signatures.
- btreecat 2y agoGenerally, avoid mocks. Run a copy of the server in a container, run client E2E tests against it.
- jt2190 2y ago> … [I]n OOP land there's the idea that types aren't mere containers for data. Can you elaborate? Types don’t contain data in any way that I understand… Do you mean constrain?
- efficax 2y agoit’s common to be imprecise and mix up the type and an instantiation of the type, and that’s all that is happening here
- svara 2y agoIt's usually not just imprecision though, I've found that a lot of programmers fundamentally don't understand what types are and this confusion might be an example of that. Types are just a layer on top of your code that help enforce certain logical / mathematical properties of your code; those that your particular type system enables you to constrain. Most actual type systems are not powerful enough to allow you to fully specify the logical objects you are actually working with, but thinking about exactly what they are and how to constrain them as well as you practically can (or should) is in my experience one of the important skills of an advanced programmer. By the way, if you know what I mean and have found a good way to effectively teach this way of thinking, let me know, because I have often been unsuccessful. I see people left and right thinking about code as a procedure that will with trial and error ultimately be made to work, rather than as an implementation of logical / mathematical objects that can be correct or not.
- stephantul 2y agoThis hits really close to home! I like thinking of types as a syntactical layer, just like in natural language. Without knowing the meaning of words, we can show that certain compositions of verbs, nouns, etc. can a priori never lead to an intelligible sentence. So trying to actually turn the sentence into a meaning or reading it (compiling/running) is useless. But then again I was trained as a linguist so…
- Dylan16807 2y ago
- throw_m239339 2y agoInheritance is one way to achieve polymorphism. it is not mandatory for OOP. Unfortunately YES, because of "Java Entreprise" pushed by consultancies 15 years ago, a lot of developers insist on encapsulating everything, even when it's redundant. Fortunately, Java is a better language today.
- throwaway2037 2y ago> a lot of developers insist on encapsulating everything, even when it's redundant. Can you give an example? 15 years ago was 2010 and Java 8 was already released. > Fortunately, Java is a better language today. In what ways? To me, it has barely changed since JDK 8 when it got lambdas. To be clear, the JVM is leaps and bounds better each long term supported release.
- rustyminnow 2y agoSealed interfaces and records allow you to effectively build sum types. Switch expressions with type based pattern matching really streamline certain patterns. Type inferred vars are massively welcome. Streams and lambdas are pretty old by now, but they are much more widely embraced in e.g. frameworks and std libs which is nice. Independently none of these are game changing, but together they provide much improved ergonomics and can reduce boilerplate. Now if only we had null safe types <3.
- chamomeal 2y agoUncle Bob in particular is a funny case. I feel like his books are largely credited as the being source those very particular OOP obsessions But he eventually turned to the dark side. He claims clojure, a functional lisp, is his absolute favorite language. He’s even got blog posts about it! Multiple! I stumbled on this while reading about clojure. I really like his blog! https://blog.cleancoder.com/uncle-bob/2019/08/22/WhyClojure.html https://blog.cleancoder.com/uncle-bob/2019/08/22/WhyClojure....
- turtleyacht 2y agoHolub [1] helped clarify this for me. Functional programming can express patterns just as well as OOP. Implementations--idioms--of a pattern can appear different but still retain its design purpose. Previously, I thought FP was a way to happy-path incidental habits to avoid studying every pattern. But if patterns are discovered, arise out of independently invented idioms, then the best I could do is reinvent what everyone else has found worked for them (and turned out to be a design pattern). It has also helped me look at Gang of Four (GoF) examples less literally--if we don't have exactly these classes, it's wrong--to context matching a potential solution with a given problem. The light bulb moment is when OS artifacts like filesystem, programming constructs like modules, and even some one-off scripts can also participate in a pattern implementation, not just having a specific constellation of classes. [1] Holub on Design Patterns
- veqq 2y ago> Functional programming can express patterns just as well as OOP. No! Patterns are just crutches for missing language features or "design patterns are bug reports against your programming language." GoF patterns are concepts useful in OOP, but the recurring patterns and architectures you see in other paradigms are totally different. And they don't apply to Lisp: https://www.norvig.com/design-patterns/ https://www.norvig.com/design-patterns/ Most don't even apply in Go: https://alexalejandre.com/programming/software-architecture-today/#in-go https://alexalejandre.com/programming/software-architecture-... > visitor is type switching, singleton global variables, command and strategy are first class functions with closures, state is building a state machine with first class functions If you could perfectly compress your code, repeated patterns would be factored away. Macros do this. In Lisp, when you find a pattern, you write code which generates that pattern, so you don't have to. https://mishadoff.com/blog/clojure-design-patterns/ https://mishadoff.com/blog/clojure-design-patterns/ > if patterns are discovered, arise out of independently invented idioms Yes. That's the point behind Christopher Alexander's pattern concept - he found architectural patterns which seemed to promote good social habits, happiness etc. Gabriel's Patterns of Software presents this far better than GoF. I strongly suggest you read it: https://www.dreamsongs.com/Files/PatternsOfSoftware.pdf https://www.dreamsongs.com/Files/PatternsOfSoftware.pdf
- tarcon 2y agoHumans reason with objects much better because that's how the real business world works.
- dragonwriter 2y agoNot really. I mean, if objects corresponded directly and only to business organizations—the active entities in business—OOP would correspond tolerably well to how “the real business world” works, but as usually implemented OOP is a very bad model for the business world, and particularly the assignment of functionality to objects bears no relation to the underlying business reality. IME, people don't actually “reason better with objects”, either.
- xigoi 2y agoMost people do not work in business.
- otabdeveloper4 2y agoReal business world works with Excel tables, not "objects". (Whatever that is.) Excel is a pure functional programming language, BTW.
- feoren 2y agoMostly agree, but there are methods that truly are best co-located with their (immutable) data. A good example is Point.Offset(deltaX, deltaY) which returns a new point relative to the callee. Why force that into a static class? There are plenty of examples where you want to use an abstraction over various immutable record types. Services vs. records is a false dichotomy and there is power in mixing the two. Yes, there are lots of functions that don't make sense to co-locate with their operands. Static classes are fine for those functions if both of these are true: the function requires no dependencies, and there is only one reasonable implementation of the function. In practice I find it rare that both of these are true. With a good Dependecy Injection system (and bad ones abound), requesting a service instance is just as simple as referencing a static class.
- ansgri 2y agoYour Point example is indeed good, it shows one of the drawbacks of small 'proper' objects. How do you handle the case of offsetting thousands of points with a single operation, which in most cases will make sense and is readily vectorizable? It's better to expose the internals and use external functions here. There's probably a deeper question, how to make objects 'transposable' in general case (like going from row-based to column-based representation) without duplicating code or exposing internals?
- feoren 2y agoHonestly I wouldn't think much about offsetting thousands of points in my normal work. I'd expect that the compiler would do a good enough job of optimizing it and it's just as parallelizable (if necessary) as using a static method. Here I'm comparing against a static OffsetPoint(startPoint, x, y) type function. I don't see a performance difference there. But you're right that it's commonly nice to operate on sets of things instead of individual things. If I were offsetting millions of points repeatedly, I'd look hard for a good data structure to optimize for whatever I'm trying to do. "Exposing Internals" is not really the big issue here; the big issue is resilience against change. The time when it's appropriate to finely optimize CPU cycles is long after that code has settled into a very durable form. It's just that, for most systems, there's a lot more time spent in the volatile stage than the durable stage. Get your Big-O right and don't chatter over networks and you won't need to worry about performance most of the time. It's much rarer that you don't have to worry about change.
- pseudocomposer 2y agoOr there’s Rust, where types are very strictly mere containers for data and nothing else (until you add traits).
- icedchai 2y agoOOP eventually degrades into "lasagna" (a play on "spaghetti code".) Layers and layers of complexity like you describe. Once you have enough "layers" it becomes almost impossible to follow what is actually happening.
- samiv 2y agoI don't think this is what lasagna means. Lasagna is when you have your software organized in layers. In other words instead of having a big ball of mud where A calls B calls C calls A calls C calls B you have layers (like in lasagna) so that A calls B calls C and you keep your code base so that classes/modules/types that are in the lower layer do not depend on or know of the anything above them and the dependencies only go one way. I love lasagna. It's great (both as design and as food) !
- seanwilson 2y agoNot trying to be funny, but "Ravioli code" might be closer: https://stackoverflow.com/questions/2052017/ravioli-code-why-an-anti-pattern https://stackoverflow.com/questions/2052017/ravioli-code-why... https://en.wikipedia.org/wiki/Spaghetti_code#Ravioli_code https://en.wikipedia.org/wiki/Spaghetti_code#Ravioli_code A related principle that I don't think is talked about enough is "locality": I'd rather have all the code about one feature in one file or close together, rather than it strewn across files where it's harder to read and understand as a whole. Traditional Java was notorious for being the opposite of this. Traditional HTML+CSS+JavaScript is also very bad for this problem.
- schoen 2y agoI love that there's a serious debate about which pasta shape is the best metaphor for particular software engineering patterns!
- throwaway2037 2y ago> Traditional Java was notorious for being the opposite of this. Is modern Java better? If so, why? Also, what languages do it better than Java and why? Any as old as Java that do it better?
- agumonkey 2y agoI believe OO was never meant to be reasoned about, it was just a way to avoid coupling just enough to avoid death. Also, even as a FP head, I think something is missing in FP for some domains.. where I go from object state soup, to function composition soup. At which point I'm in need for a kind of protocol algebra (sitting between pure FP and object graphs). Maybe haskellers do that in their own way, sadly I don't have the time nor the network to know more.
- bayindirh 2y agoOO (as a data container or not) fits into some domains very well. Gonna get stuff from a database? Objects are great. Want to move something without being accidentally written/corrupted? Objects are great. Want to model a 3D object with per node/face/$ANYTHING properties, objects are great. Does object handle everything? Of course not, but having it as a capability at hand allows some neat tricks and tidy code. I believe every problem needs a different mix of capabilities/features, and that particular mix directs my choice about which tools to use. This mindset always served me well, but who knows, maybe I'm the dumbest one in the room.
- eterm 2y agoObjects are famously bad at (relational) databases, hence a near universal loathing of ORMs.
- bayindirh 2y agoNo, just putting the results in a set of objects as boxes and move them along. I don't use ORM layers. Just put the returning data from the DB directly into their neat boxes, string them along in a vector, and pass along. I also write my templated queries myself and talk with the server directly. No need to go "FizzBuzz Enterprise Edition" for a simple task.
- btreecat 2y agoWhen the data you need isn't entirely contained w/in those "neat boxes" you realize quickly the error was trying to force those "neat boxes" onto your data model.
- kdps 2y ago> Imagine we have immutable records that hold just data and static classes as function containers, and those functions just act on the records, return some new ones and change no state Or imagine those functions are part of the immutable record and create new instances. The aspect of (im)mutability is orthogonal to where you place your logic. In the context of domain models, if the logic is an inherent part of the type and its domain, then there are good reasons to model the logic as part of the type, and those have nothing to do with Java or the typical OOP dogma (Rust chrono: `let age = today.years_since(birthday)` - Yes, you could argue that the logic is part of the trait implementation, but the struct's data is still encapsulated and correct usage of the type is enforced. There is only one way to achieve this in Java.)
- pin24 2y agoIf the encapsulation is implemented in a way that a) it is not possible to instantiate invalid objects of the type and b) all modifications via the type's methods only create valid invariants by enforcing the type's rules (either by mutating it in place or by returning a new instance for every modification), then it is ensured at compile time that this type cannot be misused ("make invalid states unrepresentable"). If that particular logic lives somewhere else, that's not possible.
- throwaway2037 2y ago> because in OOP land Speaking on behalf of enterprise CRUD devs (the vast majority of programmers), the Java POJO (pure data struct) is alive and well.
- regularjack 2y agoYou can obviously do OOP without doing what you consider to be anti-patterns. I think it's also important to bear in mind that sometimes what you consider to be an anti-pattern has reasons to exist that might not seem obvious on first sight.