📜 ⬆️ ⬇️

Enum-switch antipattern

Recently, I often see an interesting pattern in the code. It lies in the fact that to describe a small set of objects, an enum is created, and then in different places of the code, the values ​​from the enumeration are processed using the switch operator.

What does the implementation of this template look like and how is it dangerous? Let's see.

Task Description


Suppose a team is developing a text editor and is going to implement support for several programming languages ​​in it. Of course, not everyone, because there are not enough resources for this, and there will not be much point in it.

An enumeration is created to store the list of supported languages.
')
enum language
public enum Language { Java, CSharp, TSQL } 


For the work of the editor in different places you need to get some parameters that depend on a specific language. For this, the following functions are created:

GetExtensions (Language lang)
 List<string> GetExtensions(Language lang) { switch (lang) { case Language.Java: { List<string> result = new List<string>(); result.Add("java"); return result; } case Language.CSharp: { List<string> result = new List<string>(); result.Add("cs"); return result; } case Language.TSQL: { List<String> result = new List<string>(); result.Add("sql"); return result; } default: throw new InvalidOperationException(" " + lang + "  "); } } 


IsCaseSensitive (Language lang)
 bool IsCaseSensitive(Language lang) { switch (lang) { case Language.Java: case Language.CSharp: return true; case Language.TSQL: return false; default: throw new InvalidOperationException(" " + lang + "  "); } } 


GetIconFile (Language lang)
 string GetIconFile(Language lang) { switch (lang) { case Language.Java: return "bean.svg"; case Language.CSharp: return "cs.svg"; case Language.TSQL: return "tsql.svg"; default: throw new InvalidOperationException(" " + lang + "  "); } } 


Throwing an exception at the end forces the compiler, which needs to ensure the guaranteed result of each function, and the C # compiler cannot control the completeness of the set of values ​​covered by the switch operator. And if the default icon for any new programming language can be thought up in advance, then it is impossible to determine in advance whether an unknown language is case-sensitive, and what extension its source codes will have, so an exception is thrown.

As a result, at first a fairly simple picture emerges. All supported languages ​​are collected in one place. Where it is necessary to define something depending on the language, a switch is inserted. One developer to implement support for 2-3 languages ​​at the same time is very easy. But later with the support and development of the program based on this template, there will be serious problems.

Disadvantages of using enum-switch


The fact is that such an approach creates god objects. The enum itself and each switch play the role of god objects. Any change associated with one of the programming languages ​​supported by the editor will require making a change to all god objects. By working on Java support, you can break code related to C # or TransactSQL.

It will not be possible to distribute languages ​​between several developers, so that each implements support in our editor of any one separate language. Developers will have to make changes to the same files, and combining the improvements can easily break the working code.

The complexity of adding support for a new language will constantly grow, because the size of switch statements will increase, and it will be more and more difficult to control them. Such a program cannot be called quality, because good programs should eventually become better, simpler and cheaper in development and support.

Using the enum-switch approach, developers connect entities with rigid connections, which in reality are practically unrelated to each other. There can be nothing in common between TransactSQL and Java except that someone wanted to open them in one text editor. But in the program code, TransactSQL and Java ended up in the same enum type.

This is a manifestation of the god-object anti-pattern.

However, this pattern can be found manifestation and other antipatterns. The developers of a text editor do not participate in the development of programming languages, they are only engaged in the implementation of the logic of their own software product. Therefore, for the editor, language features are external data that it should be able to process. Here this data is part of the code. That is, it turned out a kind of hardcoding. If Java is released, in which the source files will have an extension from one letter J, then you will have to redo the editor and check if other languages ​​are broken.

So, the parameters of the individual instances of the set described in the program are the data that should be separated as much as possible from the code that implements the behavior of the program.

The switch statement often specifies a connection between entities from different sets. In our example, this is the connection between a programming language and an icon. However, the relationship between entities is also an entity, and it should be treated as with all other data, for example, stored in a table column. If it is not possible to use external storage, then at least write the link to the Dictionary.

Dictionary
 Dictionary<Language, string> icons = new Dictionary<Language, string>(); icons[Language.Java] = "bean.svg"; icons[Language.CSharp] = "cs.svg"; icons[Language.TSQL] = "tsql.svg"; 


At the same time, the switch statement has another unpleasant side effect. It not only sets the connection between objects, but is itself a connection. To make it clear what this is about, consider the following example:

 switch (lang) { case Language.TSQL: case Language.PLSQL: return "sql.svg"; ... } 

Two SQL dialects are assigned an icon sql.svg. Now, the language has not only an icon, but also an implicit property, which means that the TransactSQL and PL-SQL languages ​​should have the same icons. A developer who wants to change the icon for PL-SQL will decide whether he should change the icon for TransactSQL. In most cases, this is undesirable.

And finally, the enum-switch antipattern contributes to the manifestation of an error like “This value from enum is not provided”, because it is difficult to control when adding a new value to enum full coverage in all switch statements.

There is an exit


What should be done to avoid the use of this template?

In any incomprehensible situation, start the interface. In order not to use enum, get the interface. The interface should return information about the properties of an object from the described set. Add a name to this interface that was previously stored in a constant in enum.

Interface
 interface Language { string GetName(); bool IsCaseSensitive(); string GetIconFile(); List<string> GetExtensions(); } 


The creation of specific objects that implement this interface, assign a separate class provider.

Provider
 class LanguageProvider { List<Language> GetSupportedLanguages() { ... } Language DetectLanguageByFile(string fileName) { ... } Language GetDefaultLanguage() { .... } } 


To store descriptions of objects, you can use any framework. You can hard-code parameters, you can take values ​​from the database, from configuration files, download from an external resource, beat the default object configuration. The provider implementation will not affect the operation of classes using the objects created by the provider.

Now remove all functions that contain switch, if you had them. You will no longer need them, because the code does not process specific objects, but their properties.

In the example above, after the text editor supports 10–15 different programming languages, adding another language will be reduced to listing the settings from the list of already implemented ones. After all, although there are a lot of programming languages, most of the nuances that affect the editing of source codes are common.

Why do I need enum


Why is it that in most programming languages ​​there is such a type as enum?

It is convenient to use it if you do it with some caution. First of all, the enum can be applied where the number of objects is small. Each developer determines the permissible limit at his discretion. I wouldn’t merge more than 20 constants into enum.

The described set should consist of objects, the differences between which can be parameterized. For example, the days of the week differ from each other only in the sequence number, so they are well described in terms of the enum. And here any weather phenomena to enumerate in enum rather not worth it, because they have very little in common.

The set of enumerated objects must be either fixed, in which new values ​​no longer appear, or internal, which is fully defined and used only within one program.

Typical examples of the use of enum:

Source: https://habr.com/ru/post/312792/


All Articles