5 ms·
Overall, very good advice. I disagree on polymorphism through virtual methods being a solution to the maintainability problem switches pose, though. With such
by jstimpfle 8y ago
Overall, very good advice. I disagree on polymorphism through virtual methods being a solution to the maintainability problem switches pose, though.
With such polymorphism it's difficult to know the things that can happen. Implementations of the virtual method are not closed - they could come from anywhere in the codebase.
Another problem is that it's hard to cleanly separate the common code from the specialized code in each implementation class. That's why one ends up with ugly helper functions that receive a lot of state, or alternatively with unmaintainable class hierarchies.
IME the only solution is to minimize usage of datastructures that have choices in them ("ADTs") and minimize the coupling of the code to the choices. The latter is done by separating code paths early, and by only switching if absolutely necessary.
- AnimalMuppet 8y ago> With such polymorphism all hope is lost to be able to know the things that can happen. Implementations of the virtual method are not closed - they could be coming from anywhere in the codebase. Well, they could be implemented in any class that derives from the ancestor class that declared the virtual function. Unless you have one main base class, that should be a very small subset of the code. What's more, it has to have the same name. A recursive grep should show every implementation in the codebase.
- jstimpfle 8y agoReformulated "all hope is lost". In terms of ergonomics, I'd say having to recursively discover implementations is quite a stretch from reading a simple switch block that contains all the possible continuations case-by-case. Psychologically, obstacles like this can prevent you from maintaining the code.
- tigershark 8y agoIn OOP polymorphism is the way. In functional programming, if you have exhaustive pattern matching, then that is the best answer. Given that the examples are in Java 8 I would use without doubt polymorphism.
- JackMorgan 8y agoI disagree that avoiding ADTs is the solution, it only exacerbates the issue further. When you only have flat records, they often have a lot of data that is or isn't set based on the state of the record. For example, a Workflow data structure that doesn't have an ErrorMessage field set when there's been no error. This could be a data structure with an FailedWorkflow and SuccessfulWorkflow, and the ErrorMessage only on the FailedWorkflow. Helper functions that receive a lot of state aren't ugly, they are pure. Pure code is easily tested. It is easy to reason about and reuse, because it's clear what the expected inputs and outputs are. http://deliberate-software.com/anemic-domain-model/ http://deliberate-software.com/anemic-domain-model/
- jstimpfle 8y agoYes, deliberate denormalization is not a solution. If you need an ADT, use an ADT. However, you can put all the case-by-case data in separate non-ADT structures. Then think hard if you really need to "unify" them and only if so make an ADT that simply links to the other structures and contains no additional data. Works well for me. (This is not a hard rule. For example, layouts for disk serialization might be better done as a single union with a discriminator field. In this case we're not optimizing for code but for disk accesses).
- tigershark 8y agoADTs are not choice types. Algebraic Data Types are composed by product types, commonly referred as records, and sum types, called choices or discriminated unions or probably something else. You mean to minimise sum types, but I don’t really agree with that. Sum types are invaluable in domain modelling.
- jstimpfle 8y agoThanks. Yes, sum types is what I wanted to say.
- coldtea 8y ago>With such polymorphism all hope is lost to be able to know the things that can happen. Implementations of the virtual method are not closed - they could come from anywhere in the codebase. Anywhere in the codebase that satisfies the interface (or inherits from some base class) -- which is trivial to find, and what you want. Who said they had to be closed? And what if you need a plugin architecture for example?
- jstimpfle 8y agoIf the cases are fixed, that should be reflected in simpler code. The code can only be simpler if it takes advantage of the fact that the cases are fixed. Because of that it's kind of required to "close" them. A plugin architecture is an entirely different kind of beast, and rarely needed. It requires some sort of interface to achieve the decoupling. Inheritance is one option here, albeit an ugly one.
- bjpbakker 8y ago> the only solution is to minimize usage of datastructures that have choices in them ("ADTs") And then what? This only works if you can eliminate the "choice" altogether. Otherwise the "choice" still will be encoded if your system, be it much less obvious. Think using null/nil instead of an optional type. Now everywhere you use the value (including when passed to other functions) you have to test the value for not being null. With a proper ADT (Maybe, Optional, what you prefer) it is obvious when you have a value and when you don't know.
- jstimpfle 8y agoIf you follow a separate-tables approach, you can process the choices separately, and independently, in most cases, simply by iterating over the individual tables. So the choice is indeed largely eliminated. In the majority of cases you're only interested in one of the choices, or are following links that point to instances of a specific known choice. So, no switching code there. > you have to test the value for not being null Don't assume possibility of null if it's not a valid value in your data schema. There are so many codebases rotten by never ending null checks. There are no benefits from them, and only drawbacks: They confuse the reader by checking for a situation that should never happen, giving the impression it could be a valid situation. (This is actually a point the OP makes). Let me exaggerate a little bit. Superfluous null checks are similar to something like def add(int x, int y) -> int: return x + y int z = add(1, 2) if z != 3: return 0 else: return 3 Just because it's "possible" that z is not 3 because there are other values that are also integers, that doesn't mean you have to cover them. Type systems are much less helpful than many FP advocates want you to think, and if you depend too much on them you get a bad, bad codebase (and you deserve it). The main benefits that type systems bring: 1) they catch your typos early, 2) they support compilation to efficient machine code. If you use tables and indexes, there are no pointers and no null anyway. Then again, I've used -1 as a sentinel in the past, and there are many more unused negative values available if you need them :-)
- hderms 8y agoHow do you reconcile the fact you say type systems aren't useful but also that data schemas are useful. SQL is a typed language and it would probably be insane if it wasn't
- tabtab 8y agoPolymorphism and inheritance often assume that differences are hierarchical. The real world is often NOT hierarchical, or at least may easily change to not be hierarchical in the future. Noun "classification" of most domain things is messy. It's one of the reasons for the backlash in the 2000's against OOP as a domain modelling system. IF's often handle unpredictable variations better. Features are often better viewed as a buffet restaurant instead of an animal kingdom classification tree as found in biology. The biology classification-like approach popular in the 90's usually fails outside of biology. And, "lots of little methods" can make code rather hard to read & follow in my opinion. Your eyes may differ.
- jstimpfle 8y agoAre you sure meant to reply to me? I agree with you. But note that even better than a fixed number of if/elses or switches, is physically separating the data for the different cases from the start (where possible). This way if statements _can_ be avoided. That was my point. It's the approach taken by the relational model, used in RDMBSes.
- tabtab 8y agoThe devil's in the details and each situation is different. One has to know the domain well to make decent guesses about likely future change patterns. Making code future-friendly is hard and requires experience both in coding and the domain. Let's look at UI's. Should "buttons" and "hyperlinks" be considered different "types" of things? Or should we blur the distinction to be flexible? In this view, a button is a style applied to a hyperlink rather than a distinct "kind" of thing. Once you pre-type them into distinct things, it's hard to merge them: the code base hard-wires a clear distinction and too many things rely on a type-centric arrangement. A more flexible approach is to take the view that things, perhaps ANY thing, wants the ability to "act like" a hyperlink and/or button. Linking is then viewed as an optional feature, not part of some base "type". Same with drop-downs versus combo-boxes versus text boxes: we can model these via feature smorgasbords instead of sub-types and probably get a more flexible framework in the process that may also be reusable in other widgets. But the flip-side is that such flexibility can confuse newbies. Hard-wired hierarchies are quicker to learn, and test.