HN user

jammycakes

299 karma

[ my public key: https://keybase.io/jammycakes; my proof: https://keybase.io/jammycakes/sigs/vrd7Del8qi-li4kpqxXJbxtHxRowIeXHElqMaRm34G4 ]

Posts4
Comments88
View on HN

That's a good approach if you can cleanly separate out the old code from the new code, and if you can make sure that you've got all the old functionality behind the switch. Unfortunately this can be difficult at times. Feature toggles involving UI elements, third party services or legacy code can be difficult to test automatically, for example. Another risk is accidental exposure: if a feature toggle gets switched on prematurely for whatever reason, you'll end up with broken code in production.

The cases where I've experienced problems with feature toggles have been where we thought we were swapping out all the functionality but it later turned out that due to some subtleties or nuances with the system that we weren't familiar with, we had overlooked something or other.

Feature toggles sound like a less painful way of managing changes, but you really need to have a disciplined team, a well architected codebase, comprehensive test coverage and a solid switching infrastructure to avoid getting into trouble with them. My personal recommendation is to ask the question, "What would be the damage that would happen if this feature were switched on prematurely?" and if it's not a risk you're prepared to take, that's when to move to a separate branch.

This incident highlights a problem that is often overlooked in the debate about feature branches versus feature toggles.

I've worked with both feature branches and feature toggles, and while long lived feature branches can be painful to work with what with all the conflicts, they do have the advantage that problems tend to be uncovered and resolved in development before they hit production.

When feature toggles go wrong, on the other hand, they go wrong in production -- sometimes, as was the case here, with catastrophic results. I've always been nervous about the fact that feature toggles and trunk based development means merging code into main that you know for a fact to be buggy, immature, insufficiently tested and in some cases knowingly broken. If the feature toggles themselves are buggy and don't cleanly separate out your production code from your development code, you're asking for trouble.

This particular case had an additional problem: they were repurposing an existing feature toggle for something else. That's just asking for trouble.

It isn't in practice. Only a minority of methods actually need it.

It's certainly far, far better than having to add exactly the same check after every method call. Which is only what you need to do if you're working in a situation where exceptions are not an option.

Usually, no you don't. You only write a try ... catch or try ... finally block round the entire method body, from the point where you create the resources you may need to clean up to the point where you no longer need them. For example:

    var myFile = File.Open(filename);
    try {
        while ((var s = file.ReadLine()) != null) {
            var entity = ProcessLine(s);
            // do whatever you need to do to entity
        }
    }
    finally {
        myFile.Dispose();
    }
C# gives you the using keyword as syntactic sugar for this:
    using (var myFile = File.Open(filename)) {
        while ((var s = file.ReadLine()) != null) {
            ProcessLine(s);
            // do whatever you need to do to entity
        }
    }

You clean up processing that your own method is responsible for. For example, rolling back transactions that it has started, deleting temporary files that it has created, closing handles that it has opened, and so on and so forth. You rarely if ever need to know what kind of exception was thrown or why in order to do that.

You can only assume that the methods you have called have left their own work in a consistent state despite having thrown an exception. If they haven't, then they themselves have bugs and the appropriate cleanup code needs to be added there. Or, if it's a third party library, you should file a bug report or pull request with their maintainers.

You don't try to clean up other people's work for them. That would just cause confusion and result in messy, tightly coupled code that is hard to understand and reason about.

External services having gone offline, timeouts, and invalid user input are expected conditions you should handle locally.

Not necessarily. You should only handle expected conditions locally if there is a specific action that you need to take in response to them -- for example, correcting the condition that caused the error, retrying, falling back to an alternative, or cleaning up before reporting failure. Even if you do know what all the different failure modes are, you will only need to do this in a minority of cases, and those will be determined by your user stories, your acceptance criteria, your business priorities and your budgetary constraints. That is what I mean by "expected conditions." Ones that are (or that in theory could be) called out on your Jira tickets or your specification documents.

For anything else, the correct course of action is to assume that your own method is not able to fulfil its contract and to report that particular fact to its caller. Which is what "yeeting exceptions up the call stack" actually does.

Almost everything else you listed represents a bug in your software that should terminate execution.

Well of course it represents a bug in your software, but you most certainly do not terminate execution altogether. You perform any cleanup that may be necessary, you record an event in your error log, and you show a generic error message to whoever needs to know about it, whether that be the end user or your support team.

Again, what action you need to do in these cases will depend on your user stories, your acceptance criteria, your business priorities and your budgetary constraints. But it is usually done right at the top level of your code in a single location. That is why "yeeting exceptions up the call stack" is appropriate for these cases.

You only terminate execution altogether if your process is so deeply diseased that for it to continue would cause even more damage. For example, memory corruption or failures of safety-critical systems.

I’m more than a little shocked that you think yeeting exceptions up the call stack is appropriate for these cases.

I hope I've clarified what "yeeting exceptions up the call stack" actually does.

The alternative to "yeeting exceptions up the call stack" when you don't have any specific cleanup or corrective action that you can do is to continue execution regardless. This is almost never the correct thing to do as it means your code is running under assumptions that are incorrect. And that is a recipe for data corruption and all sorts of other nasties.

No; you simply abstract the underlying subsystem’s exceptions in your own types, the same way you do with any other type.

That's all very well as long as people actually do that. It doesn't always happen in practice. And even when they do, the abstractions are likely to be leaky ones.

And yes, “railway oriented approaches” can absolutely do this.

How? Please provide a code sample to demonstrate how you would do so.

If you want to (and are able to) document all possible failure modes, then checked exceptions will give you that. As far as I can tell, railway oriented approaches can't.

Unfortunately, you can only do that when the number of possible failure modes is fairly limited. In a complex codebase with lots of different layers, lots of different third party components, and lots of different abstractions and adapters, it can quickly become pretty unwieldy. And then you end up with someone or other deciding to take the easy way out and declaring their method as "throws Exception" which kind of defeats the purpose.

Missing dependencies. External services having gone offline. Timeouts. Foreign key violations. Data corruption. Invalid user input. Incorrect assumptions about how a third party library works. Incorrectly configured firewalls. Bugs in your code. Subtle incompatibilities between libraries, frameworks or protocols. Botched deployments. Hacking attacks. The list is endless.

Probably not so much of an issue if you're dealing with well validated CAD data and most of your processing is in-memory using your own code. But if you're working with enterprise applications talking to each other via microservices written by different teams with different levels of competence, legacy code (sometimes spanning back decades), complex and poorly documented third party libraries and frameworks, design decisions that are more political than technical, and so on and so forth, it can quickly mount up.

The unfortunately missing part of exceptions (in mainstream languages) is that they handle this invisibly. Figuring out, at compile time, what sort of exceptions can appear inside a given function is not obvious.

Figuring out, at compile time, what sort of exceptions appear inside a given function is a futile exercise in many contexts, and railway oriented programming does not fix it. Java tried this with checked exceptions and it fell out of favour because it became too unwieldy to manage properly.

In any significantly complex codebase, the number of possible failure modes can be significant, many of them are ones that you do not anticipate, and of those that you can anticipate, many of them are ones that you cannot meaningfully handle there and then on the spot. In these cases, the only thing that you can reasonably do is propagate the error condition up the call stack, performing any cleanup necessary on the way out.

"Handling this invisibly" is also known as "convention over configuration." In languages that use exceptions, everyone understands that this is what is going on and adjusts their assumptions accordingly.

The author followed up this post with another one a few years later titled "Against Railway Oriented Programming":

https://fsharpforfunandprofit.com/posts/against-railway-orie...

Railway-oriented programming is an interesting concept and it does have its use cases, but it does need to come with a massive health warning. I've often seen it used in practice to reinvent exception handling badly, and this is something I consider particularly ill advised because exceptions, when understood and used correctly, provide a much cleaner and more effective way of handling error conditions in most cases.

The thing about exceptions is that in most cases, they make the safe option the default. An error condition is an indication that your code can not do what its specification says that it does, and in that case you need to stop what you are doing, because to continue regardless means that your code will be operating under assumptions that are incorrect, potentially corrupting data. Error conditions can happen for a wide variety of reasons, many of which you do not anticipate and can not plan for, and in those cases the only safe option is to clean up if necessary and then propagate the error up to the caller. Exceptions do this automatically for you by default (you need to explicitly override it with a try/catch block) but alternative approaches, such as railway oriented programming, require you to add in a whole lot of extra boilerplate code that is easy to forget and easy to get wrong. If you can't handle the error condition on the way up the call stack, you would then log it at the top level and report a generic error to the user.

Having said that I see two particular use cases for this kind of technique. The first is situations where you need to handle specific, well defined and anticipated errors right at the point at which they occur. Validation is one example that comes to mind; another example is where you are trying to fetch a file or database record that does not exist. The second is situations where exception handling is not available for whatever reason. Asynchronous code using promises (for example with jQuery) are pretty much an exact implementation of railway oriented programming, but since modern JavaScript now has async/await, we can now use exception handling in these scenarios.

I think a much better pattern would be to enable dev teams to self serve. Set up the required infrastructure and guard rails, then let teams handle their own deployments and infrastructure.

I think that's how DevOps is actually supposed to be done in the first place. You view Ops -- and the code used to manage and support it -- as a product, and get a specialised team of experienced Devs (and architects) to build it.

Once you've got the basic infrastructure and architecture in place, you then train up the individual development teams to customise it, extend it and troubleshoot it as they need to. In much the same way as they do with any other software product.

In a previous job, I joined a team that was supposed to be introducing DevOps to the organisation.

It started out well -- we spent a few months hacking with Terraform, Docker, Vagrant, Kubernetes, and related technologies to implement an infrastructure-as-code approach -- automating the process of provisioning and decommissioning servers, and building a setup where development teams could deploy updates with a simple git push.

Unfortunately it all went downhill fairly rapidly. We ended up spending the majority of our time manually applying security patches to a bunch of snowflake servers that we'd lifted-and-shifted from another hosting provider to AWS, and fielding support requests from the development teams. Within a year, we were being told in no uncertain terms by our project manager that we were an operations team, not a development team.

It felt like a complete bait-and-switch. Within two years, I had left the organisation in question and moved on to a new job elsewhere doing actual development again. Last I heard, the entire team had been disbanded.

It sounds like the author of this article must have had a very similar experience. I wonder just how common it is. It seems that in many places, "DevOps" is all Ops and no Dev.

The big problem that I have with Clean Code -- and with its sequel, Clean Architecture -- is that for its most zealous proponents, it has ceased to be a means to an end and has instead become an end in itself. So they'll justify their approach by citing one or other of the SOLID principles, but they won't explain what benefit that particular SOLID principle is going to offer them in that particular case.

The point that I make about patterns and practices in programming is that they need to justify their existence in terms of value that they provide to the end user, to the customer, or to the business. If they can't provide clear evidence that they actually provide those benefits, or if they only provide benefits that the business isn't asking for, then they're just wasting time and money.

One example that Uncle Bob Martin hammers home a lot is separation of concerns. Separation of concerns can make your code a lot easier to read and maintain if it's done right -- unit testing is one good example here. But when it ceases to be a means to an end and becomes an end in itself, or when it tries to solve problems that the business isn't asking for, it degenerates into speculative generality. That's why you'll find project after project after project after project after project with cumbersome and obstructive data access layers just because you "might" want to swap out your database for some unknown mystery alternative some day.

The problem with explicit error handling is that it's all too easy to get it wrong (by forgetting to check the return value) and when it does go wrong, it goes wrong silently, introducing a risk of leaving you with corrupt data. In production.

The beauty of exceptions, on the other hand, is that the default option is the safe one. Sure, forgetting to add error handling may leave you presenting a user with a stack trace, but at least you're not billing them for something that never gets delivered.

Well in the example I've just given they reduced query times from six minutes to three seconds. If that isn't increased efficiency, then I don't know what is.

The fact is that sometimes you have to ignore the "rules," because the "rules" were designed to serve a purpose that does not apply in your particular case, or perhaps never even applied at all in the first place.

The problem with trying to separate your business layer from your data access layer is that it's often difficult if not impossible to identify which concerns go into which layer. Take paging and sorting for example. If you treat that as a business concern, you end up with your database returning more data than necessary, and your business layer ends up doing work that could have been handled far more efficiently by the database itself. On the other hand, if you treat it as a data access concern, you end up being unable to test it without hitting the database.

You need to realise that software development always involves trade-offs. Blindly sticking to the "rules" is cargo cult, and it never achieves the end results that it is supposed to.

TL:DR Successive, well intentioned, changes to architecture and technology throughout the lifetime of an application can lead to a fragmented and hard to maintain code base. Sometimes it is better to favour consistent legacy technology over fragmentation.

Nice idea in theory, sometimes impossible in practice.

A few years ago I came onto a project that had a very clearly defined separation of concerns, with a business layer, data access layer, presentation layer, and Entity Framework. This was resulting in a number of SQL queries that took over a minute to run and caused web pages to time out.

I ended up cutting right through the layers, bypassing Entity Framework altogether and replacing it with hand-crafted SQL. This ended up cutting down the query time from six minutes to three seconds.

Pet projects need not take up all of your time. All you need is a few hours every so often — an evening or so once every couple of months, or one weekend a year would set you head and shoulders above a lot of people. The whole thing of dark matter developers with no pet projects at all, versus "passionate programmers" who eat, sleep and breathe code, is a completely false dichotomy.

All employers are looking for is some publicly available evidence that you actually have the skills that you claim to have listed on your CV.

I started keeping a comprehensive developer diary (I actually refer to it as lab notes) back in December, and it's made a considerable difference to how I think about what I'm coding.

The way I write it is similar to test-driven development — I write down each step in what I'm doing before I do it. When I run a command, I copy the command itself into my notes, then once it's run I copy the important parts of the output (e.g. any error messages).

It really comes into its own when I want to pick up on a task that I had put to one side a few days previously, or when I run into a problem that I think I'd encountered before. It also makes it much easier to avoid the trap of trying the same thing several times before you realise that you're going round and round in circles.

It's also useful for writing documentation. You can just copy and paste what you've written in your lab notes into your commit summaries, Jira tickets, e-mails, spec, help files, whatever, then tidy up as appropriate.

This.

Also, I'd have thought that with weekend projects you're more likely to find things out by experimentation and reading the documentation than by asking questions. With work or classroom projects, you have to work with a fixed spec, a prescribed technology, all sorts of Best Practices, and deadlines. Weekend projects give you much more freedom in all four respects.

So yes, GitHub would be much more accurate.

While 80 characters may be a bit restrictive, there is a need for a hard limit of something.

I've found that in the absence of a hard limit, you all too quickly end up with a codebase riddled with 250+ line monsters forcing you to scroll right and left all over the place just to get the faintest smidgen of an idea of what's going on. It's a maintenance nightmare.

Meanwhile, GitHub won't show lines longer than 132 characters (on Windows) or 123 characters (on the Mac) without scrolling, no matter how wide your screen.

I personally find 100 seems to strike the right balance.

The Python interpreter itself may not do anything with the annotations, but static type checkers and linters can, and they will give you the safety that you need.

The annotations also allow the development of better IntelliSense-like tools.

If you stick to the rules, and you have only one branches/tags/trunk structure in your repo, and you actually set up the branches/tags/trunk structure in the first place, yes.

But my whole point is that there's far too much scope to get it wrong in ways that can be difficult to fix.

If you have multiple projects in your repo, svn cp and svn switch become cumbersome.

Many people check out the root folder with the entire structure and all the branches, and not just trunk.

I've seen projects that have nested branches/tags/trunk structures.

I've seen people check code in to two branches at once.

I've seen people "branch" by physically copying the files client-side then checking in the result. Losing the relationship between the branches that they need to merge successfully.

If you check in the merge then find you've messed it up, you can't roll back and start over without cluttering up your source history. And even then, how to do that is non-obvious and easy to get wrong.

And people DO make these mistakes repeatedly because svn treats branching and merging as an advanced technique. The first several times you do it, you invariably mess up, and leave a permanent record of how you messed up. Very often it's not you who learns that you've messed up but a colleague who does the merge. And once you learn to get it right, someone else on your team comes along and they get it wrong. The feedback loop is terrible.

Whereas with git it's a core competency so you learn you've messed up fairly quickly. And offline commits let you easily roll back when you do. The feedback loop is rapid and effective.

Number 2 makes me laugh: "Branches are expensive in Subversion - False. A Myth."

Totally misses the point that the expense of branching and merging in Subversion is all in the UI/UX. Not only is it horribly cumbersome, it's also much easier to get it wrong, not obvious that you are getting it wrong, and harder to recover once you've discovered you've got it wrong.

No they don't.

Shelving and checkpointing have been planned for Subversion for several years now and though they're currently slated for version 1.10, currently in development, work on those features hasn't started yet. Given that it's been bounced from 1.9 already, and that Subversion release cycles are measured in geologic eras, I'm not holding my breath.

Source: http://subversion.apache.org/roadmap.html

Depends on what kind of abstraction layer you're talking about.

The traditional BLL/BOL/DAL architecture is probably the worst. It aims for some kind of fantasy "separation of concerns" between the business layer and the data access layer, but many concerns simply can't be classified as exclusively business concerns or exclusively data access concerns. When people attempt to introduce and enforce such a separation, they end up with all sorts of performance problems.