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.
Holub [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.
> 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/ Most don't even apply 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.
> 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
>No! Patterns are just crutches for missing language features
> visitor is type switching, singleton global variables, command and strategy are first class functions with closures
Another aspect of FP is compositionnality.
Singleton -> memoization of zero-arg fn (pro: initialization is implicit)
Memoization -> fn accepting a fn and returning a memoized version
Command & Strategy -> just a lambda then ?
Objects/classes are complex. Composing them is not simple, it means composing every single method. In fact composition is handled as a special case of class derivation, interface implementation or wrapper class implementation usually, i.e. you gotta write another class. OOP has given up on this issue from the beginning. There is no point in building a framework to create classes by composing other classes because you'll have to control how each single method compose with its counterpart. Say farewell to your neat composition expressions, the granularity is not there. Just write another class instead.
Yes, Holub mentions C folks had to make do with what they had--but that those implementations, despite differences, would point to one or another common pattern (or done enough times similarly to be a pattern).
> bug reports against your programming language
Yes and no. I daresay no programming language can expect to be a universal one and still have the same ergonomics as a purposeful one or even a DSL.
At the same time, with so many amazing languages out now, new language authors may see adopters clamor for features seen in those contemporary ones.
> Macros... Lisp... Clojure
Yes, definitely have tooling for boilerplate. But that also doesn't mean the pattern could have been implemented differently as yet another idiom, or written in a way that isn't idiomatic according to one group or another.
Thank-you for the references.
We've only been speaking in terms of single languages, though. Have you combined different programs together before and thought, "Hmm, this is kind of like a pattern for..."?
> We've only been speaking in terms of single languages, though. Have you combined different programs together before and thought, "Hmm, this is kind of like a pattern for..."?
It's metacircular all the way down. In Forth or Tcl, every command can be its own program. Deploying over many machines has similar dynamics to multithreading on one etc. The same concepts, notation etc. apply - you abstract over them and voila. Better primitive sets make things manageable, or don't.
You have reversed the point of patterns, to be a tool of thought and something to aim for. Instead, they are something you notice by a certain outcome, which guides you when you want that result again.
> Lisp ... tooling for boilerplate
That's where you've missed it. This isn't some functional propaganda; there are many paradigms, OOP and functional are just single ones. You are thinking in terms of boilerplate when these other paradigms just don't have any of it. You can even do OOP without the boilerplate - it's incidental to your tools, Common Lisp with CLOS is an OOP language. The patterns in Lisp architecture are about fundamental issues of domain modeling, how to structure teams, organizations and manage who should implement what. But you can even jump higher in scope and model that in code and "compile your company". As the code executes, at some points it will ask for user input, having an accountant do so and so action or asking a committee to assemble for something else. My company works this way.
-------
> no programming language can expect to be a universal one and still have the same ergonomics as a purposeful one or even a DSL
Yes and no. Yes, overfitting a tool to the current problem space restricts it, but DSLs can have the exact same ergonomics as any other language. Cf. Language Oriented Programming: https://beautifulracket.com/appendix/why-lop-why-racket.html
Agreed. Dr. Samek says as much too and presents an object-oriented version of C, C+, in Practical Statecharts in C/C++.
Thank-you for your replies. If you happen to see this comment, I wanted to ask about reification.
The pattern is the idea, the design reifies--"makes real"--the pattern. Something else--whether code, constellation of programs, or pigeons--implements the design.
There can be many reifications of a pattern, and there can be many implementations of a design.
Do those ideas ring true?
> compile your company
I've seen this done. Human-readable instructions coexist with SQL snippets and scripts. However, once it becomes tribal knowledge, folks may not understand it.
If manual approvals had also been processed through a pipeline instead of emails and "stop and start," it may have still been in use.
I 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.
OO (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.
No, 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.
When 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.
Before starting to code any application, I tend to verify the ideas by first doing the design on my mind, then spending some time with a pen and paper, and maybe with some diagramming software. If I can't see the whole machinery in front of me, I don't start coding.
In your case, if the data is not fitting into neat boxes, it's not being forced into that in the first place. I select tools/features according to the problem at hand, not try to fit what I have into the problem at hand. If that requires a new language to learn, I'm down with that too. This is why I learnt Go, for example.
Sometimes the design stretches to its limits. If that happens, I stop, refactor and continue development. I call this outgrow/expand model.
It's not the fastest way to develop software, but it consistently gives me the simplest architecture to implement the requirements, with some wiggle room for the future.
For example, the latest tool I have written has broken the design I made in the beginning, because I overgrown what I designed (i.e. added more functionality than I anticipated). Now, I'm refactoring it, and will continue adding things after refactoring.
Every tool, every iteration brings learnt lessons, which are stored in a knowledge base.
What I am saying is that Objects are not rows. And we sometimes try to force the Object model onto a data-schema that ultimately is more rich than a chain of objects.
Which is not using the most appropriate tool for the job, per your example.
An ORM is _fine_ for stuff that has a fairly standard shape, like blob posts, user accounts, things like that. Lots of relation questions against persisted data end up not being those exact shapes and patterns yet folks generally reach for the ORM.
Made that up. Say function can be composed, that's the core algebra. f,g,h... and compose operator, but often you need more involved logic and types that can't be encoded in a simple domain Int and types like Int -> Int[0]. You need DB, Logging, Transaction, whatever lower level system used. In OO you use inheritance to be able to integrate all this through layered method calls.. I kinda describe this a protocol. Problem is, OO is loose on types and mutability.. so I'd think there's a gap to fill between function algebras and these 'protocols'. A way to describe typed "function graphs" in a way that can be intercepted / adjusted without passing tons of functions as parameters.
Again that's a bedroom thought, maybe people do this with Category Theory in a haskell library, or caml modules, I'm just not aware of it.
[0] Then there are monadic types to embed a secondary type but that seems too restrictive still.
Mostly 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.
Your 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?
Honestly 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.
This conversation thread reminds me of the very interesting and insightful talk here: Klaus Iglberger “Free Your Functions!” [video] https://www.youtube.com/watch?v=WLDT1lDOsb4.
>With a good Dependecy Injection system (and bad ones abound), requesting a service instance is just as simple as referencing a static class.
DI in .NET is very good and you can access an object with ease with DI. Still, why use it? It's another layer between the caller and calee. Creating objects and resolving those through DI takes some CPU cycles without any major added benefits and your code becomes more complex so more things can go wrong.
> Static classes are fine for those functions if ...
> there is only one reasonable implementation of the function
this. In the absence of polymorphism, a static function is just fine. I am in the phase of avoiding object notation in preference of functional notation whenever possible. I leave OO notation to cases where, for example, there is polymorphism as the functional wrapper will add little to no value.
I'd argue that any functions with arity higher than 1 (including "this"/"self"), where one operand is not clearly "primary" somehow, should live in some sort of "service" (and I am including static classes or global modules in this category). Bonus points if the function is used much more rarely than the type it operates on. I'd argue that Math.Add(x, y) is way better than x.Add(y), especially since 7.Add(8) looks a bit odd (if it even works in your language). Note that my preference includes the standard "7 + 8", since + is defined as a static function rather than as a method on an integer.
For a more complex example, let's consider mixing colors.
Say we have red = ColorRGB(196, 0, 0) and blue = ColorRGB(0, 0, 196) and our current task is "allow colors to be mixed". Which looks better:
purple = red.MixWith(blue) or
purple = Color.Mix(red, blue) ?
There are many different ways to mix colors, and many other things you might want to do with them too. When you start getting into things like
red.MixHardLight(blue) vs.
ColorMixing.HardLight(red, blue)
The advantage of the latter becomes more clear. And it naturally extends into moving your mixing into an interface instead of a static class, so you can have "ColorMixer.Mix(red, blue)", etc. It's about focusing more on the operation you're doing than the nouns you're doing them on, which just tends to be a cleaner way to think about things. There's just a lot more variety in "different operations you can do with things" than in "types of things", at least in the kind of software development I've experienced.
OOP 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.
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) !
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.
Yeah, I do not use Python, nor do I have to read much Python code, but it may be difficult to follow, too (OOP in general), just like PHP with lots of classes but I do not mind 5000 LOC PHP code that is fully OOP at least, I probably would if they were in their separate files.
If 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.
Inheritance 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.
> 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.
Sealed 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.
yeah, 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.
You 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.
> 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.)
That 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.
Because 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.
You apparently have never been in the situation of making a trivial change to production code and then have thousands of tests fail because of overuse of mocks.
You 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.
Do you want to pay me several years income to write a book on this subject, including the cost of editors, publishers, and other things I'm not even aware of (I've never written a book). This is a very complex subject but I stand by it as a general rule even though there are some exceptions. In general if there is only one implementation just use it.
Not several years' income, but I might pay you up to $50 for it if it's good.
> In general if there is only one implementation just use it.
This is good advice for people who don't understand what interfaces are for -- which seems to be most people. If you make the mistake of believing that interfaces are for describing the commonalities between similar implementations, then demanding an IFoo for every Foo is indeed a waste. Generally if I see literally the same word twice, but one has an "I" in front of it, it's a flag that the interface might not need to exist. I think this is the advice you're giving here. But you've missed the other important point: interfaces are not "about" their implementations, they're "about" their consumers. The point of an interface is for the consumer to define exactly what it needs to do its job. That's why interfaces can be useful even if there are 0 implementations: it's still a clear definition of what some service or function requires. Assuming that service isn't private/internal, it's a hook that a consumer of your module can implement if they choose.
I'd say the frequency of the following patterns is correlated with different levels of code quality -- sometimes causally, sometimes spuriously.
1. StringWarbler : IStringWarbler (worst)
2. StringWarbler (fine)
3. StringWarbler : IWarbler (maybe worse or better, but a bit smelly)
4. StringWarbler : ITextProcessor (good)
Yes, #2 is fine if all you need to do is warble strings and there's really only one thing that means. That's common and it's fine. #3 is suspect because it looks like it was abstracted along the wrong line. It looks like there were a lot of different "ThingWarblers" and someone tried to define what was similar about them, without really focusing on the consumers of these services. Whereas with #4, it looks like someone identified a need to process text in various ways, and one way to do that is to warble them. It sounds like the interface was created to describe a need, not an implementation. When you do it this way, you start to see classes that implement two or three interfaces as well.
When I see #2 everywhere, I don't think "these classes need to be implementing more interfaces!". Instead I think "the services relying on these classes are probably not doing a good job specifying exactly what they need to do their job, and are therefore brittle against changes in how those needs are fulfilled." Whether that's a problem depends on the exact situation. I feel like your advice is "prefer #2 over #3", which I generally agree with. But the better advice is "ask for what you need to do your job, in the most general form you can", which #2 (in excess) violates.
It'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.
> 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.
I don't know if I really agree with that. It seems to presume the default form of data is a map<any,any> and you constrain that down to a specific set of fields. (Or similar logic over void pointer spaghetti.) If you build up a record type from scratch you're doing something quite different. Adding a field where there used to be nothing, no way to store data there in an instance, is not what I would call a constraint.
No, you're really adding a constraint. You're adding a description of what exactly a piece of data represents, allowing the compiler or interpreter to limit the ways in which you can use it.
The form of data in a computer is simply a sequence of binary information. It is up to you as a programmer to decide what it represents.
In some cases, you can use a type system to have that automatically enforced in part.
It's important to realize that the types you can use in practice as part of a formal type system are very rarely correct in the sense of fully describing what some data represents.
For example, if a string of bytes represents a HTTP request, then "valid HTTP request bytes" is the correct type, but the type known to the compiler might just be string of bytes.
Similarly, if you represent a graph by a hash table of int to list of int (with the ints labeling nodes), then the type of that is a map from int to list of int, where the int values in the latter must exist as keys as well - but the type system might not actually know that.
In practice, there will therefore often be functions that validate the contents of the data - these are basically manual run time type checking functions.
> You're adding a description of what exactly a piece of data represents, allowing the compiler or interpreter to limit the ways in which you can use it.
Let's say I'm making a new record type with a .firstname field.
And let's say I'm not declaring what data type is stored in the field. Zero description or constraint is happening there.
The .firstname field itself was unrepresentable before I added it. There is no previously existing concept of accessing a .firstname field that I am constraining.
In this scenario, my language doesn't have access to raw memory, and even once I create this record type I don't know how it's going to be stored in memory. So I'm not constraining raw pointers into organized records because there are no pointers at all. I don't have raw byte access like in your HTTP example.
In that scenario, what am I constraining? What is the unconstrained version?
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.
Not 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.
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.