⬆️ ⬇️

Branching. What can be done with them

My last post caused a huge resonance. There were not many comments, but I received a lot of letters, and some readers even made open statements (though, attacks on me personally and on Habr as a whole prevail, but there are also thoughts on the essence of the question). Therefore, I decided to continue to write in the genre "my thoughts about the questions of a well-known company ." With this post I will try to solve two problems: (i) answer the questions and objections of the readers of the previous post and (ii) push in a certain sense the philosophical thought about non-FIC programming. There are quite a few letters, but those who are interested only in one of the posts may miss half.



And another thing : this topic (like the last one) is not hitting anyone. Just interesting to speculate about interesting issues. There is no subtext, hint, challenge. Paranoids and supporters of conspiracy theories will ask to relax.



This time I would like to look at question 4 .

')

Here is his code. A little combed so that he was going to



#include <string> #include <stdexcept> #include <iostream> class CodeGenerator { public: enum Lang {JAVA, C_PLUS_PLUS, PHP}; CodeGenerator(Lang language) { _language=language; } std::string generateCode() { switch(_language) { case JAVA: return std::string("Java: System.out.println(\"Hello world!\");"); case C_PLUS_PLUS: return std::string("C++: std::cout << \"Hello world!\";"); } throw new std::logic_error("Bad language"); } std::string someCodeRelatedThing() // used in generateCode() { switch(_language) { case JAVA: return std::string("http://www.java.com/"); case C_PLUS_PLUS: return std::string("http://www.cplusplus.com/doc/tutorial/"); } throw new std::logic_error("Bad language"); } private: Lang _language; }; int main() { CodeGenerator cg(CodeGenerator::JAVA); std::cout << cg.generateCode() << "\tMore info: " << cg.someCodeRelatedThing() << std::endl; CodeGenerator ec(CodeGenerator::PHP); try { ec.generateCode(); } catch (std::logic_error * e) { std::cout << "ERROR: " << e->what() << std::endl; } return 0; } 


Let's outline what I want to get rid of here:

Let's move consistently. Letters turned out a lot, but they are all simple and, I hope, will not require excessive brain effort. Pour yourself your favorite drink and proceed.



We divide and conquer



To begin with, we will spread everything that concerns individual languages ​​into separate classes. We did nothing new, but it became a little bit easier to develop and test.



 #include <string> #include <stdexcept> #include <iostream> class CodeGeneratorJavaProcessor { public: std::string code() { return std::string("Java: System.out.println(\"Hello world!\");"); } std::string thing() { return std::string("http://www.java.com/"); } }; class CodeGeneratorCppProcessor { public: std::string code() { return std::string("C++: std::cout << \"Hello world!\";"); } std::string thing() { return std::string("http://www.cplusplus.com/doc/tutorial/"); } }; class CodeGenerator { public: enum Lang {JAVA, C_PLUS_PLUS, PHP}; CodeGenerator(Lang language) { _language=language; } std::string generateCode() { switch(_language) { case JAVA: return CodeGeneratorJavaProcessor().code(); case C_PLUS_PLUS: return CodeGeneratorCppProcessor().code(); } throw new std::logic_error("Bad language"); } std::string someCodeRelatedThing() { switch(_language) { case JAVA: return CodeGeneratorJavaProcessor().thing(); case C_PLUS_PLUS: return CodeGeneratorCppProcessor().thing(); } throw new std::logic_error("Bad language"); } private: Lang _language; }; int main() { CodeGenerator cg(CodeGenerator::JAVA); std::cout << cg.generateCode() << "\tMore info: " << cg.someCodeRelatedThing() << std::endl; CodeGenerator ec(CodeGenerator::PHP); try { ec.generateCode(); } catch (std::logic_error * e) { std::cout << "ERROR: " << e->what() << std::endl; } return 0; } 


We will not linger long on the discussion of this purely mechanical improvement.



Single interface and multiple switch failure



