Thursday, 5 March 2020

Robert Pająk's "Lessons Learned" post - a gentle polemic

One of the people I respect and had very productive discussions with, Robert Pająk, published a blog post about lessons learned from working with Go language. He points out 12 points and I thought it would be fun to comment on them from my own experience in OO design.

Apologies for absolute tone in many places, I tried to keep this post short so I had to resort to some mental shortcuts. Also, it may seem a bit chaotic and general, which is because unpacking all of those thoughts would probably take me a book or two.

  1. Robert suggests to start project from a single assembly and then split into multiple apps or vertical segments. While I agree with the spirit of this point, I find this topic much more nuanced. First of all, my typical initial layout for apps is two assemblies - one for bootstrap/adapters (in the hexagonal architecture sense) and one for application logic. This way I can get the compiler to protect me from dependencies pointing in the wrong direction while keeping everything simple. When the application grows, I find partitioning vertically helps, although to achieve real vertical partitioning, I find that the features must differ significantly from each other. In other case, there's typically some kind of core model containing the code that many features need. Also, I have often some "lib" code growing aside, which fits in no specific place and could later be extracted as separate libraries.
  2. Robert suggests e.g. that "a little copying is better than a little dependency". I strongly disagree. Not that I consider myself a reuse zealot but that I don't think there's a single rule defining all the possible cases. Also, I don't think "little" or "much" has much to do with this. My take on dependencies is: 
    1. I try to avoid depending on frameworks. If I do, I keep them away from my application logic, 
    2. I always consider whether two pieces of code evolve together and if they do - what is the cost of them falling out of sync. Based on that I decide about dependency vs. copying, 
    3. I don't hesitate from pulling dependencies containing code that I could easily write by hand but am too lazy to do so (e.g. a Maybe library) - the reason is, when such dependencies cause issues, I can replace them with hand-written code without lots of effort.
  3. Robert suggests that clear code is better than clever code. I agree 100% but would like to rephrase it into: "if you can achieve the same with clear code or clever code, choose clear code". Sometimes I find writing clever code necessary (e.g. see how Functional.Maybe library checks whether a Maybe is "something" or "nothing").
  4. Robert suggests to avoid interfaces that have one implementation, pointing to Mark Seemaan's blog. I'd like to point several things. 
    1. First, Mark says that single-implementation interfaces are a smell. I often see this rephrased into "single implementation interfaces don't make sense" which I strongly disagree with. Single implementation interfaces make sense when they are used to achieve ISP (e.g. separating "mutate interface" from "read interface", see e.g. IReadOnlyList interface) or where they are needed to manage dependencies more cleanly. 
    2. Interfaces are also used extensively in the GOOS design/TDD approach which I often use. Still, I am against single implementation interfaces when added as an afterthought only to enable mocking. I find they often typically bad abstractions. 
    3. The last comment, in regard to Mark's article is that I myself stopped using the phrase "x is a smell" and I find it unnecessarily offensive and somewhat arrogant.
  5. Robert writes "Do not use patterns, nor do not apply SOLID, DDD, CQRS unless it really helps". Depending on what it means to "really help" I either agree or disagree on this one. About patterns - I don't see using a pattern where it doesn't help as using a pattern at all. About principles - I consider it the whole point of a principle that I adhere to it before I see the "real benefit". The point, I think, it that by following a principle I should not get into a situation where I would see how it really helps. I find that similar to sth. like "be honest" - the whole point is not to feel on my own skin that being dihonest doesn't pay.
  6. This point is about generics. I agree. Generics, at least in C#, cost not only in cognitive load. E.g. applying nullable reference analysis to generic code tends to get somewhat complicated.
  7. Robert suggests to avoid reflection and IoC containers. I agree fully.
  8. is about the cost of fluent API. I agree as well. My rule of thumb is: I treat fluent APIs as UX tool for developers. It pays back when the API has many users and needs to be very flexible while maintaining readability. I also consider fluent API "a language in a language" and effort must be taken to learn that language.
  9. is about SQL and ORMs. I don't know much about this topic, in my cases facades working on DTOs were typically good enough.
  10. is about testing. There are some sentences that I disagree with. 
    1. "Tests should show what the system does, instead of how it does it" - if this means "test on the highest level", then I disagree. A system is almost always composed of subsystems. The apps I write are typically only part of much bigger systems that even transcend software. Also, tests on different level have different purposes and properties. 
    2. "Avoid mocking whenever possibile" - I think mi agrement or not depends on what "when possible" means. When I use GOOS-style design approach, I use lots of mocks but I would still say I "avoid mocking where possible" because e.g. I don't have to mock value objects and so I don't. 
    3. "There is nothing wrong in using real database or file system in your automated tests unless the tests are not taking too much time" - time is not the only factor in play - there are others. One example is isolation. 
    4. "Integration tests give more trust, because they work the same way as code in production" - I disagree. Integration tests give more trust about two pieces of whatever being able to work together and functional tests give more trust about the functionality thus I see no superiority of integration tests in that regard. 
    5. "Mocking often leads to testing implementation details instead of behavior" - in principle, I disagree. In my opinion, there is nothing inherent of mocks that makes them any special in that regard. Most often when I hear people saying this, it means their testing approach does not match their design approach, thus they end up testing what their design approach considers an implementation detail while their testing approach does not.
  11. Again, about testing and test removal. 
    1. "If you have an integration test that already covers 10 of your unit tests, then just remove them" - I disagree with that. First of all, most of my unit tests are functional tests. Functional tests and integration tests have very different goals, so I cannot see how one can be a replacement for another. Also, "integration test" is not an alternative to "unit test" - in fact, there are integration tests that can be written on the unit level (e.g. "my DTO class should work with 3rd party serializer"). Maybe "integration" here means "higher level" - in such a case, I disagree as well. I found that in my approach, different levels of tests have different properties and thus are useful for different things, e.g. I find faster tests very useful during refactoring when ran with a CT tool such as NCrunch. 
    2. "Avoid writing tests for a single type" - again, I think that depends on the design approach. I have several open source projects where I use different design approaches and hence my testing strategies are different. One of them is driven solely by end-to-end tests, while another has three-level testing pyramid with single-type tests at the bottom and end-to-end tests at the top.
  12. "There is nothing bad in multiple asserts in a single test as long as it verifies one scenario" - not sure if I see something bad or not about it, but I do multiple "physical" asserts per test as well and find that I never had to regret the decision.