In my introductory post, I talked about my “solution mechanic” work philosophy, and like any good and trusted auto mechanic would do when you bring your car in for service, I like to take a thorough look around an application’s codebase and operating environment, whether the check engine light is on or not.
I have a list of keywords, naming conventions, anti-patterns, etc. that trigger an investigation and discussion with fellow developers. Sometimes referred to as code smells, addressing these can be an excellent starting point in prioritizing and remediating problems. SOLID programming principals can guide and help identify some; others require experience and deeper understanding.
If you have a codebase that fails to address these issues (there are lots more that aren’t listed), you are probably experiencing brittleness and a high degree of regressions between release cycles. Whether you have inherited this codebase or are the original author, sooner or later you will feel their negative effects.
C# “new” keyword.
I can think of three uses for this keyword: instantiating an object, hiding an inherited member’s implementation, and adding a constraint to a generic type. I am only going to focus on the first two; the constraint use case poses no overt problems.
There is nothing inherently wrong with explicitly creating objects if you know exactly what you want, but in modern application development (and possibly life in general), we often don’t know or don’t care exactly how something gets done, just that we can find someone or something that can do it.
Those familiar with Dependency Injection and interface-based programming know this well. We design an interface/abstraction, a contract if you will, that ensures whatever agrees to (implements) it, we can call upon it “do” without regard to “how”. If you take a dependency on the contract (ability) and not the implementation, you can easily swap out different ones. This flexibility is often referred to as modularization and is core to decoupling a codebase.
The Dependency Inversion Principle states we should depend on abstractions, not concretions. If you are explicitly creating objects (using “new”), those areas of code are locked into that way of getting something done. Think “new is glue”.
The second use of “new” is more subtle and can produce unexpected results. As a class developer, you should be making your intentions clear about what is allowable. If you intend to provide an opportunity for others to extend the functionality of your object, then you add the “virtual” modifier to a member, or “abstract” if they must provide an implementation. Either way, a subclass will “override” the member and that implementation is the one that will be used. Not so if one chooses to “new” the member. The “new” modifier does not actually override the method in the base class. It just hides the method. When casting the derived class to the base, unexpected behaviors arise. When using “override”, you are going to get the derived member’s implementation. If the member was “newed”, you will get the base’s implementation.
There was probably a good reason why the base class developer did not allow extending a certain member. Appling “new” circumvents that intention. I assume that backwards compatibility is what Microsoft had in mind when allowing this modifier. If a base class introduces a new member that happens to be the same as one in a derived class somewhere, the derived class’s member will continue to operate independent of the base’s. This is a bit of a edge case, but something to question when you see it.
Static objects (usually with the name “Utility”, “Helper”, “Manager”, etc.).
What we are hoping for in our software is low coupling so we can test or modify behaviors as things evolve. Static classes are just as bad as directly instantiating objects as we are locking into a specific implementation at that point. Sure, there are acceptable times when we just need a simple utility that will only ever do one thing one way like create a UNIX timestamp.
Naming your utility classes really boils down to a matter or preference – just stick with the same suffix (e.g. TimeUtility, DateUtility). I suggest treating these types of classes in your application as a service just like any other, complete with an interface so you can get that low coupling we are constantly working toward.
Lack of using { } or .Dispose().
This one is huge, especially when IoC is not a controlling factor throughout the codebase. There is a good reason a developer chooses to implement IDisposable – something they are doing holds onto something significant, often unmanaged resources. Unmanaged resources are not cleaned up by the CLR garbage collector, and can contribute to memory leaks or worse. The using statement/block ensures disposal is trivial and always happens, even during an exception.
For applications with properly managed dependency injection, a resolved service’s lifetime is managed implicitly on your behalf. The IoC container and/or runtime host orchestrate resource cleanup. IDisposable implementers are properly disposed of at the end of their configured lifetime.
“Internal” and “sealed” keywords.
So many things are improved, and bad practices prevented by applying these modifiers. Public members are the surface of your API. Don’t provide visibility and access to things on the off chance you think someone might need them. Once public, you are responsible for their maintenance. Using “internal” shields the outside world from changes you may want to make.
For those that design REST services, there are a multitude of functions that are called upon. One does not indecently expose all that functionality to external systems, so why do we feel the need to publicize everything in our service layers and beyond? Probably because we got ourselves into a bind and needed to satisfy an edge case, we weren’t following good encapsulation principles, or we made some excuse such as our unit tests required it (Did you know about the InternalsVisibleToAttribute?). Maybe we weren’t aware that IoC service registration could be done in an encapsulating way, or we never thought our library would be shared with other projects.
In a well-layered system, each component cannot see beyond the immediate layer they are interacting with. Marking classes “internal” can ensure this and prevent you from littering your higher layer codebase with lower layer objects, such as database entities or service layer DTOs. In the simplest case, a project should at least separate database entity objects from the presentation layer. One of the best ways of forcing this separation of concern is by not giving visibility to these objects in higher layer assemblies, which will necessitate creating translatable/adaptable objects to move data back and forth (up and down). The same approach should be taken with service layer implementations. The service interface is your public contract; a same layer DI module/extension method can setup your choice of internalized objects to do the work.
“Sealing” or preventing inheritance might sound unnecessarily cruel, because of course, why wouldn’t you want to pass on your richly functional object? There is a simple answer: you don’t need it to succeed. There is little you can do with an abstract class that you can’t do some other way. Composition should be your preferred design choice and you can implement any number of interfaces or take any number of dependencies toward that goal. You might also see a slight increase in performance with sealed classes as the JIT compiler can produce more efficient code by calling methods non-virtually.
What about extensibility? The designer of a sealed class is only responsible up to that point, probably because that’s the limit of any guarantee they are willing or able to offer. You are, of course, free to write extension methods, but then that makes you responsible for the additional functionality, which should give you pause about your approach.
Multiple classes per file.
This is just a matter of preference and dev team culture. It can be useful in source control check-in review and locating things outside of your IDE, but a lot of times it comes down to ease of visual reference and basic organization practices. Could you lump everything into the root and perform a search? Sure. Should you?... If you do go down the route of multiple classes per file, they should probably be related or very small. Treat the common file as a way to micro-organize.
Methods or classes with a high number of lines of code.
Probably a sign that your class is not adhering to the Single Responsibility Principle. There isn’t an exact number that indicates “high”, but lots of #region blocks are sometimes, but not always, a good indication that the code should be broken up.
Custom Exception classes/flow governed by exceptions.
I put these together because one usually is a sign of another. It is not necessarily bad to be descriptive about something exceptional within your codebase, but that extra information should be used to log/diagnose an unrecoverable issue, not make conditional decisions about what to do next. The if
statement expresses conditions, whose intended usage IS control flow. If you get in the habit of using exceptions in “normal” situations, how do you locate the unusual, truly exceptional ones? Expect to find a good answer from the author when you come across a try-catch pattern.
I typically run across this in service layers, for example, a custom NotFoundException
is thrown, so that higher layer code can choose to deal with the exception differently (web layer would return a 404 status or a background service just create a log entry). Throwing exceptions is one of the most expensive operations in .NET because it must create a stack trace. If your application can’t suffer this pattern, you may want to investigate returning a ServiceResult<>
object, with some factory methods to help generate actionable success and failure states.
What about throwing exceptions as guards or for validation? These constructs are typically meant to provide integrity and throwing an exception seems appropriate since a corrective action needs to be taken to properly evaluate inputs or the state of an object.
Service-locator anti-pattern.
The problem with the service-locator anti-pattern is obvious the moment you go looking for it – it is subtle and crops up in the most peculiar places. Using explicit dependencies (e.g., constructor injection), it is clear what is required to create an object. There may even be opportunities for compile-time errors instead of runtime errors. Every place you discover code that explicitly resolves objects from a service collection could be an area that introduces a breaking change. And don’t even think about injecting the service collection as an explicit dependency (as opposed to a static IoC container) - you just identified that the class requires potentially everything, and you may end up interfering with resolved objects’ lifetime. For those embracing good OOP practices, service-locator violates encapsulation because preconditions are hidden, thus you aren’t sure what the proper usage of the object is.
Are there times when this anti-pattern is acceptable? Yes. Even necessary. Some .NET middleware is registered as a singleton but executes its functionality within a certain context. It is from this context that we need an appropriate service (transient or scoped services cannot be resolved to create a singleton) and the ability to resolve what we need is typically provided through the called function’s parameters.
Significant logic beyond simple variable initialization in constructors.
A constructor is really meant for variable initialization and making an object ready to use. So long as you don’t need to make asynchronous calls, you may indeed have a non-trivial amount of work to properly initialize your object. Asynchronous constructors, while not even possible, make no sense because constructors are meant to immediately return a fully initialized object, not one at some undefined point in the future (e.g., awaiting a void or Task<>).
Adapt or die trying.
AutoMapper and Mapster are great libraries for translating the needs of one thing into another. They are the epitome of the Adapter pattern and can create one type of object from another, typically when we want to cross a boundary in our application with some data. However, these libraries have expanded to support all sorts of pre- and -post processing delegates. Resist the urge to consume these sugary extensions because, not only are we breaking the Single Responsibility Principle, but it can become hard to track down object state changes.
As a dangerous example, I worked on a codebase where AutoMapper’s translation function was used to update EntityFramework entities. I guess the assumption was that all entities were tracked, which they were, as they were passed around functions and layers. Odd constructs were put in place to resolve a DbContext from what I could only term as an “ambient singleton” since they rolled their own notion of a scoped lifetime, tied to a single thread. This was done to ensure the tracked entities were saved back to the instance they were created from, though DbContexts were still created explicitly from time-to-time. Blame was put on EF for being a memory hog and difficult to work with. Partial updates/data inconsistencies resulted and were hard to track down. They were always hunting down integrity issues. Some developers resorted to writing periodically executed database validation and clean-up scripts. There was more than one architectural and design deficiency in that codebase.
Ambient transaction management.
There must have been an article written by someone who just learned about MVC action filters and saw them as a global solution to transaction management, and your project confidently adopted. After all, calling an endpoint should be an idempotent or atomic operation (depending on the REST verb), basically a unit-of-work so we are taught. What better way to ensure consistency than to apply an action broadly - global filters embody the DRY principle, right? The problem is that long transactions can be expensive and cause locking issues. Depending on the amount of interaction all the way down the stack, a lot can be contended for. A TimeoutException
could cause everything to unwind. The user tries again, and again, and now your data layer becomes thrashed; the log file is screaming. You resort to blaming Microsoft and buy a beefier SQL Server.
Transactions are meant to provide integrity around a use case, and this does not always translate into a course-grained service call. There are likely smaller checkpoints (not necessarily in the database sense of the word - an external API call cannot be rolled back) that should compose a higher-order function. For those with complex needs, out-of-band, long running processes may be the better way to handle the work, allowing the web service to respond within a reasonable time.
Internally developed authentication mechanisms.
This should go without saying, but rolling your own security system is extremely risky, and with the wealth of libraries out there, one would be challenged to justify an in-house solution. Leave this type of work to people who think about it every day.
Poor solution/project file and naming organization.
Software developers are generally well-organized people, with all the complexity we manage, we must be. So, when I see this, it really makes me wonder who is in charge. Maybe it has something to do with this.
Hard-coded values.
I briefly mentioned the problem in a previous blog post, but it bears repeating. The hard-coded value exists without any context. Replacing it with an aptly named constant provides that context and is self-documenting. Constants (in a common location) reduce brittleness and are the simplest example of the DRY principle I can think of.
Calling .Wait() or .Result.
If you have found yourself in this position, you took a wrong turn somewhere. Doing either of these things instead of properly awaiting the task is going to (eventually) cause a deadlock. The general advice is to “await” all the way down. That is to say, your top-level method (API endpoint, event handler, etc.) should be marked as async (returning void or Task/ValueTask), then you can await calls to service layers, which in turn can await calls into the data layer or other services.
I’m not sure there is an argument to be made where the top-level method cannot be asynchronous. Even the program entry point static Main() method can be async now. More and more of .NET and 3rd-party libraries are only being delivered with asynchronous methods. As Pink Floyd sang, “Welcome, to the [state] machine.”
There is one case where you don’t have to await and that is typically if the very last line is a call to a function returning a Task. You can just return that line. Awaiting it is only going to cause the compiler to generate state machine constructs for what essentially is a passthrough at that point.
Lack of tests.
I’m not a big proponent of TDD. typically, the motivation behind it is to increase confidence; however, no matter how good we think we wrote our tests, unit or otherwise, there is always a chance that coverage is incomplete or assumptions we made about runtime scenarios were wrong. Maybe you can gain confidence in your code, but can you ever have complete confidence in your tests?
I tend to favor more realistic approaches to testing such as integration/functional tests and especially user acceptance testing. Am I saying don’t write unit tests? Not at all. There is certainly a time and a place for encapsulating limited scope proof. Just be careful about what you should be testing – a (typically public) method that produces expected outputs for specific inputs, not the behavior of a dependency within the method. We are looking to validate the smallest atomic working units within the application.
One of the reasons I believe user acceptance testing is far more important is that it can form the basis of a contract with a customer. Interacting with software that operates as it is understood to, is truly all that matters. Customers don’t care that you can show 1000+ units tests passing if their expectations for the application are not met.
There are plenty of testing frameworks available, even ones that simulate multi-step user interaction. You should be employing at least something in your development/release process, but if you really want to gain confidence in your work, may I suggest the dogfooding approach.
Reliance on convention over configuration.
I’m not a big fan of magic. Given what I do for a living, I really need to know how some things work. The wonderous “magic” that happens when we give a folder a special name, apply a certain prefix or suffix to a class name, or just so happen to name a method “Configure” in a Startup.cs file is not helping me one bit to track down configuration issues (if I’m even sure that is what the problem is). I get that we want to be efficient and may not like all the ceremony that may go into configuring an application, but for me, visibility enlightens and provides opportunities for in-code documentation, and that’s truly wonderful.
Watch out for (k)nots.
The exclamation point: a logic altering character so skinny that it can easily go unnoticed. Sure, does it make the code more concise, yes, but at the cost of readability. Is it easier to read if(boolean == false)
rather than if(!boolean)
? Your eyes can decide in the middle of the night as you are hunting down a production problem. This simple negative (re)mark has frustrated and wasted many hours of my life.