
A Few Words About Using Enums in a Changing Environment
This post offers a solution to problems with the use of enumerations when changing the composition of constants or the presence of duplication of code when using them. In other cases, the application of the approach described below is usually impractical.
In Java, starting with version 1.5, among other things, the so-called enum appeared. There are a number of benefits of using enums against named constants:
However, compared with, say, C ++, enumerations in Java are complete objects, which gives the developer much more flexibility.
Given all of the above, we can say that transfers in Java are no longer just constants (i.e. data), but full-fledged objects, from which we can draw certain conclusions about the scope of their use.
When using the approach proposed below, it is necessary to take into account that it is rather complicated. In accordance with the following post , this design should not be used in simple cases.
Its purpose is to simplify life in complex expansion-oriented situations, such as:
In the absence of these symptoms, the application of the approach below is doubtful and even dangerous, because the increased complexity of the code requires a more thorough analysis in case of any errors.
Now, in fact, let's move on to the essence of the issue that I wanted to consider.
Enumerations should be used in those cases for which switches were used in the pluses. Since enumerations can contain inherited methods, as well as implement interfaces, all the logic of the switch branch handlers can be put into methods of the enumeration class. Remember this as a fact, and consider the following problem.
Due to the apparent simplicity of the enumerations, as well as the fact that from the experience of languages like C ++ we are used to treating them as data, the enumerations control the behavior of the system in various parts of the system, as a rule, weakly connected with each other. Moreover, if we transfer all conditional logic to methods of enumeration (to get rid of branching), inevitably there is saturation and overflow of logic contained in the enumeration class. This also leads to increased connectivity of the modules.
In this case, you can do the following:
Thus, an approach is obtained that is characterized by the following pluses and minuses:
All of these disadvantages can be resolved by writing the appropriate support for this functionality in the form of a plugin for the IDE used. This, of course, requires some costs, but can be solved centrally and once, after which it can be used always and everywhere.
For example, we have the following set of classes (in different modules):
It can be seen that the modules are rather weakly interconnected.
Suppose we, at the request of the customer, have a new document status - sent for confirmation (VERIFY).
When adding a new status, you will have to add a new case in all proposed places. It’s very easy to forget to add it somewhere. Of course, you can provide default-blocks that throw exceptions, but for a large system this does not guarantee that all places will be noticed.
It is proposed to convert this code to the following form:
It can be seen that the client code has become much shorter and more understandable.
Of course, the handlers themselves still need to be written, but now when adding a new enumeration element, its handlers will have to be written. There is no longer any danger that they will forget about it (intentional sabotage and abandonment “for later” are not considered), at least at least they will think. And, as already mentioned, you can always get a default handler:
PS I wait from the habrasociety for comments on the validity of this approach. If you get enough votes, I’m ready to implement this plugin for IntelliJ IDEA, Eclipse and NetBeans.
UPD Added section "Simplicity is power!" in response to the appropriate post to show its place within the given approach.
UPD 2. By popular demand, he added an example. Also at the beginning of the post I added a brief description of the applicability of this solution.
Description of Enums in Java
In Java, starting with version 1.5, among other things, the so-called enum appeared. There are a number of benefits of using enums against named constants:
- The compiler guarantees correct type checking
- Ease of iteration over all possible enumeration values
- They take up less space in the switch block (no class name needed)
- etc.
However, compared with, say, C ++, enumerations in Java are complete objects, which gives the developer much more flexibility.
- First, all enumerations are inherited from the java.lang.Enum class, which has a number of convenient methods, namely:
- name () - the name of the constant as a string
- ordinal () - the order of the constant (corresponds to the order in which the constants are declared )
- valueOf () - a static method that allows you to get an enumeration object by class and name - Further, as it has already been announced, the enumeration class has the opportunity to get all possible enumeration values by calling the java.lang.Class.getEnumConstants () method of the enumeration class
- In the enumeration class, it is possible to specify constructors (private ones only), fields and methods
- Enumerations can implement any interface
- Moreover, the methods in the enumeration can be abstract, and specific instances of constants can determine such methods (as, by the way, and redefine already defined ones)
Given all of the above, we can say that transfers in Java are no longer just constants (i.e. data), but full-fledged objects, from which we can draw certain conclusions about the scope of their use.
Simplicity is power!
When using the approach proposed below, it is necessary to take into account that it is rather complicated. In accordance with the following post , this design should not be used in simple cases.
Its purpose is to simplify life in complex expansion-oriented situations, such as:
- The presence of several loosely coupled modules using the same enumeration class
- The use of precedents for changing the composition of enumeration constants
- Existence of code duplication in switch blocks using this enumeration
In the absence of these symptoms, the application of the approach below is doubtful and even dangerous, because the increased complexity of the code requires a more thorough analysis in case of any errors.
Using Enumerations in a Changing Environment
Now, in fact, let's move on to the essence of the issue that I wanted to consider.
Enumerations should be used in those cases for which switches were used in the pluses. Since enumerations can contain inherited methods, as well as implement interfaces, all the logic of the switch branch handlers can be put into methods of the enumeration class. Remember this as a fact, and consider the following problem.
Due to the apparent simplicity of the enumerations, as well as the fact that from the experience of languages like C ++ we are used to treating them as data, the enumerations control the behavior of the system in various parts of the system, as a rule, weakly connected with each other. Moreover, if we transfer all conditional logic to methods of enumeration (to get rid of branching), inevitably there is saturation and overflow of logic contained in the enumeration class. This also leads to increased connectivity of the modules.
In this case, you can do the following:
- We break the class of enumeration into handlers, each of which will correspond to one of the modules of the system. This way we solve the problem of congestion of the interface of the enumeration class itself.
- It remains to solve the connectivity problem. To do this, we assign an interface to each handler, and we will receive instances through the factory. The factory itself can be created using a declarative approach, i.e. interfaces will be connected with implementations at the configuration level (for example, via xml).
Thus, an approach is obtained that is characterized by the following pluses and minuses:
- + Code duplication is minimized
- + Improved code readability
- + The logic of the handlers can be divided in any way. The handler inheritance hierarchy can be any
- + When adding new handlers (modules) or enum elements, nothing will be forgotten
- + T.K. there are no restrictions on the hierarchy of handlers; you can always provide default handlers
- - Increased coding costs
- - Complication of code structure
- - For cases when it is necessary to explicitly eliminate dependencies between modules (for example, physical separation of modules), it is necessary to keep the factory configuration up to date
All of these disadvantages can be resolved by writing the appropriate support for this functionality in the form of a plugin for the IDE used. This, of course, requires some costs, but can be solved centrally and once, after which it can be used always and everywhere.
Usage example
For example, we have the following set of classes (in different modules):
public enum DocumentStatus { NEW (0), DRAFT (1), PUBLISHED (2), ARCHIVED (3); private DocumentStatus (int statusCode) { this.statusCode = statusCode; } public int getStatusCode () { return statusCode; } private int statusCode; } // Web public class DocumentWorklfowProcessor { ... public listgetAvailableOperations (DocumentStatus status) { List operations; switch (status) { case NEW: operations = ...; break; case DRAFT: operations = ...; break; case PUBLISHED: operations = ...; break; case ARCHIVED: operations = ...; break; } return operations; } public void doOperation (DocumentStatus status, Operation op) throws APIException { switch (status) { case NEW: // one set of operation parameters break; case DRAFT: // another set of operation parameters break; case PUBLISHED: // third set of operation parameters break; case ARCHIVED: // etc. break; } } } // Scheduled task public class ReportGenerationProcessor { ... public void generateReport (Report report) { DocumentStatus status = report.getDocument (). GetStatus (); ReportParams params; switch (status) { case NEW: case DRAFT: // params = some selection criteria for the elements displayed in the report break; case PUBLISHED: // params = other selection criteria for the elements displayed in the report break; case ARCHIVED: // etc. break; } // Report generation } }
It can be seen that the modules are rather weakly interconnected.
Suppose we, at the request of the customer, have a new document status - sent for confirmation (VERIFY).
When adding a new status, you will have to add a new case in all proposed places. It’s very easy to forget to add it somewhere. Of course, you can provide default-blocks that throw exceptions, but for a large system this does not guarantee that all places will be noticed.
It is proposed to convert this code to the following form:
public interface IReportGeneratorProcessor { public ReportParams getReportParams (); } public interface IDocumentWorklfowProcessor { public listgetAvailableOperations (); public void doOperation (Operation op) throws APIException; } public enum DocumentStatus { // Here, instead of new, you can use the factory or even lazy-initialization in get-methods NEW (0, new NewDocReportGeneratorProcessor (), new NewDocWorklfowProcessor ()), DRAFT (1, new DraftDocReportGeneratorProcessor (), new DraftDocWorklfowProcessor ()), PUBLISHED (2, ...), ARCHIVED (3, ...); private DocumentStatus (int statusCode, IReportGeneratorProcessor reportProc, IDocumentWorklfowProcessor docProc) { this.statusCode = statusCode; this.reportProc = reportProc; this.docProc = docProc; } public int getStatusCode () { return statusCode; } public IReportGeneratorProcessor getReportGeneratorProcessor () { return reportProc; } public IDocumentWorklfowProcessor getDocumentWorklfowProcessor () { return docProc; } private int statusCode; private IReportGeneratorProcessor reportProc; private IDocumentWorklfowProcessor docProc; } // Web public class DocumentWorklfowProcessor { ... public list getAvailableOperations (DocumentStatus status) { return status.getDocumentWorklfowProcessor (). getAvailableOperations (); } public void doOperation (DocumentStatus status, Operation op) throws APIException { status.getDocumentWorklfowProcessor (). doOperation (op); } } // Scheduled task public class ReportGenerationProcessor { ... public void generateReport (Report report) { DocumentStatus status = report.getDocument (). GetStatus (); ReportParams params = status.getReportGeneratorProcessor (). GetReportParams (); // Report generation } }
It can be seen that the client code has become much shorter and more understandable.
Of course, the handlers themselves still need to be written, but now when adding a new enumeration element, its handlers will have to be written. There is no longer any danger that they will forget about it (intentional sabotage and abandonment “for later” are not considered), at least at least they will think. And, as already mentioned, you can always get a default handler:
public IDocumentWorklfowProcessor getDocumentWorklfowProcessor () { return (docProc! = null)? docProc: DEFAULT_WORKFLOW_PROCESSOR; }
PS I wait from the habrasociety for comments on the validity of this approach. If you get enough votes, I’m ready to implement this plugin for IntelliJ IDEA, Eclipse and NetBeans.
UPD Added section "Simplicity is power!" in response to the appropriate post to show its place within the given approach.
UPD 2. By popular demand, he added an example. Also at the beginning of the post I added a brief description of the applicability of this solution.