Let's create a single interface for all languages. Now we can store in CodeGenerator not a language code, but a pointer to the generator class of this language. The conversion of a language code to a class occurs once.



 #include <string> #include <stdexcept> #include <iostream> class CodeGeneratorAbstractProcessor { public: virtual std::string code() = 0; virtual std::string thing() = 0; virtual ~CodeGeneratorAbstractProcessor() {} }; class CodeGeneratorJavaProcessor : public CodeGeneratorAbstractProcessor { public: std::string code() { return std::string("Java: System.out.println(\"Hello world!\");"); } std::string thing() { return std::string("http://www.java.com/"); } }; class CodeGeneratorCppProcessor : public CodeGeneratorAbstractProcessor { public: std::string code() { return std::string("C++: std::cout << \"Hello world!\";"); } std::string thing() { return std::string("http://www.cplusplus.com/doc/tutorial/"); } }; class CodeGeneratorBadProcessor : public CodeGeneratorAbstractProcessor { public: std::string code() { throw new std::logic_error("Bad language"); return std::string(); } std::string thing() { throw new std::logic_error("Bad language"); return std::string(); } }; class CodeGenerator { public: enum Lang {JAVA, C_PLUS_PLUS, PHP}; CodeGenerator(Lang language) : processor(0) { switch(language) { case JAVA: processor = new CodeGeneratorJavaProcessor(); break; case C_PLUS_PLUS: processor = new CodeGeneratorCppProcessor(); break; case PHP: processor = new CodeGeneratorBadProcessor(); break; } } ~CodeGenerator() { delete processor; } std::string generateCode() { return processor->code(); } std::string someCodeRelatedThing() { return processor->thing(); } private: CodeGeneratorAbstractProcessor * processor; }; int main() { CodeGenerator cg(CodeGenerator::JAVA); std::cout << cg.generateCode() << "\tMore info: " << cg.someCodeRelatedThing() << std::endl; CodeGenerator ec(CodeGenerator::PHP); try { ec.generateCode(); } catch (std::logic_error * e) { std::cout << "ERROR: " << e->what() << std::endl; } return 0; } 


I understand that it was about this design that was mentioned in the reply to my previous note. The author doubted that there could be anything else to improve. Well, let's see.



And if we do not change the view of things?



Let's take a look at what CodeGenerator is doing now. Just two things:

Do we even need such a class? Let's leave this question open for now and see how this class is supposed to be used. Obviously, something like this:



 void i_like_to_convert_enum_to_lang(CodeGenerator::Lang lang_code) { CodeGenerator cg(lang_code); std::cout << cg.generateCode() << "\tMore info: " << cg.someCodeRelatedThing() << std::endl; } 


That is, it is assumed that we are dragging the enum through life, and we will transform it into an algorithm where we suddenly need it. With all the overhead: creating / deleting objects, checking the correctness of our enum-a, handling exceptions ... Would it be wiser to once convert the enum into an object and continue to live with it? This would solve many problems and provide many benefits. Say, a single end-to-end object could collect statistics about its work, cache something ... How do you like this solution:



 void i_like_to_use_generators(CodeGeneratorAbstractProcessor * cg) { std::cout << cg->code() << "\tMore info: " << cg->thing() << std::endl; } 


Here we really abandoned CodeGenerator.



Of course, such a decisive step did not go without changing the interface, but for how many advantages we received!



By the way (going back to the discussion of the previous note), pay attention, as soon as we abandoned the _language bomb and the processor that was not like its bomb, the operation of creating and deleting a generator came so close to each other that the need to remove polymorphic objects disappeared.



And now is the time to screw up the lyrical digression.



<Lyrical digression>



Among programmers who are prone to every kind of haskel, there are curious movements of fighters against if and other branches.



The problem with ifs is that they multiply as an infection. I will give an example that can often be found in real life. Suppose you are writing a program with the ability to enable debugging.



Often this is done like this:



 #include <iostream> #include <unistd.h> int main(int argc, char *argv[]) { //   ( ) bool debug(false); int r; while ((r = getopt(argc, argv, "d")) != -1) { switch ( r ){ case 'd': debug = true; break; } } //      , //     // ( ) if (debug) { std::cout << "Some debugging" << std::endl; } return 0; } 


Examples of such code can be found in the sources cdrtools, Python, git, cmake, m4 (debug variable), firefox (debug_mode), mpalyer / mencoder (debug, b_debug fields in various structures), x11vnc (debug / crash_debug / debug_pointer) ... look at the source code of your favorite program and you will find additional illustrations.



The problem is that the first if converts one logical value to another. And then, in the main code, this second logical value is interpreted each time.



This of course does not lead to a decrease in performance (even on the contrary! This is a very good approach in terms of performance), but it clutters up the code and makes its support very complex. It’s easy to make a mistake in this whole bunch of code. This bunch of code is hard to change.



But, fortunately, there is another solution. Type of such:



 #include <iostream> #include <unistd.h> class noout : public std::ostream {}; noout& operator<< (noout& out, const char* s) { return out; } int main(int argc, char *argv[]) { //       //    noout e; std::ostream *debug(&e); int r; while ((r = getopt(argc, argv, "d")) != -1) { switch ( r ){ case 'd': debug = &std::cout; break; } } //     *debug << "Some debugging" << std::endl; return 0; } 


Here, the conversion is performed immediately to the object, which determines what to do with debugging messages.



In the main body of the program, branching is no longer needed.



