The concept of good or bad design is relative. At the same time, there are some well-established norms of programming, which in most cases guarantee it efficiency, maintainability, testability. For example, in object-oriented languages, this is the use of encapsulation, inheritance, polymorphism. There is a set of design patterns that in some cases have a positive effect on the design of the application (and sometimes negative, it all depends on the situation). On the other hand, there are opposing norms, the adherence to which sometimes leads to a design that can be called problematic. Such a design usually has the following features (one or more at a time):
- Stiffness (it is difficult to change the code, since a simple change affects many places);
- Immobility (it is difficult to divide the code into modules that can be used in other programs);
- Viscosity (to develop and / or test the code is quite difficult);
- Unnecessary Complexity (there is an unused functional in the code);
- Unnecessary Repeatability (Copy / Paste);
- Bad readability (it is difficult to understand what the code does, it is difficult to maintain it);
- Fragility (it is easy to break the functional even with small changes);
They need to be able to understand and distinguish in order to avoid a problem design or to anticipate the possible consequences of its use. These attributes are described in Robert Martin’s Agile Principles, Patterns, and Practices in C #. However, in it, as in some other review articles on this topic (which, by the way, not very much), gives a rather brief description of them and, as a rule, there are no code examples.
Let us try to correct this deficiency, focusing more or less on each of these signs.
Rigidity
As already indicated above, the hard code can be difficult to change, even in small things. This may not be a problem if the code changes infrequently or does not change at all. Thus, the code turns out to be quite good. But if he periodically demands changes and it becomes difficult to do it, he becomes problematic. Even if it works.
')
One of the common cases of stiffness is the explicit indication of class types instead of using abstractions (interfaces, base classes, etc.). Below is a sample code.
class A { B _b; public A() { _b = new B(); } public void Foo() {
Here, class A is strictly dependent on class B. That is, if in the future it is decided to use another class instead of B, this will require a change in class A and, consequently, its retesting. In addition, if many other classes are strictly dependent on B, the situation can be complicated many times over.
The way out may be the introduction of abstraction, that is, a certain IComponent interface and the introduction of such a dependency through the constructor (or something else) of class A. In this case, class A ceases to depend on a particular class B, but depends only on the IComponent interface. Class B, in turn, must implement the IComponent interface.
interface IComponent { void DoSomething(); } class A { IComponent _component; public A(IComponent component) { _component = component; } void Foo() {
Let's give a more specific example. Let there be some set of classes that log information - ProductManager and Consumer. Their task is to save some product in the database and order it accordingly. Both classes log relevant events. Let at first logging was carried out in a file. For this, the FileLogger class was used. In this case, the classes were located in different modules (assemblies).
If at the beginning there was only enough file, then there is a need to log into other storages, such as a database or a cloud service for collecting and storing information, then you will need to change all classes in the business logic module (Module 2) using FileLogger. Ultimately, this can be difficult. To solve this problem, you can enter an abstract interface to work with the logger, as shown below.
In this case, when changing the type of logger, it is enough to change the client code (Main), which initializes the logger and inserts it into the Designer ProductManager and Consumer. Thus, we closed the business logic classes from modification with regard to the type of logger, which was what was required.
In addition to direct references to the classes used, rigidity can manifest itself in any other way, leading to difficulties when changing the code. There may be an infinite number of them, but we will try to give another example. Suppose there is some code that displays the area of ​​geometric shapes to the console.
static void Main() { var rectangle = new Rectangle() { W = 3, H = 5 }; var circle = new Circle() { R = 7 }; var shapes = new Shape[] { rectangle, circle }; ShapeHelper.ReportShapesSize(shapes); } class ShapeHelper { private static double GetShapeArea(Shape shape) { if (shape is Rectangle) { return ((Rectangle)shape).W * ((Rectangle)shape).H; } if (shape is Circle) { return 2 * Math.PI * ((Circle)shape).R * ((Circle)shape).R; } throw new InvalidOperationException("Not supported shape"); } public static void ReportShapesSize(Shape[] shapes) { foreach(Shape shape in shapes) { if (shape is Rectangle) { double area = GetShapeArea(shape); Console.WriteLine($"Rectangle's area is {area}"); } if (shape is Circle) { double area = GetShapeArea(shape); Console.WriteLine($"Circle's area is {area}"); } } } } public class Shape { } public class Rectangle : Shape { public double W { get; set; } public double H { get; set; } } public class Circle : Shape { public double R { get; set; } }
This code shows that when adding a new shape you will have to change the methods of the ShapeHelper class. One option would be to transfer the drawing algorithm to the classes of geometric figures themselves (Rectangle and Circle), as shown below. This is how we isolate the relevant logic in the respective classes, thus narrowing down the responsibility of the ShapeHelper class to displaying the information on the console.
static void Main() { var rectangle = new Rectangle() { W = 3, H = 5 }; var circle = new Circle() { R = 7 }; var shapes = new Shape[]() { rectangle, circle }; ShapeHelper.ReportShapesSize(shapes); } class ShapeHelper { public static void ReportShapesSize(Shape[] shapes) { foreach(Shape shape in shapes) { shape.Report(); } } } public abstract class Shape { public abstract void Report(); } public class Rectangle : Shape { public double W { get; set; } public double H { get; set; } public override void Report() { double area = W * H; Console.WriteLine($"Rectangle's area is {area}"); } } public class Circle : Shape { public double R { get; set; } public override void Report() { double area = 2 * Math.PI * R * R; Console.WriteLine($"Circle's area is {area}"); } }
As a result, we actually closed the ShapeHelper class for changes to add new types of shapes by using inheritance and polymorphism.
Immobility
Immobility is manifested in the difficulty of splitting code into reusable modules. As a result, the project may lose its ability to evolve and, as a result, will cease to be competitive.
As an example, consider a desktop program, all of whose code is implemented in an executable application file (exe) and has been designed in such a way that it is not provided to separate business logic into separate modules or classes. Then, some time later, the following business requirements arose before the program developer:
- Change the user interface by making it a Web application;
- Publish the functionality of the program as a set of Web services available to third-party clients for use in their own applications.
In this case, these requirements are difficult to implement, since all the program code is sewn into the executable module.
The figure below shows an example of a fixed design as opposed to one that does not suffer from this feature. They are separated by a dotted line. As can be seen from the figure, the distribution of code across reusable modules (Logic), as well as the publication of functionality at the level of Web services, allows its use in various client applications (App), which is a definite plus.

Immobility can also be called monolithic design. It’s hard to break it up like a stone lump into smaller and more useful pieces of code. How to avoid this problem? At the design stage, it is better to think about how likely it is to use this or that functionality in other systems. Potentially reusable code should be immediately placed in separate modules and classes.
Viscosity
Viscosity can be divided into two types:
- Development viscosity;
- Viscosity of the environment.
Development viscosity is manifested in the relative difficulty of following the chosen design of the application. This can happen because there are too many requirements for the programmer to be fulfilled, while there is a much more convenient way of development. The viscosity of the environment can manifest itself in the inefficiency of the process of building, deploying and testing the application.
As a simple example of Development Viscosity, you can work with the constants that are
required (By Design) to be placed in a separate module (Module 1) for use by other components (Module 2 and Module 3).
If, for some reason, the process of assembling a constant module takes considerable time, it will be difficult for developers to wait for it to finish. In addition, you can pay attention to the fact that the module of constants contains rather heterogeneous entities related to fundamentally different parts of the business logic (financial and marketing modules). That is, the constant module can change quite often for reasons that are independent of each other, which can lead to additional problems in the form of a process for synchronizing changes.
All this slows down the development process and can strain programmers. Options for a less viscous design would be to create separate constant modules — one for the appropriate business logic module, or to transfer the constants to the right place without selecting a separate module for them.
An example of environment viscosity is development and testing of an application on a remote client virtual machine. Sometimes this workflow becomes unbearable because of a slow Internet connection, and therefore the developer can systematically neglect the integration testing of written code, which can eventually lead to problems on the client side when using this functionality (Bugs).
Unnecessary Complexity
In this case, the design has unused functionality at the moment (for the future, as it were, but it will come in handy). This fact can make it difficult to support and maintain the program, as well as increase development and testing time. For example, consider a program that requires reading some data from the database. For this, a special component DataManager was written, which is used in another component.
class DataManager { object[] GetData() {
If the developer adds a new method to DataManager to write data to the database (WriteData), which is currently not used, but with very little probability may be useful in the future, this will be a manifestation of the Unnecessary Complexity feature.
Another example of unnecessary complexity is the “for all occasions” interface. For example, consider an interface with a single Process method that accepts an object of type string.
interface IProcessor { void Process(string message); }
If the task was to process a specific message type with a strictly defined structure, it would be easier to create a strongly typed interface, rather than forcing developers to deserialize this line into a specific message type every time.
As a separate point, one can cite an excessive enthusiasm for design patterns in cases where this is not necessary to do. As a result, this may lead to a viscosity design.
Why waste time writing potentially unused code? QA is sometimes required to test such a code because it is actually published and made available for use by third-party clients. This also eats off the release time. To lay the functional for the future is only on condition that the possible benefit from its presence exceeds the cost of its development and testing.
Unnecessary Repeatability
Probably the majority of developers have come across or come across this feature, which consists in multiple copying of either the same logic, or some portion of the code entirely. The main threat is the vulnerability of such a code when making changes - having corrected something in one place, you can forget to do it in another. In addition, more time is required when making changes as compared with the situation when the code is devoid of this feature.
Unnecessary Repeatability can be a consequence of the negligence of developers, and the stiffness / fragility of the design, when not repeating the code is much more difficult and riskier than doing it. However, in any case, repeatability cannot be allowed and the code needs to be constantly improved by transferring reusable areas into common methods and classes.
Bad Readability
This feature is manifested in the fact that the code is difficult to read and understand what it does (and why it does so). The reasons for poor readability can be non-compliance with the requirements for code design (syntax, naming of variables, classes, etc.), intricate implementation logic, and more.
Below is an example of hard-to-read code that implements a method that operates on a Boolean variable.
void Process_true_false(string trueorfalsevalue) { if (trueorfalsevalue.ToString().Length == 4) {
Here you can select several problems at once. First, the names of the methods and variables are not subject to generally accepted conventions. Secondly, the implementation of the method is not the best.
Perhaps you should immediately take a boolean value, not a string. But even if you take a string, it is better to convert it to a boolean value at the beginning of the method, rather than using the method of determining the length of the string. Third, the text of the exception (that's not nice) does not officially correspond to the business style. Reading such texts, one may get the feeling that the code was written by a non-professional (there may be a controversial point, but still). The method could be rewritten as follows (provided that it takes a boolean value, not a string):
public void Process(bool value) { if (value) {
Here is another option for refactoring (if you still need to accept the string).
public void Process(string value) { bool bValue = false; if (!bool.TryParse(value, out bValue)) { throw new ArgumentException($"The {value} is not boolean"); } if (bValue) {
Over difficult to readable code should be refactored, if it starts to bring problems. For example, if its maintenance and reproduction leads to the emergence of a large number of bugs.
Fragility
The fragility of the program means the simplicity of its failure in case of changes. There are two types of failures - compilation errors and run-time errors. The former may be the backside of Stiffness, the latter are the most dangerous, since they are often already on the client’s side. So they are an indicator of fragility.
The indicator is undoubtedly relative. Someone rules the code very carefully and the probability of breakage is low. Someone on the contrary - in a hurry and inattentively. But still, with the same artists, a different code can generate a different number of errors. Probably we can say that the more illegible it is, the more complicated it is, the more it relies on the execution time of the program rather than the compilation stage, the more fragile it is.
At the same time, the functionality often breaks down, which they were not even going to change. It may suffer due to the high coherence of the logic of the various components among themselves.
Consider a specific example. Here, the logic of authorization of a user with a certain role (determined by the roleId parameter) for accessing a certain resource (resourceUri) is rendered into a static method.
static void Main() { if (Helper.Authorize(1, "/pictures")) { Console.WriteLine("Authorized"); } } class Helper { public static bool Authorize(int roleId, string resourceUri) { if (roleId == 1 || roleId == 10) { if (resourceUri == "/pictures") { return true; } } if (roleId == 1 || roleId == 2 && resourceUri == "/admin") { return true; } return false; } }
You may notice that the logic is rather "hairy." Obviously, adding new roles and resources makes it easy to break. As a result, some role may get or lose access to some resource. Reducing embrittlement would help the creation of the Resource class, which would internally store the resource identifier and the list of supported roles, as shown below.
static void Main() { var picturesResource = new Resource() { Uri = "/pictures" }; picturesResource.AddRole(1); if (picturesResource.IsAvailable(1)) { Console.WriteLine("Authorized"); } } class Resource { private List<int> _roles = new List<int>(); public string Uri { get; set; } public void AddRole(int roleId) { _roles.Add(roleId); } public void RemoveRole(int roleId) { _roles.Remove(roleId); } public bool IsAvailable(int roleId) { return _roles.Contains(roleId); } }
, .
? , . , , .
, , , .
Conclusion
. . , , . , .