Moreover, it is now very easy to develop this code. For example, we can add a new stream to direct debug information.



 #include <iostream> #include <unistd.h> class noout : public std::ostream {}; noout& operator<< (noout& out, const char* s) { return out; } int main(int argc, char *argv[]) { //     //      noout e; std::ostream *debug(&e); int r; while ((r = getopt(argc, argv, "de")) != -1) { switch ( r ){ case 'd': debug = &std::cout; break; case 'e': debug = &std::cerr; break; } } //        //  ,     *debug << "Some debugging" << std::endl; return 0; } 


Of course, this implementation only illustrates the approach. Of course I forgot to add



 noout& operator<< (noout& out, const signed char* s); noout& operator<< (noout& out, const unsigned char* s); ... 


and many, many things. Of course, it would be more appropriate to use a complete logging tool. This is all clear. But I hope that the idea itself, I illustrated.



And, pay attention, and here the objects live a polymorphic life, but here we still do not need polymorphic deletion (this again going back to the previous article).



</ Lyrical digression>



Separate the interface from the implementation



Let's rewrite the CodeGeneratorAbstractProcessor class a bit by demonstrating the concept of NVI. See how nicely the implementation is separated from the interface:



 #include <string> #include <stdexcept> #include <iostream> class CodeGeneratorAbstractProcessor { public: std::string generateCode() { return code(); } std::string someCodeRelatedThing() { return thing(); } protected: ~CodeGeneratorAbstractProcessor() {} private: virtual std::string code() = 0; virtual std::string thing() = 0; }; class CodeGeneratorJavaProcessor : public CodeGeneratorAbstractProcessor { public: std::string code() { return std::string("Java: System.out.println(\"Hello world!\");"); } std::string thing() { return std::string("http://www.java.com/"); } }; class CodeGeneratorCppProcessor : public CodeGeneratorAbstractProcessor { public: std::string code() { return std::string("C++: std::cout << \"Hello world!\";"); } std::string thing() { return std::string("http://www.cplusplus.com/doc/tutorial/"); } }; class CodeGeneratorBadProcessor : public CodeGeneratorAbstractProcessor { public: std::string code() { throw new std::logic_error("Bad language"); return std::string(); } std::string thing() { throw new std::logic_error("Bad language"); return std::string(); } }; void i_like_to_use_generators(CodeGeneratorAbstractProcessor * cg) { std::cout << cg->generateCode() << "\tMore info: " << cg->someCodeRelatedThing() << std::endl; } int main() { CodeGeneratorJavaProcessor java; i_like_to_use_generators(&java); return 0; } 


Open methods implement the interface. Private and virtual methods are used to customize the behavior of a class. These two things can now be juggled with ease and ease. Doing both the best. For example:



 #include <string> #include <stdexcept> #include <iostream> class CodeGeneratorAbstractProcessor { public: std::string generateCode() { return lang() + ": " + code(); } std::string someCodeRelatedThing() { return thing(); } protected: ~CodeGeneratorAbstractProcessor() {} private: virtual std::string lang() = 0; virtual std::string code() = 0; virtual std::string thing() = 0; }; class CodeGeneratorJavaProcessor : public CodeGeneratorAbstractProcessor { public: std::string lang() { return std::string("Java"); } std::string code() { return std::string("System.out.println(\"Hello world!\");"); } std::string thing() { return std::string("http://www.java.com/"); } }; class CodeGeneratorCppProcessor : public CodeGeneratorAbstractProcessor { public: std::string lang() { return std::string("C++"); } std::string code() { return std::string("std::cout << \"Hello world!\";"); } std::string thing() { return std::string("http://www.cplusplus.com/doc/tutorial/"); } }; class CodeGeneratorBadProcessor : public CodeGeneratorAbstractProcessor { public: std::string lang() { throw new std::logic_error("Bad language"); return std::string("???"); } std::string code() { throw new std::logic_error("Bad language"); return std::string(); } std::string thing() { throw new std::logic_error("Bad language"); return std::string(); } }; void i_like_to_use_generators(CodeGeneratorAbstractProcessor * cg) { std::cout << cg->generateCode() << "\tMore info: " << cg->someCodeRelatedThing() << std::endl; } int main() { CodeGeneratorJavaProcessor java; i_like_to_use_generators(&java); return 0; } 


And the user interface and the “interface” for the developer can now be made compact, not inconsistent, convenient, logical. And the class itself now dictates to everyone a certain discipline and structure.



Thus, we have fulfilled all the wishes stated at the beginning of the article. It is time to stop. I take off my hat to all who read to the end. Thank.



And the picture shows a dolmen, which has nothing to do with the article. It was just they who wanted to issue an article on this topic.

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



All Articles