├── .gitignore ├── README.md ├── code ├── c │ ├── sdk-result │ │ ├── .gitignore │ │ ├── CMakeLists.txt │ │ ├── build.sh │ │ ├── include │ │ │ └── sdk │ │ │ │ └── fwd_ctrl.h │ │ ├── src │ │ │ ├── CMakeLists.txt │ │ │ └── fwd_ctrl.c │ │ └── test │ │ │ ├── CMakeLists.txt │ │ │ └── test_chip.cpp │ └── sdk │ │ ├── .gitignore │ │ ├── CMakeLists.txt │ │ ├── build.sh │ │ ├── include │ │ └── sdk │ │ │ └── fwd_ctrl.h │ │ ├── src │ │ ├── CMakeLists.txt │ │ └── fwd_ctrl.c │ │ └── test │ │ ├── CMakeLists.txt │ │ └── test_chip.cpp ├── cpp │ ├── original │ │ ├── Customer.cpp │ │ ├── Customer.h │ │ ├── Main.cpp │ │ ├── Movie.cpp │ │ ├── Movie.h │ │ ├── Rental.cpp │ │ └── Rental.h │ └── result │ │ ├── ChildrensPrice.cpp │ │ ├── ChildrensPrice.h │ │ ├── Customer.cpp │ │ ├── Customer.h │ │ ├── Main.cpp │ │ ├── Movie.cpp │ │ ├── Movie.h │ │ ├── NewReleasePrice.cpp │ │ ├── NewReleasePrice.h │ │ ├── Price.h │ │ ├── RegularPrice.cpp │ │ ├── RegularPrice.h │ │ ├── Rental.cpp │ │ └── Rental.h ├── java │ ├── original │ │ ├── pom.xml │ │ └── src │ │ │ ├── main │ │ │ └── java │ │ │ │ └── magicbowen │ │ │ │ ├── Customer.java │ │ │ │ ├── Movie.java │ │ │ │ └── Rental.java │ │ │ └── test │ │ │ └── java │ │ │ └── magicbowen │ │ │ └── CustomerTest.java │ └── result │ │ ├── pom.xml │ │ └── src │ │ ├── main │ │ └── java │ │ │ └── magicbowen │ │ │ ├── Customer.java │ │ │ ├── Movie.java │ │ │ ├── MovieType.java │ │ │ ├── Rental.java │ │ │ ├── StatementFormatter.java │ │ │ └── TextStatementFormatter.java │ │ └── test │ │ └── java │ │ └── magicbowen │ │ ├── CustomerTest.java │ │ ├── TextStatementFormatterTest.java │ │ └── testdoubles │ │ └── SpyStatementFormatter.java └── js │ ├── original │ ├── package-lock.json │ ├── package.json │ ├── src │ │ └── index.js │ └── test │ │ ├── invoices.json │ │ ├── plays.json │ │ └── testStatement.js │ └── result │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── calculator.js │ ├── createStatementData.js │ ├── formatter.js │ └── index.js │ └── test │ ├── invoices.json │ ├── plays.json │ ├── testHtmlStatement..js │ └── testStatement.js ├── effective-refactoring-1.md ├── effective-refactoring-2.md ├── effective-refactoring-3.md ├── pics ├── 3factor.jpeg ├── code.png ├── cover2-small.png ├── cover2.jpg ├── extractExample.jpeg ├── formular.png └── select.png └── refactoring-2nd.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_store 2 | 3 | code/js/original/node_modules/* 4 | code/js/result/node_modules/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Refactoring 2 | 3 | 本库记录一些关于重构的心得和经验,包含以下内容: 4 | 5 | - [高效重构(一):正确理解重构](./effective-refactoring-1.md) 6 | - [高效重构(二):掌握重构手法](./effective-refactoring-2.md) 7 | - [高效重构(三):构建高效工程环境](./effective-refactoring-3.md) 8 | 9 | - [关于重构的重构 - 《重构》第二版导读](./refactoring-2nd.md) 10 | 11 | - `./code`目录下是示例代码。目前包含C++、Java、JavaScript的版本,随后会添加更多语言的版本。 12 | -------------------------------------------------------------------------------- /code/c/sdk-result/.gitignore: -------------------------------------------------------------------------------- 1 | build/* 2 | .vscode/* -------------------------------------------------------------------------------- /code/c/sdk-result/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.4) 2 | 3 | project(sdk) 4 | 5 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-invalid-offsetof -g -std=gnu++11") 6 | 7 | if(MSVC) 8 | add_definitions(-D_CRT_SECURE_NO_WARNINGS) 9 | add_definitions(-DMSVC_VMG_ENABLED) 10 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /vmg") 11 | endif(MSVC) 12 | 13 | include_directories("${CMAKE_CURRENT_SOURCE_DIR}/include") 14 | 15 | add_subdirectory("src") 16 | 17 | if(ENABLE_TEST) 18 | add_subdirectory(test) 19 | endif() 20 | -------------------------------------------------------------------------------- /code/c/sdk-result/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | target="build" 4 | 5 | if [ ! -d $target ]; then 6 | mkdir -p $target 7 | fi 8 | 9 | cd $target 10 | 11 | echo "*******************************************************************************" 12 | echo "start to build project ..." 13 | 14 | 15 | cmake -DENABLE_TEST=1 .. 16 | cmake --build . 17 | 18 | if [ $? -ne 0 ]; then 19 | echo "FAILED!" 20 | cd .. 21 | exit 1 22 | fi 23 | 24 | echo "*******************************************************************************" 25 | echo "start to run tests..." 26 | 27 | ./test/test_sdk --gtest_color=yes $1 $2 28 | 29 | if [ $? -ne 0 ]; then 30 | echo "FAILED!" 31 | cd .. 32 | exit 1 33 | fi 34 | 35 | cd .. 36 | 37 | echo "*******************************************************************************" 38 | echo "SUCCESS!" 39 | exit 0 -------------------------------------------------------------------------------- /code/c/sdk-result/include/sdk/fwd_ctrl.h: -------------------------------------------------------------------------------- 1 | #ifndef H6AB49706_ADB6_11E9_9140_A45E60C4B579 2 | #define H6AB49706_ADB6_11E9_9140_A45E60C4B579 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | typedef int Status; 9 | 10 | #define SUCCESS (Status)0 11 | #define FAILURE (Status)-1 12 | 13 | #define NULL_PTR 0 14 | 15 | typedef unsigned int PortId; 16 | typedef unsigned int PackageId; 17 | 18 | typedef enum { 19 | CPU_PORT = 0, 20 | FWD_PORT, 21 | } PortType; 22 | 23 | typedef struct Port { 24 | PortId id; 25 | PortType type; 26 | unsigned int count_threshold; 27 | unsigned int bytes_threshold; 28 | } Port; 29 | 30 | typedef struct Chip { 31 | Port* ports[12]; 32 | unsigned int port_number; 33 | } Chip; 34 | 35 | typedef enum { 36 | DATA_PDU = 0, 37 | CTRL_CMD, 38 | HAND_SHAKE, 39 | } PackageType; 40 | 41 | typedef struct { 42 | PackageId id; 43 | PackageType type; 44 | unsigned int bytes; 45 | PortId src_pid; 46 | PortId tgt_pid; 47 | } Package; 48 | 49 | typedef struct { 50 | Package* elems[12]; 51 | unsigned int number; 52 | } Packages; 53 | 54 | Status forward_ctrl(const Chip* chip, Packages* pkgs); 55 | 56 | #ifdef __cplusplus 57 | } 58 | #endif 59 | 60 | #endif 61 | -------------------------------------------------------------------------------- /code/c/sdk-result/src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | file(GLOB_RECURSE all_files 2 | *.cpp 3 | *.cc 4 | *.c++ 5 | *.c 6 | *.C) 7 | 8 | add_library(sdk STATIC ${all_files}) 9 | -------------------------------------------------------------------------------- /code/c/sdk-result/src/fwd_ctrl.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "sdk/fwd_ctrl.h" 3 | 4 | Status forward_ctrl(const Chip* chip, Packages* pkgs) { 5 | unsigned int port_bytes[12] = {0}; 6 | unsigned int port_count[12] = {0}; 7 | int pkg_index = 0; 8 | int port_index = 0; 9 | unsigned int bytes_left = 0; 10 | 11 | if (chip == NULL_PTR || pkgs == NULL_PTR) { 12 | printf("[ERROR]: exist null ptr, chip = %p, pkgs = %p!\n", chip, pkgs); 13 | return FAILURE; 14 | } 15 | 16 | for(pkg_index = 0; pkg_index < pkgs->number && pkg_index < 12; pkg_index++) { 17 | for (port_index = 0; port_index < chip->port_number && port_index < 12; port_index++) { 18 | if (chip->ports[port_index]->id == pkgs->elems[pkg_index]->src_pid) { 19 | break; 20 | } 21 | } 22 | if (port_index >= chip->port_number || port_index >= 12) { 23 | pkgs->elems[pkg_index]->bytes = 0; 24 | printf("[ERROR]: src port %d not found of package %d!\n", pkgs->elems[pkg_index]->src_pid, pkgs->elems[pkg_index]->id); 25 | continue; 26 | } 27 | switch (pkgs->elems[pkg_index]->type) { 28 | case DATA_PDU: 29 | for (port_index = 0; port_index < chip->port_number && port_index < 12; port_index++) { 30 | if (chip->ports[port_index]->id == pkgs->elems[pkg_index]->tgt_pid) { 31 | if (port_count[port_index] < chip->ports[port_index]->count_threshold) { 32 | if (chip->ports[port_index]->type == FWD_PORT) { 33 | if (chip->ports[port_index]->bytes_threshold > port_bytes[port_index]) 34 | { 35 | bytes_left = chip->ports[port_index]->bytes_threshold - port_bytes[port_index]; 36 | if (bytes_left < pkgs->elems[pkg_index]->bytes) { 37 | pkgs->elems[pkg_index]->bytes = bytes_left; 38 | port_bytes[port_index] += pkgs->elems[pkg_index]->bytes; 39 | } 40 | port_bytes[port_index] += pkgs->elems[pkg_index]->bytes; 41 | port_count[port_index]++; 42 | } else { 43 | pkgs->elems[pkg_index]->bytes = 0; 44 | printf("[WARN]: package %d\n meets the bytes threshold of target port %d!\n", pkgs->elems[pkg_index]->id, pkgs->elems[pkg_index]->tgt_pid); 45 | } 46 | } else { 47 | pkgs->elems[pkg_index]->bytes = 0; 48 | printf("[WARN]: cpu port %d does not receive data pdu package %d!\n", pkgs->elems[pkg_index]->tgt_pid, pkgs->elems[pkg_index]->id); 49 | } 50 | } else { 51 | pkgs->elems[pkg_index]->bytes = 0; 52 | printf("[WARN]: package %d meets the count threshold of target port %d!\n", pkgs->elems[pkg_index]->id, pkgs->elems[pkg_index]->tgt_pid); 53 | } 54 | break; 55 | } 56 | } 57 | if (port_index >= chip->port_number || port_index >= 12) { 58 | pkgs->elems[pkg_index]->bytes = 0; 59 | printf("[ERROR]: target port %d not found of package %d!\n", pkgs->elems[pkg_index]->tgt_pid, pkgs->elems[pkg_index]->id); 60 | } 61 | break; 62 | case CTRL_CMD: 63 | for (port_index = 0; port_index < chip->port_number && port_index < 12; port_index++) { 64 | if (chip->ports[port_index]->id == pkgs->elems[pkg_index]->tgt_pid) { 65 | if (chip->ports[port_index]->type == CPU_PORT) { 66 | if (port_count[port_index] < chip->ports[port_index]->count_threshold) { 67 | port_count[port_index]++; 68 | 69 | } else { 70 | pkgs->elems[pkg_index]->bytes = 0; 71 | printf("[WARN]: package %d meets the count threshold of target cpu port %d!\n", pkgs->elems[pkg_index]->id, pkgs->elems[pkg_index]->tgt_pid); 72 | } 73 | } else { 74 | if (chip->ports[port_index]->bytes_threshold > port_bytes[port_index]) 75 | { 76 | bytes_left = chip->ports[port_index]->bytes_threshold - port_bytes[port_index]; 77 | if (bytes_left < pkgs->elems[pkg_index]->bytes) { 78 | pkgs->elems[pkg_index]->bytes = bytes_left; 79 | port_bytes[port_index] += pkgs->elems[pkg_index]->bytes; 80 | } 81 | port_bytes[port_index] += pkgs->elems[pkg_index]->bytes; 82 | port_count[port_index]++; 83 | } else { 84 | pkgs->elems[pkg_index]->bytes = 0; 85 | printf("[WARN]: package %d meets the bytes threshold of target fwd port %d!\n", pkgs->elems[pkg_index]->id, pkgs->elems[pkg_index]->tgt_pid); 86 | } 87 | } 88 | break; 89 | } 90 | } 91 | if (port_index >= chip->port_number || port_index >= 12) { 92 | pkgs->elems[pkg_index]->bytes = 0; 93 | printf("[ERROR]: target port %d not found of package %d!\n", pkgs->elems[pkg_index]->tgt_pid, pkgs->elems[pkg_index]->id); 94 | } 95 | break; 96 | case HAND_SHAKE: 97 | for (port_index = 0; port_index < chip->port_number && port_index < 12; port_index++) { 98 | if (chip->ports[port_index]->id == pkgs->elems[pkg_index]->src_pid) { 99 | if (port_count[port_index] < chip->ports[port_index]->count_threshold) { 100 | if (chip->ports[port_index]->type == CPU_PORT) { 101 | pkgs->elems[pkg_index]->tgt_pid = pkgs->elems[pkg_index]->src_pid; 102 | port_count[port_index]++; 103 | } else { 104 | pkgs->elems[pkg_index]->tgt_pid = pkgs->elems[pkg_index]->src_pid; 105 | port_bytes[port_index] += pkgs->elems[pkg_index]->bytes; 106 | port_count[port_index]++; 107 | } 108 | } else { 109 | pkgs->elems[pkg_index]->bytes = 0; 110 | printf("[WARN]: package %d meets the count threshold of target fwd port %d!\n", pkgs->elems[pkg_index]->id, pkgs->elems[pkg_index]->tgt_pid); 111 | } 112 | break; 113 | } 114 | } 115 | if (port_index >= chip->port_number || port_index >= 12) { 116 | pkgs->elems[pkg_index]->bytes = 0; 117 | printf("[ERROR]: target port %d not found of package %d!\n", pkgs->elems[pkg_index]->tgt_pid, pkgs->elems[pkg_index]->id); 118 | } 119 | break; 120 | default: 121 | break; 122 | } 123 | } 124 | return SUCCESS; 125 | } 126 | -------------------------------------------------------------------------------- /code/c/sdk-result/test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(test_sdk) 2 | 3 | include_directories("${CMAKE_CURRENT_SOURCE_DIR}" 4 | "/usr/local/include/") 5 | 6 | file(GLOB_RECURSE all_files 7 | *.cpp 8 | *.cc 9 | *.c++ 10 | *.c 11 | *.C) 12 | 13 | link_directories("/usr/local/lib") 14 | 15 | add_executable(test_sdk ${all_files}) 16 | 17 | target_link_libraries(test_sdk sdk gtest gtest_main) 18 | -------------------------------------------------------------------------------- /code/c/sdk-result/test/test_chip.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "sdk/fwd_ctrl.h" 3 | 4 | struct SDK_TEST : testing::Test 5 | { 6 | protected: 7 | Chip chip{.port_number = 0}; 8 | Packages pkgs{.number = 0}; 9 | }; 10 | 11 | TEST_F(SDK_TEST, should_fail_when_none_chip_or_none_package) 12 | { 13 | ASSERT_EQ(FAILURE, forward_ctrl(NULL_PTR, NULL_PTR)); 14 | ASSERT_EQ(FAILURE, forward_ctrl(NULL_PTR, &pkgs)); 15 | ASSERT_EQ(FAILURE, forward_ctrl(&chip, NULL_PTR)); 16 | } 17 | 18 | TEST_F(SDK_TEST, should_not_forward_data_pdu_to_none_port) 19 | { 20 | Package pkg{.id = 1, .type = DATA_PDU, .bytes = 1024}; 21 | pkgs.elems[0] = &pkg; 22 | pkgs.number = 1; 23 | 24 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 25 | 26 | ASSERT_EQ(1, pkg.id); 27 | ASSERT_EQ(0, pkg.bytes); 28 | ASSERT_EQ(DATA_PDU, pkg.type); 29 | } 30 | 31 | TEST_F(SDK_TEST, should_not_forward_data_pdu_whose_src_port_is_not_exist) 32 | { 33 | Package pkg{.id = 1, .type = DATA_PDU, .bytes = 1024, .src_pid = 1, .tgt_pid = 3}; 34 | pkgs.elems[0] = &pkg; 35 | pkgs.number = 1; 36 | 37 | Port port{.id = 3, .type = CPU_PORT, .count_threshold = 1, .bytes_threshold = 1500}; 38 | chip.ports[0] = &port; 39 | chip.port_number = 1; 40 | 41 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 42 | 43 | ASSERT_EQ(1, pkg.id); 44 | ASSERT_EQ(0, pkg.bytes); 45 | ASSERT_EQ(DATA_PDU, pkg.type); 46 | } 47 | 48 | TEST_F(SDK_TEST, should_not_forward_data_pdu_to_cpu_port) 49 | { 50 | Package pkg{.id = 1, .type = DATA_PDU, .bytes = 1024, .src_pid = 1, .tgt_pid = 3}; 51 | pkgs.elems[0] = &pkg; 52 | pkgs.number = 1; 53 | 54 | Port src_port{.id = 1}; 55 | Port tgt_port{.id = 3, .type = CPU_PORT, .count_threshold = 1, .bytes_threshold = 1500}; 56 | chip.ports[0] = &src_port; 57 | chip.ports[1] = &tgt_port; 58 | chip.port_number = 2; 59 | 60 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 61 | 62 | ASSERT_EQ(1, pkg.id); 63 | ASSERT_EQ(0, pkg.bytes); 64 | } 65 | 66 | TEST_F(SDK_TEST, should_forward_data_pdu_to_fwd_port_when_under_bytes_and_count_threshold) 67 | { 68 | Package pkg{.id = 1, .type = DATA_PDU, .bytes = 1024, .src_pid = 1, .tgt_pid = 3}; 69 | pkgs.elems[0] = &pkg; 70 | pkgs.number = 1; 71 | 72 | Port src_port{.id = 1}; 73 | Port tgt_port{.id = 3, .type = FWD_PORT, .count_threshold = 1, .bytes_threshold = 1500}; 74 | chip.ports[0] = &src_port; 75 | chip.ports[1] = &tgt_port; 76 | chip.port_number = 2; 77 | 78 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 79 | 80 | ASSERT_EQ(1, pkg.id); 81 | ASSERT_EQ(1024, pkg.bytes); 82 | ASSERT_EQ(DATA_PDU, pkg.type); 83 | ASSERT_EQ(1, pkg.src_pid); 84 | ASSERT_EQ(3, pkg.tgt_pid); 85 | } 86 | 87 | TEST_F(SDK_TEST, should_forward_data_pdu_to_fwd_port_but_exceed_bytes_threshold) 88 | { 89 | Package pkg{.id = 1, .type = DATA_PDU, .bytes = 1024, .src_pid = 1, .tgt_pid = 3}; 90 | pkgs.elems[0] = &pkg; 91 | pkgs.number = 1; 92 | 93 | Port src_port{.id = 1}; 94 | Port tgt_port{.id = 3, .type = FWD_PORT, .count_threshold = 1, .bytes_threshold = 1000}; 95 | chip.ports[0] = &src_port; 96 | chip.ports[1] = &tgt_port; 97 | chip.port_number = 2; 98 | 99 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 100 | 101 | ASSERT_EQ(1, pkg.id); 102 | ASSERT_EQ(1000, pkg.bytes); 103 | ASSERT_EQ(DATA_PDU, pkg.type); 104 | ASSERT_EQ(1, pkg.src_pid); 105 | ASSERT_EQ(3, pkg.tgt_pid); 106 | } 107 | 108 | TEST_F(SDK_TEST, should_not_forward_data_pdu_to_fwd_port_when_exceed_count_threshold) 109 | { 110 | Package pkg{.id = 1, .type = DATA_PDU, .bytes = 1024, .src_pid = 1, .tgt_pid = 3}; 111 | pkgs.elems[0] = &pkg; 112 | pkgs.number = 1; 113 | 114 | Port src_port{.id = 1}; 115 | Port tgt_port{.id = 3, .type = FWD_PORT, .count_threshold = 0, .bytes_threshold = 1500}; 116 | chip.ports[0] = &src_port; 117 | chip.ports[1] = &tgt_port; 118 | chip.port_number = 2; 119 | 120 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 121 | 122 | ASSERT_EQ(1, pkg.id); 123 | ASSERT_EQ(0, pkg.bytes); 124 | ASSERT_EQ(DATA_PDU, pkg.type); 125 | ASSERT_EQ(1, pkg.src_pid); 126 | ASSERT_EQ(3, pkg.tgt_pid); 127 | } 128 | 129 | TEST_F(SDK_TEST, should_forward_multi_data_pdu_to_fwd_port_which_not_exceed_count_threshold_but_exceed_bytes_threshold) 130 | { 131 | Package pkg1{.id = 1, .type = DATA_PDU, .bytes = 1000, .src_pid = 1, .tgt_pid = 3}; 132 | Package pkg2{.id = 2, .type = DATA_PDU, .bytes = 1000, .src_pid = 1, .tgt_pid = 3}; 133 | Package pkg3{.id = 3, .type = DATA_PDU, .bytes = 1000, .src_pid = 1, .tgt_pid = 3}; 134 | pkgs.elems[0] = &pkg1; 135 | pkgs.elems[1] = &pkg2; 136 | pkgs.elems[2] = &pkg3; 137 | pkgs.number = 3; 138 | 139 | Port src_port{.id = 1}; 140 | Port tgt_port{.id = 3, .type = FWD_PORT, .count_threshold = 3, .bytes_threshold = 2500}; 141 | chip.ports[0] = &src_port; 142 | chip.ports[1] = &tgt_port; 143 | chip.port_number = 2; 144 | 145 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 146 | 147 | ASSERT_EQ(1, pkg1.id); 148 | ASSERT_EQ(1000, pkg1.bytes); 149 | ASSERT_EQ(DATA_PDU, pkg1.type); 150 | ASSERT_EQ(1, pkg1.src_pid); 151 | ASSERT_EQ(3, pkg1.tgt_pid); 152 | 153 | ASSERT_EQ(2, pkg2.id); 154 | ASSERT_EQ(1000, pkg2.bytes); 155 | ASSERT_EQ(DATA_PDU, pkg2.type); 156 | ASSERT_EQ(1, pkg2.src_pid); 157 | ASSERT_EQ(3, pkg2.tgt_pid); 158 | 159 | ASSERT_EQ(3, pkg3.id); 160 | ASSERT_EQ(500, pkg3.bytes); 161 | ASSERT_EQ(DATA_PDU, pkg3.type); 162 | ASSERT_EQ(1, pkg3.src_pid); 163 | ASSERT_EQ(3, pkg3.tgt_pid); 164 | } 165 | 166 | TEST_F(SDK_TEST, should_forward_multi_data_pdu_to_fwd_port_which_not_exceed_bytes_threshold_but_exceed_count_threshold) 167 | { 168 | Package pkg1{.id = 1, .type = DATA_PDU, .bytes = 1000, .src_pid = 1, .tgt_pid = 3}; 169 | Package pkg2{.id = 2, .type = DATA_PDU, .bytes = 1000, .src_pid = 1, .tgt_pid = 3}; 170 | Package pkg3{.id = 3, .type = DATA_PDU, .bytes = 1000, .src_pid = 1, .tgt_pid = 3}; 171 | pkgs.elems[0] = &pkg1; 172 | pkgs.elems[1] = &pkg2; 173 | pkgs.elems[2] = &pkg3; 174 | pkgs.number = 3; 175 | 176 | Port src_port{.id = 1}; 177 | Port tgt_port{.id = 3, .type = FWD_PORT, .count_threshold = 2, .bytes_threshold = 2500}; 178 | chip.ports[0] = &src_port; 179 | chip.ports[1] = &tgt_port; 180 | chip.port_number = 2; 181 | 182 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 183 | 184 | ASSERT_EQ(1, pkg1.id); 185 | ASSERT_EQ(1000, pkg1.bytes); 186 | ASSERT_EQ(DATA_PDU, pkg1.type); 187 | ASSERT_EQ(1, pkg1.src_pid); 188 | ASSERT_EQ(3, pkg1.tgt_pid); 189 | 190 | ASSERT_EQ(2, pkg2.id); 191 | ASSERT_EQ(1000, pkg2.bytes); 192 | ASSERT_EQ(DATA_PDU, pkg2.type); 193 | ASSERT_EQ(1, pkg2.src_pid); 194 | ASSERT_EQ(3, pkg2.tgt_pid); 195 | 196 | ASSERT_EQ(3, pkg3.id); 197 | ASSERT_EQ(0, pkg3.bytes); 198 | ASSERT_EQ(DATA_PDU, pkg3.type); 199 | ASSERT_EQ(1, pkg3.src_pid); 200 | ASSERT_EQ(3, pkg3.tgt_pid); 201 | } 202 | 203 | TEST_F(SDK_TEST, should_not_forward_ctrl_cmd_package_whose_src_port_is_not_exist) 204 | { 205 | Package pkg{.id = 1, .type = CTRL_CMD, .bytes = 1000, .src_pid = 1, .tgt_pid = 3}; 206 | pkgs.elems[0] = &pkg; 207 | pkgs.number = 1; 208 | 209 | Port port{.id = 3, .type = CPU_PORT, .count_threshold = 1, .bytes_threshold = 2000}; 210 | chip.ports[0] = &port; 211 | chip.port_number = 1; 212 | 213 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 214 | 215 | ASSERT_EQ(1, pkg.id); 216 | ASSERT_EQ(0, pkg.bytes); 217 | ASSERT_EQ(CTRL_CMD, pkg.type); 218 | } 219 | 220 | TEST_F(SDK_TEST, should_forward_ctrl_cmd_package_to_cpu_port_when_not_exceed_count_threshold_but_exceed_bytes_threshold) 221 | { 222 | Package pkg{.id = 1, .type = CTRL_CMD, .bytes = 1000, .src_pid = 1, .tgt_pid = 3}; 223 | pkgs.elems[0] = &pkg; 224 | pkgs.number = 1; 225 | 226 | Port src_port{.id = 1}; 227 | Port tgt_port{.id = 3, .type = CPU_PORT, .count_threshold = 1, .bytes_threshold = 0}; 228 | chip.ports[0] = &src_port; 229 | chip.ports[1] = &tgt_port; 230 | chip.port_number = 2; 231 | 232 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 233 | 234 | ASSERT_EQ(1, pkg.id); 235 | ASSERT_EQ(1000, pkg.bytes); 236 | ASSERT_EQ(CTRL_CMD, pkg.type); 237 | ASSERT_EQ(1, pkg.src_pid); 238 | ASSERT_EQ(3, pkg.tgt_pid); 239 | } 240 | 241 | TEST_F(SDK_TEST, should_forward_ctrl_cmd_package_to_fwd_port_when_not_exceed_bytes_threshold_but_exceed_count_threshold) 242 | { 243 | Package pkg{.id = 1, .type = CTRL_CMD, .bytes = 1000, .src_pid = 1, .tgt_pid = 3}; 244 | pkgs.elems[0] = &pkg; 245 | pkgs.number = 1; 246 | 247 | Port src_port{.id = 1}; 248 | Port tgt_port{.id = 3, .type = FWD_PORT, .count_threshold = 0, .bytes_threshold = 400}; 249 | chip.ports[0] = &src_port; 250 | chip.ports[1] = &tgt_port; 251 | chip.port_number = 2; 252 | 253 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 254 | 255 | ASSERT_EQ(1, pkg.id); 256 | ASSERT_EQ(400, pkg.bytes); 257 | ASSERT_EQ(CTRL_CMD, pkg.type); 258 | ASSERT_EQ(1, pkg.src_pid); 259 | ASSERT_EQ(3, pkg.tgt_pid); 260 | } 261 | 262 | TEST_F(SDK_TEST, should_forward_multi_ctrl_cmd_packages_to_cpu_port_and_fwd_port) 263 | { 264 | Package pkg1{.id = 1, .type = CTRL_CMD, .bytes = 1000, .src_pid = 1, .tgt_pid = 2}; 265 | Package pkg2{.id = 2, .type = CTRL_CMD, .bytes = 1000, .src_pid = 2, .tgt_pid = 1}; 266 | Package pkg3{.id = 3, .type = CTRL_CMD, .bytes = 1000, .src_pid = 1, .tgt_pid = 2}; 267 | pkgs.elems[0] = &pkg1; 268 | pkgs.elems[1] = &pkg2; 269 | pkgs.elems[2] = &pkg3; 270 | pkgs.number = 3; 271 | 272 | Port port1{.id = 1, .type = CPU_PORT, .count_threshold = 1, .bytes_threshold = 400}; 273 | Port port2{.id = 2, .type = FWD_PORT, .count_threshold = 0, .bytes_threshold = 1400}; 274 | chip.ports[0] = &port1; 275 | chip.ports[1] = &port2; 276 | chip.port_number = 2; 277 | 278 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 279 | 280 | ASSERT_EQ(1, pkg1.id); 281 | ASSERT_EQ(1000, pkg1.bytes); 282 | ASSERT_EQ(CTRL_CMD, pkg1.type); 283 | ASSERT_EQ(1, pkg1.src_pid); 284 | ASSERT_EQ(2, pkg1.tgt_pid); 285 | 286 | ASSERT_EQ(2, pkg2.id); 287 | ASSERT_EQ(1000, pkg2.bytes); 288 | ASSERT_EQ(CTRL_CMD, pkg2.type); 289 | ASSERT_EQ(2, pkg2.src_pid); 290 | ASSERT_EQ(1, pkg2.tgt_pid); 291 | 292 | ASSERT_EQ(3, pkg3.id); 293 | ASSERT_EQ(400, pkg3.bytes); 294 | ASSERT_EQ(CTRL_CMD, pkg3.type); 295 | ASSERT_EQ(1, pkg3.src_pid); 296 | ASSERT_EQ(2, pkg3.tgt_pid); 297 | } 298 | 299 | TEST_F(SDK_TEST, should_not_forward_hand_shake_package_whose_src_port_is_not_exist) 300 | { 301 | Package pkg{.id = 1, .type = HAND_SHAKE, .bytes = 1024, .src_pid = 1, .tgt_pid = 3}; 302 | pkgs.elems[0] = &pkg; 303 | pkgs.number = 1; 304 | 305 | Port port{.id = 3, .type = CPU_PORT, .count_threshold = 1, .bytes_threshold = 2000}; 306 | chip.ports[0] = &port; 307 | chip.port_number = 1; 308 | 309 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 310 | 311 | ASSERT_EQ(1, pkg.id); 312 | ASSERT_EQ(0, pkg.bytes); 313 | ASSERT_EQ(HAND_SHAKE, pkg.type); 314 | } 315 | 316 | TEST_F(SDK_TEST, should_not_forward_hand_shake_package_to_cpu_port_when_exceed_count_threshold) 317 | { 318 | Package pkg{.id = 1, .type = HAND_SHAKE, .bytes = 1000, .src_pid = 1}; 319 | pkgs.elems[0] = &pkg; 320 | pkgs.number = 1; 321 | 322 | Port port{.id = 1, .type = CPU_PORT, .count_threshold = 0, .bytes_threshold = 2000}; 323 | chip.ports[0] = &port; 324 | chip.port_number = 1; 325 | 326 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 327 | 328 | ASSERT_EQ(1, pkg.id); 329 | ASSERT_EQ(0, pkg.bytes); 330 | ASSERT_EQ(HAND_SHAKE, pkg.type); 331 | } 332 | 333 | TEST_F(SDK_TEST, should_not_forward_hand_shake_package_to_fwd_port_when_exceed_count_threshold) 334 | { 335 | Package pkg{.id = 1, .type = HAND_SHAKE, .bytes = 1000, .src_pid = 1}; 336 | pkgs.elems[0] = &pkg; 337 | pkgs.number = 1; 338 | 339 | Port port{.id = 1, .type = FWD_PORT, .count_threshold = 0, .bytes_threshold = 2000}; 340 | chip.ports[0] = &port; 341 | chip.port_number = 1; 342 | 343 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 344 | 345 | ASSERT_EQ(1, pkg.id); 346 | ASSERT_EQ(0, pkg.bytes); 347 | ASSERT_EQ(HAND_SHAKE, pkg.type); 348 | } 349 | 350 | TEST_F(SDK_TEST, should_forward_whole_hand_shake_package_to_cpu_port_when_not_exceed_count_threshold) 351 | { 352 | Package pkg{.id = 1, .type = HAND_SHAKE, .bytes = 1000, .src_pid = 1}; 353 | pkgs.elems[0] = &pkg; 354 | pkgs.number = 1; 355 | 356 | Port port{.id = 1, .type = CPU_PORT, .count_threshold = 1, .bytes_threshold = 400}; 357 | chip.ports[0] = &port; 358 | chip.port_number = 1; 359 | 360 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 361 | 362 | ASSERT_EQ(1, pkg.id); 363 | ASSERT_EQ(1000, pkg.bytes); 364 | ASSERT_EQ(HAND_SHAKE, pkg.type); 365 | ASSERT_EQ(1, pkg.src_pid); 366 | ASSERT_EQ(1, pkg.tgt_pid); 367 | } 368 | 369 | TEST_F(SDK_TEST, should_forward_whole_hand_shake_package_to_fwd_port_when_not_exceed_count_threshold) 370 | { 371 | Package pkg{.id = 1, .type = HAND_SHAKE, .bytes = 1000, .src_pid = 1}; 372 | pkgs.elems[0] = &pkg; 373 | pkgs.number = 1; 374 | 375 | Port port{.id = 1, .type = FWD_PORT, .count_threshold = 1, .bytes_threshold = 400}; 376 | chip.ports[0] = &port; 377 | chip.port_number = 1; 378 | 379 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 380 | 381 | ASSERT_EQ(1, pkg.id); 382 | ASSERT_EQ(1000, pkg.bytes); 383 | ASSERT_EQ(HAND_SHAKE, pkg.type); 384 | ASSERT_EQ(1, pkg.src_pid); 385 | ASSERT_EQ(1, pkg.tgt_pid); 386 | } 387 | 388 | TEST_F(SDK_TEST, should_forward_all_whole_hand_shake_packages_to_fwd_port_when_exceed_bytes_threshold) 389 | { 390 | Package pkg1{.id = 1, .type = HAND_SHAKE, .bytes = 1000, .src_pid = 1}; 391 | Package pkg2{.id = 2, .type = HAND_SHAKE, .bytes = 1000, .src_pid = 1}; 392 | pkgs.elems[0] = &pkg1; 393 | pkgs.elems[1] = &pkg2; 394 | pkgs.number = 2; 395 | 396 | Port port{.id = 1, .type = FWD_PORT, .count_threshold = 2, .bytes_threshold = 400}; 397 | chip.ports[0] = &port; 398 | chip.port_number = 1; 399 | 400 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 401 | 402 | ASSERT_EQ(1, pkg1.id); 403 | ASSERT_EQ(1000, pkg1.bytes); 404 | ASSERT_EQ(HAND_SHAKE, pkg1.type); 405 | ASSERT_EQ(1, pkg1.src_pid); 406 | ASSERT_EQ(1, pkg1.tgt_pid); 407 | 408 | ASSERT_EQ(2, pkg2.id); 409 | ASSERT_EQ(1000, pkg2.bytes); 410 | ASSERT_EQ(HAND_SHAKE, pkg2.type); 411 | ASSERT_EQ(1, pkg2.src_pid); 412 | ASSERT_EQ(1, pkg2.tgt_pid); 413 | } 414 | 415 | TEST_F(SDK_TEST, should_forward_all_whole_hand_shake_packages_but_other_packages_according_bytes_threshold) 416 | { 417 | Package pkg1{.id = 1, .type = HAND_SHAKE, .bytes = 1000, .src_pid = 3}; 418 | Package pkg2{.id = 2, .type = CTRL_CMD , .bytes = 1000, .src_pid = 1, .tgt_pid = 3}; 419 | Package pkg3{.id = 3, .type = DATA_PDU , .bytes = 1000, .src_pid = 1, .tgt_pid = 3}; 420 | Package pkg4{.id = 4, .type = HAND_SHAKE, .bytes = 1000, .src_pid = 3}; 421 | pkgs.elems[0] = &pkg1; 422 | pkgs.elems[1] = &pkg2; 423 | pkgs.elems[2] = &pkg3; 424 | pkgs.elems[3] = &pkg4; 425 | pkgs.number = 4; 426 | 427 | Port src_port{.id = 1}; 428 | Port tgt_port{.id = 3, .type = FWD_PORT, .count_threshold = 4, .bytes_threshold = 2500}; 429 | chip.ports[0] = &src_port; 430 | chip.ports[1] = &tgt_port; 431 | chip.port_number = 2; 432 | 433 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 434 | 435 | ASSERT_EQ(1, pkg1.id); 436 | ASSERT_EQ(1000, pkg1.bytes); 437 | ASSERT_EQ(HAND_SHAKE, pkg1.type); 438 | ASSERT_EQ(3, pkg1.src_pid); 439 | ASSERT_EQ(3, pkg1.tgt_pid); 440 | 441 | ASSERT_EQ(2, pkg2.id); 442 | ASSERT_EQ(1000, pkg2.bytes); 443 | ASSERT_EQ(CTRL_CMD, pkg2.type); 444 | ASSERT_EQ(1, pkg2.src_pid); 445 | ASSERT_EQ(3, pkg2.tgt_pid); 446 | 447 | ASSERT_EQ(3, pkg3.id); 448 | ASSERT_EQ(500, pkg3.bytes); 449 | ASSERT_EQ(DATA_PDU, pkg3.type); 450 | ASSERT_EQ(1, pkg3.src_pid); 451 | ASSERT_EQ(3, pkg3.tgt_pid); 452 | 453 | ASSERT_EQ(4, pkg4.id); 454 | ASSERT_EQ(1000, pkg4.bytes); 455 | ASSERT_EQ(HAND_SHAKE, pkg4.type); 456 | ASSERT_EQ(3, pkg4.src_pid); 457 | ASSERT_EQ(3, pkg4.tgt_pid); 458 | } 459 | 460 | TEST_F(SDK_TEST, should_forward_multi_packages_to_multi_ports_according_forward_rules) 461 | { 462 | Package pkg1{.id = 1, .type = HAND_SHAKE, .bytes = 1000, .src_pid = 1}; 463 | Package pkg2{.id = 2, .type = CTRL_CMD , .bytes = 1000, .src_pid = 2, .tgt_pid = 1}; 464 | Package pkg3{.id = 3, .type = DATA_PDU , .bytes = 1000, .src_pid = 1, .tgt_pid = 2}; 465 | Package pkg4{.id = 4, .type = HAND_SHAKE, .bytes = 1000, .src_pid = 2}; 466 | Package pkg5{.id = 5, .type = DATA_PDU , .bytes = 1000, .src_pid = 3, .tgt_pid = 2}; 467 | Package pkg6{.id = 6, .type = DATA_PDU , .bytes = 1000, .src_pid = 1, .tgt_pid = 3}; 468 | Package pkg7{.id = 7, .type = CTRL_CMD , .bytes = 2000, .src_pid = 2, .tgt_pid = 3}; 469 | Package pkg8{.id = 8, .type = HAND_SHAKE, .bytes = 2000, .src_pid = 4}; 470 | pkgs.elems[0] = &pkg1; 471 | pkgs.elems[1] = &pkg2; 472 | pkgs.elems[2] = &pkg3; 473 | pkgs.elems[3] = &pkg4; 474 | pkgs.elems[4] = &pkg5; 475 | pkgs.elems[5] = &pkg6; 476 | pkgs.elems[6] = &pkg7; 477 | pkgs.elems[7] = &pkg8; 478 | pkgs.number = 8; 479 | 480 | Port port1{.id = 1, .type = FWD_PORT, .count_threshold = 1, .bytes_threshold = 1500}; 481 | Port port2{.id = 2, .type = FWD_PORT, .count_threshold = 2, .bytes_threshold = 1500}; 482 | Port port3{.id = 3, .type = CPU_PORT, .count_threshold = 1, .bytes_threshold = 1500}; 483 | Port port4{.id = 4, .type = CPU_PORT, .count_threshold = 3, .bytes_threshold = 1500}; 484 | chip.ports[0] = &port1; 485 | chip.ports[1] = &port2; 486 | chip.ports[2] = &port3; 487 | chip.ports[3] = &port4; 488 | chip.port_number = 4; 489 | 490 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 491 | 492 | ASSERT_EQ(1, pkg1.id); 493 | ASSERT_EQ(1000, pkg1.bytes); 494 | ASSERT_EQ(HAND_SHAKE, pkg1.type); 495 | ASSERT_EQ(1, pkg1.src_pid); 496 | ASSERT_EQ(1, pkg1.tgt_pid); 497 | 498 | ASSERT_EQ(2, pkg2.id); 499 | ASSERT_EQ(500, pkg2.bytes); 500 | ASSERT_EQ(CTRL_CMD, pkg2.type); 501 | ASSERT_EQ(2, pkg2.src_pid); 502 | ASSERT_EQ(1, pkg2.tgt_pid); 503 | 504 | ASSERT_EQ(3, pkg3.id); 505 | ASSERT_EQ(1000, pkg3.bytes); 506 | ASSERT_EQ(DATA_PDU, pkg3.type); 507 | ASSERT_EQ(1, pkg3.src_pid); 508 | ASSERT_EQ(2, pkg3.tgt_pid); 509 | 510 | ASSERT_EQ(4, pkg4.id); 511 | ASSERT_EQ(1000, pkg4.bytes); 512 | ASSERT_EQ(HAND_SHAKE, pkg4.type); 513 | ASSERT_EQ(2, pkg4.src_pid); 514 | ASSERT_EQ(2, pkg4.tgt_pid); 515 | 516 | ASSERT_EQ(5, pkg5.id); 517 | ASSERT_EQ(0, pkg5.bytes); 518 | ASSERT_EQ(DATA_PDU, pkg5.type); 519 | ASSERT_EQ(3, pkg5.src_pid); 520 | ASSERT_EQ(2, pkg5.tgt_pid); 521 | 522 | ASSERT_EQ(6, pkg6.id); 523 | ASSERT_EQ(0, pkg6.bytes); 524 | ASSERT_EQ(DATA_PDU, pkg6.type); 525 | ASSERT_EQ(1, pkg6.src_pid); 526 | ASSERT_EQ(3, pkg6.tgt_pid); 527 | 528 | ASSERT_EQ(7, pkg7.id); 529 | ASSERT_EQ(2000, pkg7.bytes); 530 | ASSERT_EQ(CTRL_CMD, pkg7.type); 531 | ASSERT_EQ(2, pkg7.src_pid); 532 | ASSERT_EQ(3, pkg7.tgt_pid); 533 | 534 | ASSERT_EQ(8, pkg8.id); 535 | ASSERT_EQ(2000, pkg8.bytes); 536 | ASSERT_EQ(HAND_SHAKE, pkg8.type); 537 | ASSERT_EQ(4, pkg8.src_pid); 538 | ASSERT_EQ(4, pkg8.tgt_pid); 539 | } -------------------------------------------------------------------------------- /code/c/sdk/.gitignore: -------------------------------------------------------------------------------- 1 | build/* -------------------------------------------------------------------------------- /code/c/sdk/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.4) 2 | 3 | project(sdk) 4 | 5 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-invalid-offsetof -g -std=gnu++11") 6 | 7 | if(MSVC) 8 | add_definitions(-D_CRT_SECURE_NO_WARNINGS) 9 | add_definitions(-DMSVC_VMG_ENABLED) 10 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /vmg") 11 | endif(MSVC) 12 | 13 | include_directories("${CMAKE_CURRENT_SOURCE_DIR}/include") 14 | 15 | add_subdirectory("src") 16 | 17 | if(ENABLE_TEST) 18 | add_subdirectory(test) 19 | endif() 20 | -------------------------------------------------------------------------------- /code/c/sdk/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | target="build" 4 | 5 | if [ ! -d $target ]; then 6 | mkdir -p $target 7 | fi 8 | 9 | cd $target 10 | 11 | echo "*******************************************************************************" 12 | echo "start to build project ..." 13 | 14 | 15 | cmake -DENABLE_TEST=1 .. 16 | cmake --build . 17 | 18 | if [ $? -ne 0 ]; then 19 | echo "FAILED!" 20 | cd .. 21 | exit 1 22 | fi 23 | 24 | echo "*******************************************************************************" 25 | echo "start to run tests..." 26 | 27 | ./test/test_sdk --gtest_color=yes $1 $2 28 | 29 | if [ $? -ne 0 ]; then 30 | echo "FAILED!" 31 | cd .. 32 | exit 1 33 | fi 34 | 35 | cd .. 36 | 37 | echo "*******************************************************************************" 38 | echo "SUCCESS!" 39 | exit 0 -------------------------------------------------------------------------------- /code/c/sdk/include/sdk/fwd_ctrl.h: -------------------------------------------------------------------------------- 1 | #ifndef H6AB49706_ADB6_11E9_9140_A45E60C4B579 2 | #define H6AB49706_ADB6_11E9_9140_A45E60C4B579 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | typedef int Status; 9 | 10 | #define SUCCESS (Status)0 11 | #define FAILURE (Status)-1 12 | 13 | #define NULL_PTR 0 14 | 15 | typedef unsigned int PortId; 16 | typedef unsigned int PackageId; 17 | 18 | typedef enum { 19 | CPU_PORT = 0, 20 | FWD_PORT, 21 | } PortType; 22 | 23 | typedef struct Port { 24 | PortId id; 25 | PortType type; 26 | unsigned int count_threshold; 27 | unsigned int bytes_threshold; 28 | } Port; 29 | 30 | typedef struct Chip { 31 | Port* ports[12]; 32 | unsigned int port_number; 33 | } Chip; 34 | 35 | typedef enum { 36 | DATA_PDU = 0, 37 | CTRL_CMD, 38 | HAND_SHAKE, 39 | } PackageType; 40 | 41 | typedef struct { 42 | PackageId id; 43 | PackageType type; 44 | unsigned int bytes; 45 | PortId src_pid; 46 | PortId tgt_pid; 47 | } Package; 48 | 49 | typedef struct { 50 | Package* elems[12]; 51 | unsigned int number; 52 | } Packages; 53 | 54 | Status forward_ctrl(const Chip* chip, Packages* pkgs); 55 | 56 | #ifdef __cplusplus 57 | } 58 | #endif 59 | 60 | #endif 61 | -------------------------------------------------------------------------------- /code/c/sdk/src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | file(GLOB_RECURSE all_files 2 | *.cpp 3 | *.cc 4 | *.c++ 5 | *.c 6 | *.C) 7 | 8 | add_library(sdk STATIC ${all_files}) 9 | -------------------------------------------------------------------------------- /code/c/sdk/src/fwd_ctrl.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "sdk/fwd_ctrl.h" 3 | 4 | Status forward_ctrl(const Chip* chip, Packages* pkgs) { 5 | unsigned int port_bytes[12] = {0}; 6 | unsigned int port_count[12] = {0}; 7 | int pkg_index = 0; 8 | int port_index = 0; 9 | unsigned int bytes_left = 0; 10 | 11 | if (chip == NULL_PTR || pkgs == NULL_PTR) { 12 | printf("[ERROR]: exist null ptr, chip = %p, pkgs = %p!\n", chip, pkgs); 13 | return FAILURE; 14 | } 15 | 16 | for(pkg_index = 0; pkg_index < pkgs->number && pkg_index < 12; pkg_index++) { 17 | for (port_index = 0; port_index < chip->port_number && port_index < 12; port_index++) { 18 | if (chip->ports[port_index]->id == pkgs->elems[pkg_index]->src_pid) { 19 | break; 20 | } 21 | } 22 | if (port_index >= chip->port_number || port_index >= 12) { 23 | pkgs->elems[pkg_index]->bytes = 0; 24 | printf("[ERROR]: src port %d not found of package %d!\n", pkgs->elems[pkg_index]->src_pid, pkgs->elems[pkg_index]->id); 25 | continue; 26 | } 27 | switch (pkgs->elems[pkg_index]->type) { 28 | case DATA_PDU: 29 | for (port_index = 0; port_index < chip->port_number && port_index < 12; port_index++) { 30 | if (chip->ports[port_index]->id == pkgs->elems[pkg_index]->tgt_pid) { 31 | if (port_count[port_index] < chip->ports[port_index]->count_threshold) { 32 | if (chip->ports[port_index]->type == FWD_PORT) { 33 | if (chip->ports[port_index]->bytes_threshold > port_bytes[port_index]) 34 | { 35 | bytes_left = chip->ports[port_index]->bytes_threshold - port_bytes[port_index]; 36 | if (bytes_left < pkgs->elems[pkg_index]->bytes) { 37 | pkgs->elems[pkg_index]->bytes = bytes_left; 38 | port_bytes[port_index] += pkgs->elems[pkg_index]->bytes; 39 | } 40 | port_bytes[port_index] += pkgs->elems[pkg_index]->bytes; 41 | port_count[port_index]++; 42 | } else { 43 | pkgs->elems[pkg_index]->bytes = 0; 44 | printf("[WARN]: package %d\n meets the bytes threshold of target port %d!\n", pkgs->elems[pkg_index]->id, pkgs->elems[pkg_index]->tgt_pid); 45 | } 46 | } else { 47 | pkgs->elems[pkg_index]->bytes = 0; 48 | printf("[WARN]: cpu port %d does not receive data pdu package %d!\n", pkgs->elems[pkg_index]->tgt_pid, pkgs->elems[pkg_index]->id); 49 | } 50 | } else { 51 | pkgs->elems[pkg_index]->bytes = 0; 52 | printf("[WARN]: package %d meets the count threshold of target port %d!\n", pkgs->elems[pkg_index]->id, pkgs->elems[pkg_index]->tgt_pid); 53 | } 54 | break; 55 | } 56 | } 57 | if (port_index >= chip->port_number || port_index >= 12) { 58 | pkgs->elems[pkg_index]->bytes = 0; 59 | printf("[ERROR]: target port %d not found of package %d!\n", pkgs->elems[pkg_index]->tgt_pid, pkgs->elems[pkg_index]->id); 60 | } 61 | break; 62 | case CTRL_CMD: 63 | for (port_index = 0; port_index < chip->port_number && port_index < 12; port_index++) { 64 | if (chip->ports[port_index]->id == pkgs->elems[pkg_index]->tgt_pid) { 65 | if (chip->ports[port_index]->type == CPU_PORT) { 66 | if (port_count[port_index] < chip->ports[port_index]->count_threshold) { 67 | port_count[port_index]++; 68 | 69 | } else { 70 | pkgs->elems[pkg_index]->bytes = 0; 71 | printf("[WARN]: package %d meets the count threshold of target cpu port %d!\n", pkgs->elems[pkg_index]->id, pkgs->elems[pkg_index]->tgt_pid); 72 | } 73 | } else { 74 | if (chip->ports[port_index]->bytes_threshold > port_bytes[port_index]) 75 | { 76 | bytes_left = chip->ports[port_index]->bytes_threshold - port_bytes[port_index]; 77 | if (bytes_left < pkgs->elems[pkg_index]->bytes) { 78 | pkgs->elems[pkg_index]->bytes = bytes_left; 79 | port_bytes[port_index] += pkgs->elems[pkg_index]->bytes; 80 | } 81 | port_bytes[port_index] += pkgs->elems[pkg_index]->bytes; 82 | port_count[port_index]++; 83 | } else { 84 | pkgs->elems[pkg_index]->bytes = 0; 85 | printf("[WARN]: package %d meets the bytes threshold of target fwd port %d!\n", pkgs->elems[pkg_index]->id, pkgs->elems[pkg_index]->tgt_pid); 86 | } 87 | } 88 | break; 89 | } 90 | } 91 | if (port_index >= chip->port_number || port_index >= 12) { 92 | pkgs->elems[pkg_index]->bytes = 0; 93 | printf("[ERROR]: target port %d not found of package %d!\n", pkgs->elems[pkg_index]->tgt_pid, pkgs->elems[pkg_index]->id); 94 | } 95 | break; 96 | case HAND_SHAKE: 97 | for (port_index = 0; port_index < chip->port_number && port_index < 12; port_index++) { 98 | if (chip->ports[port_index]->id == pkgs->elems[pkg_index]->src_pid) { 99 | if (port_count[port_index] < chip->ports[port_index]->count_threshold) { 100 | if (chip->ports[port_index]->type == CPU_PORT) { 101 | pkgs->elems[pkg_index]->tgt_pid = pkgs->elems[pkg_index]->src_pid; 102 | port_count[port_index]++; 103 | } else { 104 | pkgs->elems[pkg_index]->tgt_pid = pkgs->elems[pkg_index]->src_pid; 105 | port_bytes[port_index] += pkgs->elems[pkg_index]->bytes; 106 | port_count[port_index]++; 107 | } 108 | } else { 109 | pkgs->elems[pkg_index]->bytes = 0; 110 | printf("[WARN]: package %d meets the count threshold of target fwd port %d!\n", pkgs->elems[pkg_index]->id, pkgs->elems[pkg_index]->tgt_pid); 111 | } 112 | break; 113 | } 114 | } 115 | if (port_index >= chip->port_number || port_index >= 12) { 116 | pkgs->elems[pkg_index]->bytes = 0; 117 | printf("[ERROR]: target port %d not found of package %d!\n", pkgs->elems[pkg_index]->tgt_pid, pkgs->elems[pkg_index]->id); 118 | } 119 | break; 120 | default: 121 | break; 122 | } 123 | } 124 | return SUCCESS; 125 | } 126 | -------------------------------------------------------------------------------- /code/c/sdk/test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(test_sdk) 2 | 3 | include_directories("${CMAKE_CURRENT_SOURCE_DIR}" 4 | "/usr/local/include/") 5 | 6 | file(GLOB_RECURSE all_files 7 | *.cpp 8 | *.cc 9 | *.c++ 10 | *.c 11 | *.C) 12 | 13 | link_directories("/usr/local/lib") 14 | 15 | add_executable(test_sdk ${all_files}) 16 | 17 | target_link_libraries(test_sdk sdk gtest gtest_main) 18 | -------------------------------------------------------------------------------- /code/c/sdk/test/test_chip.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "sdk/fwd_ctrl.h" 3 | 4 | struct SDK_TEST : testing::Test 5 | { 6 | protected: 7 | Chip chip{.port_number = 0}; 8 | Packages pkgs{.number = 0}; 9 | }; 10 | 11 | TEST_F(SDK_TEST, should_fail_when_none_chip_or_none_package) 12 | { 13 | ASSERT_EQ(FAILURE, forward_ctrl(NULL_PTR, NULL_PTR)); 14 | ASSERT_EQ(FAILURE, forward_ctrl(NULL_PTR, &pkgs)); 15 | ASSERT_EQ(FAILURE, forward_ctrl(&chip, NULL_PTR)); 16 | } 17 | 18 | TEST_F(SDK_TEST, should_not_forward_data_pdu_to_none_port) 19 | { 20 | Package pkg{.id = 1, .type = DATA_PDU, .bytes = 1024}; 21 | pkgs.elems[0] = &pkg; 22 | pkgs.number = 1; 23 | 24 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 25 | 26 | ASSERT_EQ(1, pkg.id); 27 | ASSERT_EQ(0, pkg.bytes); 28 | ASSERT_EQ(DATA_PDU, pkg.type); 29 | } 30 | 31 | TEST_F(SDK_TEST, should_not_forward_data_pdu_whose_src_port_is_not_exist) 32 | { 33 | Package pkg{.id = 1, .type = DATA_PDU, .bytes = 1024, .src_pid = 1, .tgt_pid = 3}; 34 | pkgs.elems[0] = &pkg; 35 | pkgs.number = 1; 36 | 37 | Port port{.id = 3, .type = CPU_PORT, .count_threshold = 1, .bytes_threshold = 1500}; 38 | chip.ports[0] = &port; 39 | chip.port_number = 1; 40 | 41 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 42 | 43 | ASSERT_EQ(1, pkg.id); 44 | ASSERT_EQ(0, pkg.bytes); 45 | ASSERT_EQ(DATA_PDU, pkg.type); 46 | } 47 | 48 | TEST_F(SDK_TEST, should_not_forward_data_pdu_to_cpu_port) 49 | { 50 | Package pkg{.id = 1, .type = DATA_PDU, .bytes = 1024, .src_pid = 1, .tgt_pid = 3}; 51 | pkgs.elems[0] = &pkg; 52 | pkgs.number = 1; 53 | 54 | Port src_port{.id = 1}; 55 | Port tgt_port{.id = 3, .type = CPU_PORT, .count_threshold = 1, .bytes_threshold = 1500}; 56 | chip.ports[0] = &src_port; 57 | chip.ports[1] = &tgt_port; 58 | chip.port_number = 2; 59 | 60 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 61 | 62 | ASSERT_EQ(1, pkg.id); 63 | ASSERT_EQ(0, pkg.bytes); 64 | } 65 | 66 | TEST_F(SDK_TEST, should_forward_data_pdu_to_fwd_port_when_under_bytes_and_count_threshold) 67 | { 68 | Package pkg{.id = 1, .type = DATA_PDU, .bytes = 1024, .src_pid = 1, .tgt_pid = 3}; 69 | pkgs.elems[0] = &pkg; 70 | pkgs.number = 1; 71 | 72 | Port src_port{.id = 1}; 73 | Port tgt_port{.id = 3, .type = FWD_PORT, .count_threshold = 1, .bytes_threshold = 1500}; 74 | chip.ports[0] = &src_port; 75 | chip.ports[1] = &tgt_port; 76 | chip.port_number = 2; 77 | 78 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 79 | 80 | ASSERT_EQ(1, pkg.id); 81 | ASSERT_EQ(1024, pkg.bytes); 82 | ASSERT_EQ(DATA_PDU, pkg.type); 83 | ASSERT_EQ(1, pkg.src_pid); 84 | ASSERT_EQ(3, pkg.tgt_pid); 85 | } 86 | 87 | TEST_F(SDK_TEST, should_forward_data_pdu_to_fwd_port_but_exceed_bytes_threshold) 88 | { 89 | Package pkg{.id = 1, .type = DATA_PDU, .bytes = 1024, .src_pid = 1, .tgt_pid = 3}; 90 | pkgs.elems[0] = &pkg; 91 | pkgs.number = 1; 92 | 93 | Port src_port{.id = 1}; 94 | Port tgt_port{.id = 3, .type = FWD_PORT, .count_threshold = 1, .bytes_threshold = 1000}; 95 | chip.ports[0] = &src_port; 96 | chip.ports[1] = &tgt_port; 97 | chip.port_number = 2; 98 | 99 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 100 | 101 | ASSERT_EQ(1, pkg.id); 102 | ASSERT_EQ(1000, pkg.bytes); 103 | ASSERT_EQ(DATA_PDU, pkg.type); 104 | ASSERT_EQ(1, pkg.src_pid); 105 | ASSERT_EQ(3, pkg.tgt_pid); 106 | } 107 | 108 | TEST_F(SDK_TEST, should_not_forward_data_pdu_to_fwd_port_when_exceed_count_threshold) 109 | { 110 | Package pkg{.id = 1, .type = DATA_PDU, .bytes = 1024, .src_pid = 1, .tgt_pid = 3}; 111 | pkgs.elems[0] = &pkg; 112 | pkgs.number = 1; 113 | 114 | Port src_port{.id = 1}; 115 | Port tgt_port{.id = 3, .type = FWD_PORT, .count_threshold = 0, .bytes_threshold = 1500}; 116 | chip.ports[0] = &src_port; 117 | chip.ports[1] = &tgt_port; 118 | chip.port_number = 2; 119 | 120 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 121 | 122 | ASSERT_EQ(1, pkg.id); 123 | ASSERT_EQ(0, pkg.bytes); 124 | ASSERT_EQ(DATA_PDU, pkg.type); 125 | ASSERT_EQ(1, pkg.src_pid); 126 | ASSERT_EQ(3, pkg.tgt_pid); 127 | } 128 | 129 | TEST_F(SDK_TEST, should_forward_multi_data_pdu_to_fwd_port_which_not_exceed_count_threshold_but_exceed_bytes_threshold) 130 | { 131 | Package pkg1{.id = 1, .type = DATA_PDU, .bytes = 1000, .src_pid = 1, .tgt_pid = 3}; 132 | Package pkg2{.id = 2, .type = DATA_PDU, .bytes = 1000, .src_pid = 1, .tgt_pid = 3}; 133 | Package pkg3{.id = 3, .type = DATA_PDU, .bytes = 1000, .src_pid = 1, .tgt_pid = 3}; 134 | pkgs.elems[0] = &pkg1; 135 | pkgs.elems[1] = &pkg2; 136 | pkgs.elems[2] = &pkg3; 137 | pkgs.number = 3; 138 | 139 | Port src_port{.id = 1}; 140 | Port tgt_port{.id = 3, .type = FWD_PORT, .count_threshold = 3, .bytes_threshold = 2500}; 141 | chip.ports[0] = &src_port; 142 | chip.ports[1] = &tgt_port; 143 | chip.port_number = 2; 144 | 145 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 146 | 147 | ASSERT_EQ(1, pkg1.id); 148 | ASSERT_EQ(1000, pkg1.bytes); 149 | ASSERT_EQ(DATA_PDU, pkg1.type); 150 | ASSERT_EQ(1, pkg1.src_pid); 151 | ASSERT_EQ(3, pkg1.tgt_pid); 152 | 153 | ASSERT_EQ(2, pkg2.id); 154 | ASSERT_EQ(1000, pkg2.bytes); 155 | ASSERT_EQ(DATA_PDU, pkg2.type); 156 | ASSERT_EQ(1, pkg2.src_pid); 157 | ASSERT_EQ(3, pkg2.tgt_pid); 158 | 159 | ASSERT_EQ(3, pkg3.id); 160 | ASSERT_EQ(500, pkg3.bytes); 161 | ASSERT_EQ(DATA_PDU, pkg3.type); 162 | ASSERT_EQ(1, pkg3.src_pid); 163 | ASSERT_EQ(3, pkg3.tgt_pid); 164 | } 165 | 166 | TEST_F(SDK_TEST, should_forward_multi_data_pdu_to_fwd_port_which_not_exceed_bytes_threshold_but_exceed_count_threshold) 167 | { 168 | Package pkg1{.id = 1, .type = DATA_PDU, .bytes = 1000, .src_pid = 1, .tgt_pid = 3}; 169 | Package pkg2{.id = 2, .type = DATA_PDU, .bytes = 1000, .src_pid = 1, .tgt_pid = 3}; 170 | Package pkg3{.id = 3, .type = DATA_PDU, .bytes = 1000, .src_pid = 1, .tgt_pid = 3}; 171 | pkgs.elems[0] = &pkg1; 172 | pkgs.elems[1] = &pkg2; 173 | pkgs.elems[2] = &pkg3; 174 | pkgs.number = 3; 175 | 176 | Port src_port{.id = 1}; 177 | Port tgt_port{.id = 3, .type = FWD_PORT, .count_threshold = 2, .bytes_threshold = 2500}; 178 | chip.ports[0] = &src_port; 179 | chip.ports[1] = &tgt_port; 180 | chip.port_number = 2; 181 | 182 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 183 | 184 | ASSERT_EQ(1, pkg1.id); 185 | ASSERT_EQ(1000, pkg1.bytes); 186 | ASSERT_EQ(DATA_PDU, pkg1.type); 187 | ASSERT_EQ(1, pkg1.src_pid); 188 | ASSERT_EQ(3, pkg1.tgt_pid); 189 | 190 | ASSERT_EQ(2, pkg2.id); 191 | ASSERT_EQ(1000, pkg2.bytes); 192 | ASSERT_EQ(DATA_PDU, pkg2.type); 193 | ASSERT_EQ(1, pkg2.src_pid); 194 | ASSERT_EQ(3, pkg2.tgt_pid); 195 | 196 | ASSERT_EQ(3, pkg3.id); 197 | ASSERT_EQ(0, pkg3.bytes); 198 | ASSERT_EQ(DATA_PDU, pkg3.type); 199 | ASSERT_EQ(1, pkg3.src_pid); 200 | ASSERT_EQ(3, pkg3.tgt_pid); 201 | } 202 | 203 | TEST_F(SDK_TEST, should_not_forward_ctrl_cmd_package_whose_src_port_is_not_exist) 204 | { 205 | Package pkg{.id = 1, .type = CTRL_CMD, .bytes = 1000, .src_pid = 1, .tgt_pid = 3}; 206 | pkgs.elems[0] = &pkg; 207 | pkgs.number = 1; 208 | 209 | Port port{.id = 3, .type = CPU_PORT, .count_threshold = 1, .bytes_threshold = 2000}; 210 | chip.ports[0] = &port; 211 | chip.port_number = 1; 212 | 213 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 214 | 215 | ASSERT_EQ(1, pkg.id); 216 | ASSERT_EQ(0, pkg.bytes); 217 | ASSERT_EQ(CTRL_CMD, pkg.type); 218 | } 219 | 220 | TEST_F(SDK_TEST, should_forward_ctrl_cmd_package_to_cpu_port_when_not_exceed_count_threshold_but_exceed_bytes_threshold) 221 | { 222 | Package pkg{.id = 1, .type = CTRL_CMD, .bytes = 1000, .src_pid = 1, .tgt_pid = 3}; 223 | pkgs.elems[0] = &pkg; 224 | pkgs.number = 1; 225 | 226 | Port src_port{.id = 1}; 227 | Port tgt_port{.id = 3, .type = CPU_PORT, .count_threshold = 1, .bytes_threshold = 0}; 228 | chip.ports[0] = &src_port; 229 | chip.ports[1] = &tgt_port; 230 | chip.port_number = 2; 231 | 232 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 233 | 234 | ASSERT_EQ(1, pkg.id); 235 | ASSERT_EQ(1000, pkg.bytes); 236 | ASSERT_EQ(CTRL_CMD, pkg.type); 237 | ASSERT_EQ(1, pkg.src_pid); 238 | ASSERT_EQ(3, pkg.tgt_pid); 239 | } 240 | 241 | TEST_F(SDK_TEST, should_forward_ctrl_cmd_package_to_fwd_port_when_not_exceed_bytes_threshold_but_exceed_count_threshold) 242 | { 243 | Package pkg{.id = 1, .type = CTRL_CMD, .bytes = 1000, .src_pid = 1, .tgt_pid = 3}; 244 | pkgs.elems[0] = &pkg; 245 | pkgs.number = 1; 246 | 247 | Port src_port{.id = 1}; 248 | Port tgt_port{.id = 3, .type = FWD_PORT, .count_threshold = 0, .bytes_threshold = 400}; 249 | chip.ports[0] = &src_port; 250 | chip.ports[1] = &tgt_port; 251 | chip.port_number = 2; 252 | 253 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 254 | 255 | ASSERT_EQ(1, pkg.id); 256 | ASSERT_EQ(400, pkg.bytes); 257 | ASSERT_EQ(CTRL_CMD, pkg.type); 258 | ASSERT_EQ(1, pkg.src_pid); 259 | ASSERT_EQ(3, pkg.tgt_pid); 260 | } 261 | 262 | TEST_F(SDK_TEST, should_forward_multi_ctrl_cmd_packages_to_cpu_port_and_fwd_port) 263 | { 264 | Package pkg1{.id = 1, .type = CTRL_CMD, .bytes = 1000, .src_pid = 1, .tgt_pid = 2}; 265 | Package pkg2{.id = 2, .type = CTRL_CMD, .bytes = 1000, .src_pid = 2, .tgt_pid = 1}; 266 | Package pkg3{.id = 3, .type = CTRL_CMD, .bytes = 1000, .src_pid = 1, .tgt_pid = 2}; 267 | pkgs.elems[0] = &pkg1; 268 | pkgs.elems[1] = &pkg2; 269 | pkgs.elems[2] = &pkg3; 270 | pkgs.number = 3; 271 | 272 | Port port1{.id = 1, .type = CPU_PORT, .count_threshold = 1, .bytes_threshold = 400}; 273 | Port port2{.id = 2, .type = FWD_PORT, .count_threshold = 0, .bytes_threshold = 1400}; 274 | chip.ports[0] = &port1; 275 | chip.ports[1] = &port2; 276 | chip.port_number = 2; 277 | 278 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 279 | 280 | ASSERT_EQ(1, pkg1.id); 281 | ASSERT_EQ(1000, pkg1.bytes); 282 | ASSERT_EQ(CTRL_CMD, pkg1.type); 283 | ASSERT_EQ(1, pkg1.src_pid); 284 | ASSERT_EQ(2, pkg1.tgt_pid); 285 | 286 | ASSERT_EQ(2, pkg2.id); 287 | ASSERT_EQ(1000, pkg2.bytes); 288 | ASSERT_EQ(CTRL_CMD, pkg2.type); 289 | ASSERT_EQ(2, pkg2.src_pid); 290 | ASSERT_EQ(1, pkg2.tgt_pid); 291 | 292 | ASSERT_EQ(3, pkg3.id); 293 | ASSERT_EQ(400, pkg3.bytes); 294 | ASSERT_EQ(CTRL_CMD, pkg3.type); 295 | ASSERT_EQ(1, pkg3.src_pid); 296 | ASSERT_EQ(2, pkg3.tgt_pid); 297 | } 298 | 299 | TEST_F(SDK_TEST, should_not_forward_hand_shake_package_whose_src_port_is_not_exist) 300 | { 301 | Package pkg{.id = 1, .type = HAND_SHAKE, .bytes = 1024, .src_pid = 1, .tgt_pid = 3}; 302 | pkgs.elems[0] = &pkg; 303 | pkgs.number = 1; 304 | 305 | Port port{.id = 3, .type = CPU_PORT, .count_threshold = 1, .bytes_threshold = 2000}; 306 | chip.ports[0] = &port; 307 | chip.port_number = 1; 308 | 309 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 310 | 311 | ASSERT_EQ(1, pkg.id); 312 | ASSERT_EQ(0, pkg.bytes); 313 | ASSERT_EQ(HAND_SHAKE, pkg.type); 314 | } 315 | 316 | TEST_F(SDK_TEST, should_not_forward_hand_shake_package_to_cpu_port_when_exceed_count_threshold) 317 | { 318 | Package pkg{.id = 1, .type = HAND_SHAKE, .bytes = 1000, .src_pid = 1}; 319 | pkgs.elems[0] = &pkg; 320 | pkgs.number = 1; 321 | 322 | Port port{.id = 1, .type = CPU_PORT, .count_threshold = 0, .bytes_threshold = 2000}; 323 | chip.ports[0] = &port; 324 | chip.port_number = 1; 325 | 326 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 327 | 328 | ASSERT_EQ(1, pkg.id); 329 | ASSERT_EQ(0, pkg.bytes); 330 | ASSERT_EQ(HAND_SHAKE, pkg.type); 331 | } 332 | 333 | TEST_F(SDK_TEST, should_not_forward_hand_shake_package_to_fwd_port_when_exceed_count_threshold) 334 | { 335 | Package pkg{.id = 1, .type = HAND_SHAKE, .bytes = 1000, .src_pid = 1}; 336 | pkgs.elems[0] = &pkg; 337 | pkgs.number = 1; 338 | 339 | Port port{.id = 1, .type = FWD_PORT, .count_threshold = 0, .bytes_threshold = 2000}; 340 | chip.ports[0] = &port; 341 | chip.port_number = 1; 342 | 343 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 344 | 345 | ASSERT_EQ(1, pkg.id); 346 | ASSERT_EQ(0, pkg.bytes); 347 | ASSERT_EQ(HAND_SHAKE, pkg.type); 348 | } 349 | 350 | TEST_F(SDK_TEST, should_forward_whole_hand_shake_package_to_cpu_port_when_not_exceed_count_threshold) 351 | { 352 | Package pkg{.id = 1, .type = HAND_SHAKE, .bytes = 1000, .src_pid = 1}; 353 | pkgs.elems[0] = &pkg; 354 | pkgs.number = 1; 355 | 356 | Port port{.id = 1, .type = CPU_PORT, .count_threshold = 1, .bytes_threshold = 400}; 357 | chip.ports[0] = &port; 358 | chip.port_number = 1; 359 | 360 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 361 | 362 | ASSERT_EQ(1, pkg.id); 363 | ASSERT_EQ(1000, pkg.bytes); 364 | ASSERT_EQ(HAND_SHAKE, pkg.type); 365 | ASSERT_EQ(1, pkg.src_pid); 366 | ASSERT_EQ(1, pkg.tgt_pid); 367 | } 368 | 369 | TEST_F(SDK_TEST, should_forward_whole_hand_shake_package_to_fwd_port_when_not_exceed_count_threshold) 370 | { 371 | Package pkg{.id = 1, .type = HAND_SHAKE, .bytes = 1000, .src_pid = 1}; 372 | pkgs.elems[0] = &pkg; 373 | pkgs.number = 1; 374 | 375 | Port port{.id = 1, .type = FWD_PORT, .count_threshold = 1, .bytes_threshold = 400}; 376 | chip.ports[0] = &port; 377 | chip.port_number = 1; 378 | 379 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 380 | 381 | ASSERT_EQ(1, pkg.id); 382 | ASSERT_EQ(1000, pkg.bytes); 383 | ASSERT_EQ(HAND_SHAKE, pkg.type); 384 | ASSERT_EQ(1, pkg.src_pid); 385 | ASSERT_EQ(1, pkg.tgt_pid); 386 | } 387 | 388 | TEST_F(SDK_TEST, should_forward_all_whole_hand_shake_packages_to_fwd_port_when_exceed_bytes_threshold) 389 | { 390 | Package pkg1{.id = 1, .type = HAND_SHAKE, .bytes = 1000, .src_pid = 1}; 391 | Package pkg2{.id = 2, .type = HAND_SHAKE, .bytes = 1000, .src_pid = 1}; 392 | pkgs.elems[0] = &pkg1; 393 | pkgs.elems[1] = &pkg2; 394 | pkgs.number = 2; 395 | 396 | Port port{.id = 1, .type = FWD_PORT, .count_threshold = 2, .bytes_threshold = 400}; 397 | chip.ports[0] = &port; 398 | chip.port_number = 1; 399 | 400 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 401 | 402 | ASSERT_EQ(1, pkg1.id); 403 | ASSERT_EQ(1000, pkg1.bytes); 404 | ASSERT_EQ(HAND_SHAKE, pkg1.type); 405 | ASSERT_EQ(1, pkg1.src_pid); 406 | ASSERT_EQ(1, pkg1.tgt_pid); 407 | 408 | ASSERT_EQ(2, pkg2.id); 409 | ASSERT_EQ(1000, pkg2.bytes); 410 | ASSERT_EQ(HAND_SHAKE, pkg2.type); 411 | ASSERT_EQ(1, pkg2.src_pid); 412 | ASSERT_EQ(1, pkg2.tgt_pid); 413 | } 414 | 415 | TEST_F(SDK_TEST, should_forward_all_whole_hand_shake_packages_but_other_packages_according_bytes_threshold) 416 | { 417 | Package pkg1{.id = 1, .type = HAND_SHAKE, .bytes = 1000, .src_pid = 3}; 418 | Package pkg2{.id = 2, .type = CTRL_CMD , .bytes = 1000, .src_pid = 1, .tgt_pid = 3}; 419 | Package pkg3{.id = 3, .type = DATA_PDU , .bytes = 1000, .src_pid = 1, .tgt_pid = 3}; 420 | Package pkg4{.id = 4, .type = HAND_SHAKE, .bytes = 1000, .src_pid = 3}; 421 | pkgs.elems[0] = &pkg1; 422 | pkgs.elems[1] = &pkg2; 423 | pkgs.elems[2] = &pkg3; 424 | pkgs.elems[3] = &pkg4; 425 | pkgs.number = 4; 426 | 427 | Port src_port{.id = 1}; 428 | Port tgt_port{.id = 3, .type = FWD_PORT, .count_threshold = 4, .bytes_threshold = 2500}; 429 | chip.ports[0] = &src_port; 430 | chip.ports[1] = &tgt_port; 431 | chip.port_number = 2; 432 | 433 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 434 | 435 | ASSERT_EQ(1, pkg1.id); 436 | ASSERT_EQ(1000, pkg1.bytes); 437 | ASSERT_EQ(HAND_SHAKE, pkg1.type); 438 | ASSERT_EQ(3, pkg1.src_pid); 439 | ASSERT_EQ(3, pkg1.tgt_pid); 440 | 441 | ASSERT_EQ(2, pkg2.id); 442 | ASSERT_EQ(1000, pkg2.bytes); 443 | ASSERT_EQ(CTRL_CMD, pkg2.type); 444 | ASSERT_EQ(1, pkg2.src_pid); 445 | ASSERT_EQ(3, pkg2.tgt_pid); 446 | 447 | ASSERT_EQ(3, pkg3.id); 448 | ASSERT_EQ(500, pkg3.bytes); 449 | ASSERT_EQ(DATA_PDU, pkg3.type); 450 | ASSERT_EQ(1, pkg3.src_pid); 451 | ASSERT_EQ(3, pkg3.tgt_pid); 452 | 453 | ASSERT_EQ(4, pkg4.id); 454 | ASSERT_EQ(1000, pkg4.bytes); 455 | ASSERT_EQ(HAND_SHAKE, pkg4.type); 456 | ASSERT_EQ(3, pkg4.src_pid); 457 | ASSERT_EQ(3, pkg4.tgt_pid); 458 | } 459 | 460 | TEST_F(SDK_TEST, should_forward_multi_packages_to_multi_ports_according_forward_rules) 461 | { 462 | Package pkg1{.id = 1, .type = HAND_SHAKE, .bytes = 1000, .src_pid = 1}; 463 | Package pkg2{.id = 2, .type = CTRL_CMD , .bytes = 1000, .src_pid = 2, .tgt_pid = 1}; 464 | Package pkg3{.id = 3, .type = DATA_PDU , .bytes = 1000, .src_pid = 1, .tgt_pid = 2}; 465 | Package pkg4{.id = 4, .type = HAND_SHAKE, .bytes = 1000, .src_pid = 2}; 466 | Package pkg5{.id = 5, .type = DATA_PDU , .bytes = 1000, .src_pid = 3, .tgt_pid = 2}; 467 | Package pkg6{.id = 6, .type = DATA_PDU , .bytes = 1000, .src_pid = 1, .tgt_pid = 3}; 468 | Package pkg7{.id = 7, .type = CTRL_CMD , .bytes = 2000, .src_pid = 2, .tgt_pid = 3}; 469 | Package pkg8{.id = 8, .type = HAND_SHAKE, .bytes = 2000, .src_pid = 4}; 470 | pkgs.elems[0] = &pkg1; 471 | pkgs.elems[1] = &pkg2; 472 | pkgs.elems[2] = &pkg3; 473 | pkgs.elems[3] = &pkg4; 474 | pkgs.elems[4] = &pkg5; 475 | pkgs.elems[5] = &pkg6; 476 | pkgs.elems[6] = &pkg7; 477 | pkgs.elems[7] = &pkg8; 478 | pkgs.number = 8; 479 | 480 | Port port1{.id = 1, .type = FWD_PORT, .count_threshold = 1, .bytes_threshold = 1500}; 481 | Port port2{.id = 2, .type = FWD_PORT, .count_threshold = 2, .bytes_threshold = 1500}; 482 | Port port3{.id = 3, .type = CPU_PORT, .count_threshold = 1, .bytes_threshold = 1500}; 483 | Port port4{.id = 4, .type = CPU_PORT, .count_threshold = 3, .bytes_threshold = 1500}; 484 | chip.ports[0] = &port1; 485 | chip.ports[1] = &port2; 486 | chip.ports[2] = &port3; 487 | chip.ports[3] = &port4; 488 | chip.port_number = 4; 489 | 490 | ASSERT_EQ(SUCCESS, forward_ctrl(&chip, &pkgs)); 491 | 492 | ASSERT_EQ(1, pkg1.id); 493 | ASSERT_EQ(1000, pkg1.bytes); 494 | ASSERT_EQ(HAND_SHAKE, pkg1.type); 495 | ASSERT_EQ(1, pkg1.src_pid); 496 | ASSERT_EQ(1, pkg1.tgt_pid); 497 | 498 | ASSERT_EQ(2, pkg2.id); 499 | ASSERT_EQ(500, pkg2.bytes); 500 | ASSERT_EQ(CTRL_CMD, pkg2.type); 501 | ASSERT_EQ(2, pkg2.src_pid); 502 | ASSERT_EQ(1, pkg2.tgt_pid); 503 | 504 | ASSERT_EQ(3, pkg3.id); 505 | ASSERT_EQ(1000, pkg3.bytes); 506 | ASSERT_EQ(DATA_PDU, pkg3.type); 507 | ASSERT_EQ(1, pkg3.src_pid); 508 | ASSERT_EQ(2, pkg3.tgt_pid); 509 | 510 | ASSERT_EQ(4, pkg4.id); 511 | ASSERT_EQ(1000, pkg4.bytes); 512 | ASSERT_EQ(HAND_SHAKE, pkg4.type); 513 | ASSERT_EQ(2, pkg4.src_pid); 514 | ASSERT_EQ(2, pkg4.tgt_pid); 515 | 516 | ASSERT_EQ(5, pkg5.id); 517 | ASSERT_EQ(0, pkg5.bytes); 518 | ASSERT_EQ(DATA_PDU, pkg5.type); 519 | ASSERT_EQ(3, pkg5.src_pid); 520 | ASSERT_EQ(2, pkg5.tgt_pid); 521 | 522 | ASSERT_EQ(6, pkg6.id); 523 | ASSERT_EQ(0, pkg6.bytes); 524 | ASSERT_EQ(DATA_PDU, pkg6.type); 525 | ASSERT_EQ(1, pkg6.src_pid); 526 | ASSERT_EQ(3, pkg6.tgt_pid); 527 | 528 | ASSERT_EQ(7, pkg7.id); 529 | ASSERT_EQ(2000, pkg7.bytes); 530 | ASSERT_EQ(CTRL_CMD, pkg7.type); 531 | ASSERT_EQ(2, pkg7.src_pid); 532 | ASSERT_EQ(3, pkg7.tgt_pid); 533 | 534 | ASSERT_EQ(8, pkg8.id); 535 | ASSERT_EQ(2000, pkg8.bytes); 536 | ASSERT_EQ(HAND_SHAKE, pkg8.type); 537 | ASSERT_EQ(4, pkg8.src_pid); 538 | ASSERT_EQ(4, pkg8.tgt_pid); 539 | } -------------------------------------------------------------------------------- /code/cpp/original/Customer.cpp: -------------------------------------------------------------------------------- 1 | #include "Customer.h" 2 | #include "Movie.h" 3 | #include 4 | 5 | using std::stringstream; 6 | 7 | ///////////////////////////////////////////////////////////// 8 | Customer::Customer(const string& name) : name(name) 9 | { 10 | } 11 | 12 | ///////////////////////////////////////////////////////////// 13 | string Customer::getName() const 14 | { 15 | return name; 16 | } 17 | 18 | ///////////////////////////////////////////////////////////// 19 | void Customer::addRental(const Rental& rental) 20 | { 21 | rentals.push_back(rental); 22 | } 23 | 24 | ///////////////////////////////////////////////////////////// 25 | string Customer::statement() const 26 | { 27 | double totalAmount = 0; 28 | int frequetRenterPointer = 0; 29 | 30 | stringstream ss; 31 | 32 | ss << "Rental Record for " << getName() << "\n"; 33 | 34 | for(Rentals::const_iterator rental = rentals.begin(); rental != rentals.end(); ++rental) 35 | { 36 | double thisAmount = 0; 37 | 38 | // determine amounts for each line 39 | switch(rental->getMovie().getPriceCode()) 40 | { 41 | case Movie::REGULAR: 42 | thisAmount += 2; 43 | if(rental->getDaysRented() > 2) 44 | { 45 | thisAmount += (rental->getDaysRented() - 2) * 1.5; 46 | } 47 | break; 48 | 49 | case Movie::NEW_RELEASE: 50 | thisAmount += rental->getDaysRented() * 3; 51 | break; 52 | 53 | case Movie::CHILDRENS: 54 | thisAmount += 1.5; 55 | if(rental->getDaysRented() > 3) 56 | { 57 | thisAmount += (rental->getDaysRented() - 3) * 1.5; 58 | } 59 | break; 60 | } 61 | 62 | // add frequent renter points 63 | frequetRenterPointer++; 64 | 65 | // add bonus for a two day new release rental 66 | if((rental->getMovie().getPriceCode() == Movie::NEW_RELEASE) 67 | &&(rental->getDaysRented() > 1)) 68 | { 69 | frequetRenterPointer++; 70 | } 71 | 72 | // show figures for this rental 73 | ss << "\t" << rental->getMovie().getTitle() << "\t" << thisAmount << "\n"; 74 | 75 | totalAmount += thisAmount; 76 | } 77 | 78 | // add footer lines 79 | ss << "Amount ownd is " << totalAmount << "\n"; 80 | ss << "You earned " << frequetRenterPointer << " frequent renter points"; 81 | 82 | return ss.str(); 83 | } 84 | -------------------------------------------------------------------------------- /code/cpp/original/Customer.h: -------------------------------------------------------------------------------- 1 | #ifndef CUSTOMER_H_ 2 | #define CUSTOMER_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include "Rental.h" 8 | 9 | using std::string; 10 | using std::vector; 11 | 12 | struct Customer 13 | { 14 | Customer(const string& name); 15 | string getName() const; 16 | void addRental(const Rental& rental); 17 | string statement() const; 18 | 19 | private: 20 | typedef vector Rentals; 21 | Rentals rentals; 22 | string name; 23 | }; 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /code/cpp/original/Main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "Customer.h" 4 | #include "Movie.h" 5 | #include "Rental.h" 6 | 7 | using namespace std; 8 | 9 | #define ASSERT_EQ(expect, result) \ 10 | if(expect == result) \ 11 | { \ 12 | cout << "PASS" << endl; \ 13 | } \ 14 | else \ 15 | { \ 16 | cout << "FAILED! " << endl; \ 17 | cout << "EXPECT: " << endl; \ 18 | cout << expect << endl; \ 19 | cout << "RESULT: " << endl; \ 20 | cout << result << endl; \ 21 | } 22 | 23 | int main() 24 | { 25 | auto_ptr m1(new Movie("Family", Movie::CHILDRENS)); 26 | auto_ptr m2(new Movie("The Star on the Earth", Movie::NEW_RELEASE)); 27 | auto_ptr m3(new Movie("THe American Leader", Movie::REGULAR)); 28 | 29 | Customer customer("Brain"); 30 | customer.addRental(Rental(m1.get(), 3)); 31 | customer.addRental(Rental(m2.get(), 4)); 32 | customer.addRental(Rental(m3.get(), 5)); 33 | 34 | string expect = ""; 35 | expect += "Rental Record for Brain\n"; 36 | expect += "\tFamily\t1.5\n"; 37 | expect += "\tThe Star on the Earth\t12\n"; 38 | expect += "\tTHe American Leader\t6.5\n"; 39 | expect += "Amount ownd is 20\n"; 40 | expect += "You earned 4 frequent renter points"; 41 | 42 | ASSERT_EQ(expect, customer.statement()); 43 | 44 | return 0; 45 | } 46 | -------------------------------------------------------------------------------- /code/cpp/original/Movie.cpp: -------------------------------------------------------------------------------- 1 | #include "Movie.h" 2 | 3 | //////////////////////////////////////////////////////////// 4 | Movie::Movie(const string& title, const int priceCode) : title(title), priceCode(priceCode) 5 | { 6 | } 7 | 8 | //////////////////////////////////////////////////////////// 9 | int Movie::getPriceCode() const 10 | { 11 | return priceCode; 12 | } 13 | 14 | //////////////////////////////////////////////////////////// 15 | void Movie::setPriceCode(const int priceCode) 16 | { 17 | this->priceCode = priceCode; 18 | } 19 | 20 | //////////////////////////////////////////////////////////// 21 | string Movie::getTitle() const 22 | { 23 | return title; 24 | } 25 | -------------------------------------------------------------------------------- /code/cpp/original/Movie.h: -------------------------------------------------------------------------------- 1 | #ifndef MOVIE_H_ 2 | #define MOVIE_H_ 3 | 4 | #include 5 | 6 | using std::string; 7 | 8 | struct Movie 9 | { 10 | Movie(const string& title, const int priceCode); 11 | int getPriceCode() const; 12 | void setPriceCode(const int priceCode); 13 | string getTitle() const; 14 | 15 | static const int CHILDRENS = 2; 16 | static const int REGULAR = 0; 17 | static const int NEW_RELEASE = 1; 18 | 19 | private: 20 | string title; 21 | int priceCode; 22 | }; 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /code/cpp/original/Rental.cpp: -------------------------------------------------------------------------------- 1 | #include "Rental.h" 2 | 3 | //////////////////////////////////////////////////////////// 4 | Rental::Rental(const Movie* movie, const int daysRented) 5 | : movie(movie), daysRented(daysRented) 6 | { 7 | } 8 | 9 | //////////////////////////////////////////////////////////// 10 | int Rental::getDaysRented() const 11 | { 12 | return daysRented; 13 | } 14 | 15 | //////////////////////////////////////////////////////////// 16 | const Movie& Rental::getMovie() const 17 | { 18 | return *movie; 19 | } 20 | 21 | -------------------------------------------------------------------------------- /code/cpp/original/Rental.h: -------------------------------------------------------------------------------- 1 | #ifndef RENTAL_H_ 2 | #define RENTAL_H_ 3 | 4 | struct Movie; 5 | 6 | struct Rental 7 | { 8 | Rental(const Movie* movie, const int daysRented); 9 | int getDaysRented() const; 10 | const Movie& getMovie() const; 11 | 12 | private: 13 | const Movie* movie; 14 | int daysRented; 15 | }; 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /code/cpp/result/ChildrensPrice.cpp: -------------------------------------------------------------------------------- 1 | #include "ChildrensPrice.h" 2 | 3 | ///////////////////////////////////////////////////////////// 4 | double ChildrensPrice::getCharge(const int daysRented) const 5 | { 6 | double result = 1.5; 7 | if(daysRented > 3) 8 | { 9 | result += (daysRented - 3) * 1.5; 10 | } 11 | return result; 12 | } 13 | -------------------------------------------------------------------------------- /code/cpp/result/ChildrensPrice.h: -------------------------------------------------------------------------------- 1 | #ifndef CHILDRENSPRICE_H_ 2 | #define CHILDRENSPRICE_H_ 3 | 4 | #include "Price.h" 5 | 6 | struct ChildrensPrice : Price 7 | { 8 | virtual double getCharge(const int daysRented) const; 9 | }; 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /code/cpp/result/Customer.cpp: -------------------------------------------------------------------------------- 1 | #include "Customer.h" 2 | #include "Movie.h" 3 | #include 4 | 5 | using std::stringstream; 6 | 7 | ///////////////////////////////////////////////////////////// 8 | Customer::Customer(const string& name) : name(name) 9 | { 10 | } 11 | 12 | ///////////////////////////////////////////////////////////// 13 | string Customer::getName() const 14 | { 15 | return name; 16 | } 17 | 18 | ///////////////////////////////////////////////////////////// 19 | void Customer::addRental(const Rental& rental) 20 | { 21 | rentals.push_back(rental); 22 | } 23 | 24 | ///////////////////////////////////////////////////////////// 25 | double Customer::getTotalCharge() const 26 | { 27 | double result = 0; 28 | for(Rentals::const_iterator rental = rentals.begin(); rental != rentals.end(); ++rental) 29 | { 30 | result += rental->getCharge(); 31 | } 32 | 33 | return result; 34 | } 35 | 36 | ///////////////////////////////////////////////////////////// 37 | int Customer::getTotalFrequetRenterPointer() const 38 | { 39 | int result = 0; 40 | for(Rentals::const_iterator rental = rentals.begin(); rental != rentals.end(); ++rental) 41 | { 42 | result += rental->getFrequentRenterPointers(); 43 | } 44 | 45 | return result; 46 | } 47 | 48 | ///////////////////////////////////////////////////////////// 49 | string Customer::statement() const 50 | { 51 | stringstream ss; 52 | 53 | ss << "Rental Record for " << getName() << "\n"; 54 | 55 | for(Rentals::const_iterator rental = rentals.begin(); rental != rentals.end(); ++rental) 56 | { 57 | // show figures for this rental 58 | ss << "\t" << rental->getMovie().getTitle() << "\t" << rental->getCharge() << "\n"; 59 | } 60 | 61 | // add footer lines 62 | ss << "Amount ownd is " << getTotalCharge() << "\n"; 63 | ss << "You earned " << getTotalFrequetRenterPointer() << " frequent renter points"; 64 | 65 | return ss.str(); 66 | } 67 | -------------------------------------------------------------------------------- /code/cpp/result/Customer.h: -------------------------------------------------------------------------------- 1 | #ifndef CUSTOMER_H_ 2 | #define CUSTOMER_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include "Rental.h" 8 | 9 | using std::string; 10 | using std::vector; 11 | 12 | struct Customer 13 | { 14 | Customer(const string& name); 15 | string getName() const; 16 | void addRental(const Rental& rental); 17 | string statement() const; 18 | 19 | private: 20 | double getTotalCharge() const; 21 | int getTotalFrequetRenterPointer() const; 22 | 23 | private: 24 | typedef vector Rentals; 25 | Rentals rentals; 26 | string name; 27 | }; 28 | 29 | #endif 30 | -------------------------------------------------------------------------------- /code/cpp/result/Main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "Customer.h" 4 | #include "Movie.h" 5 | #include "Rental.h" 6 | 7 | using namespace std; 8 | 9 | #define ASSERT_EQ(expect, result) \ 10 | if(expect == result) \ 11 | { \ 12 | cout << "PASS" << endl; \ 13 | } \ 14 | else \ 15 | { \ 16 | cout << "FAILED! " << endl; \ 17 | cout << "EXPECT: " << endl; \ 18 | cout << expect << endl; \ 19 | cout << "RESULT: " << endl; \ 20 | cout << result << endl; \ 21 | } 22 | 23 | int main() 24 | { 25 | auto_ptr m1(new Movie("Family", Movie::CHILDRENS)); 26 | auto_ptr m2(new Movie("The Star on the Earth", Movie::NEW_RELEASE)); 27 | auto_ptr m3(new Movie("THe American Leader", Movie::REGULAR)); 28 | 29 | Customer customer("Brain"); 30 | customer.addRental(Rental(m1.get(), 3)); 31 | customer.addRental(Rental(m2.get(), 4)); 32 | customer.addRental(Rental(m3.get(), 5)); 33 | 34 | string expect = ""; 35 | expect += "Rental Record for Brain\n"; 36 | expect += "\tFamily\t1.5\n"; 37 | expect += "\tThe Star on the Earth\t12\n"; 38 | expect += "\tTHe American Leader\t6.5\n"; 39 | expect += "Amount ownd is 20\n"; 40 | expect += "You earned 4 frequent renter points"; 41 | 42 | ASSERT_EQ(expect, customer.statement()); 43 | 44 | return 0; 45 | } 46 | -------------------------------------------------------------------------------- /code/cpp/result/Movie.cpp: -------------------------------------------------------------------------------- 1 | #include "Movie.h" 2 | #include "ChildrensPrice.h" 3 | #include "NewReleasePrice.h" 4 | #include "RegularPrice.h" 5 | #include 6 | 7 | //////////////////////////////////////////////////////////// 8 | Movie::Movie(const string& title, const int priceCode) : title(title) 9 | { 10 | setPriceCode(priceCode); 11 | } 12 | 13 | //////////////////////////////////////////////////////////// 14 | void Movie::setPriceCode(const int priceCode) 15 | { 16 | switch(priceCode) 17 | { 18 | case REGULAR: 19 | price.reset(new RegularPrice()); 20 | break; 21 | case CHILDRENS: 22 | price.reset(new ChildrensPrice()); 23 | break; 24 | case NEW_RELEASE: 25 | price.reset(new NewReleasePrice()); 26 | break; 27 | default: 28 | throw std::exception(); 29 | } 30 | } 31 | 32 | //////////////////////////////////////////////////////////// 33 | string Movie::getTitle() const 34 | { 35 | return title; 36 | } 37 | 38 | //////////////////////////////////////////////////////////// 39 | int Movie::getFrequentRenterPointers(const int daysRented) const 40 | { 41 | return price->getFrequentRenterPointers(daysRented); 42 | } 43 | 44 | //////////////////////////////////////////////////////////// 45 | double Movie::getCharge(const int daysRented) const 46 | { 47 | return price->getCharge(daysRented); 48 | } 49 | -------------------------------------------------------------------------------- /code/cpp/result/Movie.h: -------------------------------------------------------------------------------- 1 | #ifndef MOVIE_H_ 2 | #define MOVIE_H_ 3 | 4 | #include 5 | #include 6 | 7 | using std::auto_ptr; 8 | using std::string; 9 | 10 | struct Price; 11 | 12 | struct Movie 13 | { 14 | Movie(const string& title, const int priceCode); 15 | string getTitle() const; 16 | 17 | double getCharge(const int daysRented) const; 18 | int getFrequentRenterPointers(const int daysRented) const; 19 | 20 | static const int CHILDRENS = 2; 21 | static const int REGULAR = 0; 22 | static const int NEW_RELEASE = 1; 23 | 24 | private: 25 | void setPriceCode(const int priceCode); 26 | 27 | private: 28 | string title; 29 | auto_ptr price; 30 | }; 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /code/cpp/result/NewReleasePrice.cpp: -------------------------------------------------------------------------------- 1 | #include "NewReleasePrice.h" 2 | 3 | ///////////////////////////////////////////////////////////// 4 | double NewReleasePrice::getCharge(const int daysRented) const 5 | { 6 | return daysRented * 3; 7 | } 8 | 9 | ///////////////////////////////////////////////////////////// 10 | int NewReleasePrice::getFrequentRenterPointers(const int daysRented) const 11 | { 12 | return (daysRented > 1) ? 2 : 1; 13 | } 14 | -------------------------------------------------------------------------------- /code/cpp/result/NewReleasePrice.h: -------------------------------------------------------------------------------- 1 | #ifndef NEWRELEASEPRICE_H_ 2 | #define NEWRELEASEPRICE_H_ 3 | 4 | #include "Price.h" 5 | 6 | struct NewReleasePrice : Price 7 | { 8 | virtual double getCharge(const int daysRented) const; 9 | virtual int getFrequentRenterPointers(const int daysRented) const; 10 | }; 11 | 12 | #endif 13 | -------------------------------------------------------------------------------- /code/cpp/result/Price.h: -------------------------------------------------------------------------------- 1 | #ifndef PRICE_H_ 2 | #define PRICE_H_ 3 | 4 | struct Price 5 | { 6 | virtual double getCharge(const int daysRented) const = 0; 7 | 8 | virtual int getFrequentRenterPointers(const int daysRented) const 9 | { 10 | return 1; 11 | } 12 | 13 | virtual ~Price() {} 14 | }; 15 | 16 | #endif 17 | -------------------------------------------------------------------------------- /code/cpp/result/RegularPrice.cpp: -------------------------------------------------------------------------------- 1 | #include "RegularPrice.h" 2 | 3 | ///////////////////////////////////////////////////////////// 4 | double RegularPrice::getCharge(const int daysRented) const 5 | { 6 | double result = 2; 7 | if(daysRented > 2) 8 | { 9 | result += (daysRented - 2) * 1.5; 10 | } 11 | return result; 12 | } 13 | -------------------------------------------------------------------------------- /code/cpp/result/RegularPrice.h: -------------------------------------------------------------------------------- 1 | #ifndef REGULARPRICE_H_ 2 | #define REGULARPRICE_H_ 3 | 4 | #include "Price.h" 5 | 6 | struct RegularPrice : Price 7 | { 8 | virtual double getCharge(const int daysRented) const; 9 | }; 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /code/cpp/result/Rental.cpp: -------------------------------------------------------------------------------- 1 | #include "Rental.h" 2 | 3 | #include "Movie.h" 4 | 5 | //////////////////////////////////////////////////////////// 6 | Rental::Rental(const Movie* movie, const int daysRented) 7 | : movie(movie), daysRented(daysRented) 8 | { 9 | } 10 | 11 | //////////////////////////////////////////////////////////// 12 | const Movie& Rental::getMovie() const 13 | { 14 | return *movie; 15 | } 16 | 17 | //////////////////////////////////////////////////////////// 18 | double Rental::getCharge() const 19 | { 20 | return movie->getCharge(daysRented); 21 | } 22 | 23 | //////////////////////////////////////////////////////////// 24 | int Rental::getFrequentRenterPointers() const 25 | { 26 | return movie->getFrequentRenterPointers(daysRented); 27 | } 28 | -------------------------------------------------------------------------------- /code/cpp/result/Rental.h: -------------------------------------------------------------------------------- 1 | #ifndef RENTAL_H_ 2 | #define RENTAL_H_ 3 | 4 | struct Movie; 5 | 6 | struct Rental 7 | { 8 | Rental(const Movie* movie, const int daysRented); 9 | const Movie& getMovie() const; 10 | 11 | double getCharge() const; 12 | int getFrequentRenterPointers() const; 13 | 14 | private: 15 | const Movie* movie; 16 | int daysRented; 17 | }; 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /code/java/original/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | magicbowen 6 | RefactoringPractice 7 | 1.0-SNAPSHOT 8 | 9 | 10 | 11 | org.apache.maven.plugins 12 | maven-compiler-plugin 13 | 14 | 1.7 15 | 1.7 16 | 17 | 18 | 19 | 20 | jar 21 | 22 | RefactoringPractice 23 | http://maven.apache.org 24 | 25 | 26 | UTF-8 27 | 28 | 29 | 30 | 31 | junit 32 | junit 33 | 4.12 34 | test 35 | 36 | 37 | org.hamcrest 38 | java-hamcrest 39 | 2.0.0.0 40 | test 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /code/java/original/src/main/java/magicbowen/Customer.java: -------------------------------------------------------------------------------- 1 | package magicbowen; 2 | 3 | 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | public class Customer { 8 | private String name; 9 | private List rentals = new ArrayList<>(); 10 | 11 | public Customer(String name) { 12 | this.name = name; 13 | } 14 | 15 | public String getName() { 16 | return name; 17 | } 18 | 19 | public void addRental(Rental rental) { 20 | rentals.add(rental); 21 | } 22 | 23 | public String statement() { 24 | double totalAmount = 0; 25 | int frequentRenterPoints = 0; 26 | String result = "Rental record for " + getName() + "\n"; 27 | for (Rental rental : rentals) { 28 | double amount = 0; 29 | switch (rental.getMovie().getPriceCode()) { 30 | case Movie.REGULAR: 31 | amount += 2; 32 | if (rental.getDaysRented() > 2) { 33 | amount += (rental.getDaysRented() - 2) * 1.5; 34 | } 35 | break; 36 | case Movie.NEW_RELEASE: 37 | amount += rental.getDaysRented() * 3; 38 | break; 39 | case Movie.CHILDREN: 40 | amount += 1.5; 41 | if (rental.getDaysRented() > 3) { 42 | amount += (rental.getDaysRented() - 3) * 1.5; 43 | } 44 | break; 45 | } 46 | 47 | frequentRenterPoints++; 48 | if ((rental.getMovie().getPriceCode() == Movie.NEW_RELEASE) && (rental.getDaysRented() > 1)) { 49 | frequentRenterPoints++; 50 | } 51 | 52 | result += "\t" + rental.getMovie().getTitle() + "\t" + String.valueOf(amount) + "\n"; 53 | totalAmount += amount; 54 | } 55 | result += "Amount owed is " + String.valueOf(totalAmount) + "\n"; 56 | result += "You earned " + String.valueOf(frequentRenterPoints) + " frequent renter points\n"; 57 | return result; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /code/java/original/src/main/java/magicbowen/Movie.java: -------------------------------------------------------------------------------- 1 | package magicbowen; 2 | 3 | public class Movie { 4 | public static final int REGULAR = 0; 5 | public static final int NEW_RELEASE = 1; 6 | public static final int CHILDREN = 2; 7 | 8 | private String title; 9 | 10 | private int priceCode; 11 | 12 | public Movie(String title, int priceCode) { 13 | this.title = title; 14 | this.priceCode = priceCode; 15 | } 16 | 17 | public int getPriceCode() { 18 | return priceCode; 19 | } 20 | 21 | public String getTitle() { 22 | return title; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /code/java/original/src/main/java/magicbowen/Rental.java: -------------------------------------------------------------------------------- 1 | package magicbowen; 2 | 3 | 4 | public class Rental { 5 | private Movie movie; 6 | private int daysRented; 7 | 8 | public Rental(Movie movie, int daysRented) { 9 | this.movie = movie; 10 | this.daysRented = daysRented; 11 | } 12 | 13 | public int getDaysRented() { 14 | return daysRented; 15 | } 16 | 17 | public Movie getMovie() { 18 | return movie; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /code/java/original/src/test/java/magicbowen/CustomerTest.java: -------------------------------------------------------------------------------- 1 | package magicbowen; 2 | 3 | import org.junit.Test; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.Matchers.*; 6 | 7 | 8 | public class CustomerTest { 9 | @Test 10 | public void statement() throws Exception { 11 | Customer customer = new Customer("Robert"); 12 | customer.addRental(new Rental(new Movie("The Beautiful Life", Movie.NEW_RELEASE), 5)); 13 | customer.addRental(new Rental(new Movie("Tom and Jerry", Movie.CHILDREN), 4)); 14 | customer.addRental(new Rental(new Movie("Animal World", Movie.REGULAR), 2)); 15 | String statement = new StringBuilder() 16 | .append("Rental record for Robert\n") 17 | .append("\tThe Beautiful Life\t15.0\n") 18 | .append("\tTom and Jerry\t3.0\n") 19 | .append("\tAnimal World\t2.0\n") 20 | .append("Amount owed is 20.0\n") 21 | .append("You earned 4 frequent renter points\n") 22 | .toString(); 23 | assertThat(statement, equalTo(customer.statement())); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /code/java/result/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | magicbowen 6 | RefactoringResult 7 | 1.0-SNAPSHOT 8 | 9 | 10 | 11 | org.apache.maven.plugins 12 | maven-compiler-plugin 13 | 14 | 1.8 15 | 1.8 16 | 17 | 18 | 19 | 20 | jar 21 | 22 | RefactoringResult 23 | http://maven.apache.org 24 | 25 | 26 | UTF-8 27 | 28 | 29 | 30 | 31 | junit 32 | junit 33 | 4.12 34 | test 35 | 36 | 37 | org.hamcrest 38 | java-hamcrest 39 | 2.0.0.0 40 | test 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /code/java/result/src/main/java/magicbowen/Customer.java: -------------------------------------------------------------------------------- 1 | package magicbowen; 2 | 3 | 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.function.BinaryOperator; 7 | import java.util.function.Function; 8 | 9 | 10 | public class Customer { 11 | public Customer(String name) { 12 | this.name = name; 13 | } 14 | 15 | public void addRental(Rental rental) { 16 | rentals.add(rental); 17 | } 18 | 19 | public String statement(StatementFormatter formatter) { 20 | formatter.onCustomer(getName()); 21 | rentals.stream().forEach(rental -> formatter.onRental(rental.getMovie().getTitle(), rental.getCharge())); 22 | formatter.onTotal(getTotalCharge(), getTotalRenterPoints()); 23 | return formatter.statement(); 24 | } 25 | 26 | private double getTotalCharge() { 27 | return accumulate(0.0, rental -> rental.getCharge(), Double::sum); 28 | } 29 | 30 | private int getTotalRenterPoints() { 31 | return accumulate(0, rental -> rental.getPoints(), Integer::sum); 32 | } 33 | 34 | private T accumulate(T initial, Function mapper, BinaryOperator accumulator) { 35 | return rentals.stream().map(mapper).reduce(initial, accumulator); 36 | } 37 | 38 | private String getName() { 39 | return name; 40 | } 41 | 42 | private String name; 43 | private List rentals = new ArrayList<>(); 44 | } 45 | -------------------------------------------------------------------------------- /code/java/result/src/main/java/magicbowen/Movie.java: -------------------------------------------------------------------------------- 1 | package magicbowen; 2 | 3 | 4 | public class Movie { 5 | private String title; 6 | private MovieType type; 7 | 8 | public Movie(String title, MovieType type) { 9 | this.title = title; 10 | this.type = type; 11 | } 12 | 13 | public String getTitle() { 14 | return title; 15 | } 16 | 17 | public double getCharge(int daysRented) { 18 | return type.getCharge(daysRented); 19 | } 20 | 21 | public int getPoints(int daysRented) { 22 | return type.getPoints(daysRented); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /code/java/result/src/main/java/magicbowen/MovieType.java: -------------------------------------------------------------------------------- 1 | package magicbowen; 2 | 3 | 4 | public enum MovieType { 5 | REGULAR_TYPE() { 6 | public double getCharge(int daysRented) { 7 | double result = 2; 8 | if (daysRented > 2) { 9 | result += (daysRented - 2) * 1.5; 10 | } 11 | return result; 12 | } 13 | }, 14 | NEW_RELEASE_TYPE() { 15 | public double getCharge(int daysRented) { 16 | return daysRented * 3; 17 | } 18 | public int getPoints(int daysRented) { 19 | return (daysRented > 1) ? 2 : 1; 20 | } 21 | }, 22 | CHILDREN_TYPE() { 23 | public double getCharge(int daysRented) { 24 | double result = 1.5; 25 | if (daysRented > 3) { 26 | result += (daysRented - 3) * 1.5; 27 | } 28 | return result; 29 | } 30 | }; 31 | 32 | public abstract double getCharge(int daysRented); 33 | 34 | public int getPoints(int daysRented) { 35 | return 1; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /code/java/result/src/main/java/magicbowen/Rental.java: -------------------------------------------------------------------------------- 1 | package magicbowen; 2 | 3 | 4 | public class Rental { 5 | private Movie movie; 6 | private int daysRented; 7 | 8 | public Rental(Movie movie, int daysRented) { 9 | this.movie = movie; 10 | this.daysRented = daysRented; 11 | } 12 | 13 | public Movie getMovie() { 14 | return movie; 15 | } 16 | 17 | public double getCharge() { 18 | return movie.getCharge(daysRented); 19 | } 20 | 21 | public int getPoints() { 22 | return movie.getPoints(daysRented); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /code/java/result/src/main/java/magicbowen/StatementFormatter.java: -------------------------------------------------------------------------------- 1 | package magicbowen; 2 | 3 | 4 | public interface StatementFormatter { 5 | void onCustomer(String name); 6 | void onRental(String movieName, double charge); 7 | void onTotal(double charge, int points); 8 | String statement(); 9 | } 10 | -------------------------------------------------------------------------------- /code/java/result/src/main/java/magicbowen/TextStatementFormatter.java: -------------------------------------------------------------------------------- 1 | package magicbowen; 2 | 3 | 4 | public class TextStatementFormatter implements StatementFormatter { 5 | @Override 6 | public void onCustomer(String name) { 7 | statement.append(String.format("Rental record for %s%n", name)); 8 | } 9 | 10 | @Override 11 | public void onRental(String movieName, double charge) { 12 | statement.append(String.format("\t%s\t%.1f\n", movieName, charge)); 13 | } 14 | 15 | @Override 16 | public void onTotal(double charge, int points) { 17 | statement.append(String.format("Amount owed is %.1f%n", charge)); 18 | statement.append(String.format("You earned %d frequent renter points%n", points)); 19 | } 20 | 21 | @Override 22 | public String statement() { 23 | return statement.toString(); 24 | } 25 | 26 | private StringBuilder statement = new StringBuilder(); 27 | } 28 | -------------------------------------------------------------------------------- /code/java/result/src/test/java/magicbowen/CustomerTest.java: -------------------------------------------------------------------------------- 1 | package magicbowen; 2 | 3 | import magicbowen.testdoubles.SpyStatementFormatter; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | 7 | import static magicbowen.MovieType.*; 8 | import static org.hamcrest.MatcherAssert.assertThat; 9 | import static org.hamcrest.Matchers.*; 10 | 11 | 12 | public class CustomerTest { 13 | private static final String NAME = "Robert"; 14 | 15 | private Customer customer; 16 | private SpyStatementFormatter formatter; 17 | 18 | @Before 19 | public void setup() { 20 | customer = new Customer(NAME); 21 | formatter = new SpyStatementFormatter(); 22 | } 23 | 24 | @Test 25 | public void baseInfo() { 26 | customer.statement(formatter); 27 | assertThat(NAME, equalTo(formatter.getCustomerName())); 28 | assertThat(0.0, equalTo(formatter.getTotalCharge())); 29 | assertThat(0, equalTo(formatter.getTotalPoints())); 30 | } 31 | 32 | @Test 33 | public void newReleaseLessThan2Days() { 34 | customer.addRental(new Rental(new Movie("A", NEW_RELEASE_TYPE), 1)); 35 | customer.statement(formatter); 36 | assertThat(3.0, equalTo(formatter.getChargeOf("A"))); 37 | assertThat(3.0, equalTo(formatter.getTotalCharge())); 38 | assertThat(1, equalTo(formatter.getTotalPoints())); 39 | } 40 | 41 | @Test 42 | public void newReleaseMoreThan3Days() { 43 | customer.addRental(new Rental(new Movie("A", NEW_RELEASE_TYPE), 4)); 44 | customer.statement(formatter); 45 | assertThat(12.0, equalTo(formatter.getTotalCharge())); 46 | assertThat(2, equalTo(formatter.getTotalPoints())); 47 | } 48 | 49 | // More tests for all algorithms of different movie types 50 | 51 | // Just one end-to-end test to verify the relation of all processes 52 | @Test 53 | public void statement() { 54 | customer.addRental(new Rental(new Movie("A", NEW_RELEASE_TYPE), 5)); 55 | customer.addRental(new Rental(new Movie("B", CHILDREN_TYPE), 4)); 56 | customer.addRental(new Rental(new Movie("C", REGULAR_TYPE), 2)); 57 | 58 | customer.statement(formatter); 59 | 60 | assertThat(NAME, equalTo(formatter.getCustomerName())); 61 | assertThat(15.0, equalTo(formatter.getChargeOf("A"))); 62 | assertThat(3.0, equalTo(formatter.getChargeOf("B"))); 63 | assertThat(2.0, equalTo(formatter.getChargeOf("C"))); 64 | assertThat(20.0, equalTo(formatter.getTotalCharge())); 65 | assertThat(4, equalTo(formatter.getTotalPoints())); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /code/java/result/src/test/java/magicbowen/TextStatementFormatterTest.java: -------------------------------------------------------------------------------- 1 | package magicbowen; 2 | 3 | import org.junit.Test; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.Matchers.equalTo; 6 | 7 | 8 | public class TextStatementFormatterTest { 9 | @Test 10 | public void statement() throws Exception { 11 | 12 | StatementFormatter formatter = new TextStatementFormatter(); 13 | formatter.onCustomer("Robert"); 14 | formatter.onRental("The Beautiful Life", 15.0); 15 | formatter.onRental("Tom and Jerry", 3.0); 16 | formatter.onRental("Animal World", 2.0); 17 | formatter.onTotal(20.0, 4); 18 | 19 | String statement = new StringBuilder() 20 | .append("Rental record for Robert\n") 21 | .append("\tThe Beautiful Life\t15.0\n") 22 | .append("\tTom and Jerry\t3.0\n") 23 | .append("\tAnimal World\t2.0\n") 24 | .append("Amount owed is 20.0\n") 25 | .append("You earned 4 frequent renter points\n") 26 | .toString(); 27 | 28 | assertThat(statement, equalTo(formatter.statement())); 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /code/java/result/src/test/java/magicbowen/testdoubles/SpyStatementFormatter.java: -------------------------------------------------------------------------------- 1 | package magicbowen.testdoubles; 2 | 3 | import magicbowen.StatementFormatter; 4 | 5 | import java.util.HashMap; 6 | 7 | 8 | public class SpyStatementFormatter implements StatementFormatter { 9 | private String customerName; 10 | private HashMap rentals = new HashMap<>(); 11 | private int totalPoints; 12 | private double totalCharge; 13 | 14 | @Override 15 | public void onCustomer(String name) { 16 | customerName = name; 17 | } 18 | 19 | @Override 20 | public void onRental(String movieName, double charge) { 21 | rentals.put(movieName, charge); 22 | } 23 | 24 | @Override 25 | public void onTotal(double charge, int points) { 26 | totalCharge = charge; 27 | totalPoints = points; 28 | } 29 | 30 | @Override 31 | public String statement() { 32 | return null; 33 | } 34 | 35 | public String getCustomerName() { 36 | return customerName; 37 | } 38 | 39 | public double getChargeOf(String movieName) { 40 | return rentals.get(movieName); 41 | } 42 | 43 | public int getTotalPoints() { 44 | return totalPoints; 45 | } 46 | 47 | public double getTotalCharge() { 48 | return totalCharge; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /code/js/original/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "refactoring", 3 | "version": "1.0.0", 4 | "description": "Refactoring Practice", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "mocha" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/MagicBowen/refactoring/tree/master/code/js/original" 15 | }, 16 | "keywords": [ 17 | "refactoring", 18 | "js", 19 | "practice" 20 | ], 21 | "author": "MagicBowen", 22 | "license": "ISC", 23 | "dependencies": {}, 24 | "devDependencies": { 25 | "mocha": "*", 26 | "chai": "*" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /code/js/original/src/index.js: -------------------------------------------------------------------------------- 1 | function statement(invoice, plays) { 2 | let totalAmount = 0; 3 | let volumeCredits = 0; 4 | let result = `Statement for ${invoice.customer}\n`; 5 | const format = new Intl.NumberFormat("en-US", { 6 | style: "currency", 7 | currency: "USD", 8 | minimumFractionDigits: 2}).format; 9 | 10 | for (let perf of invoice.performances) { 11 | const play = plays[perf.playID]; 12 | let thisAmount = 0; 13 | 14 | switch (play.type) { 15 | case "tragedy": 16 | thisAmount = 40000; 17 | if (perf.audience > 30) { 18 | thisAmount += 1000 * (perf.audience - 30); 19 | } 20 | break; 21 | case "comedy": 22 | thisAmount = 30000; 23 | if (perf.audience > 20) { 24 | thisAmount += 10000 + 500 * (perf.audience - 20); 25 | } 26 | thisAmount += 300 * perf.audience; 27 | break; 28 | default: 29 | throw new Error(`unknown type: ${play.type}`); 30 | } 31 | 32 | // add volume credits 33 | volumeCredits += Math.max(perf.audience - 30, 0); 34 | // add extra credit for every ten comedy attendees 35 | if ("comedy" === play.type) volumeCredits += Math.floor(perf.audience / 5); 36 | 37 | // print line for this order 38 | result += ` ${play.name}: ${format(thisAmount/100)} (${perf.audience} seats)\n`; 39 | totalAmount += thisAmount; 40 | } 41 | result += `Amount owned is ${format(totalAmount/100)}\n`; 42 | result += `You earned ${volumeCredits} credits\n`; 43 | return result; 44 | } 45 | 46 | module.exports = statement; -------------------------------------------------------------------------------- /code/js/original/test/invoices.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "customer": "BigCo", 4 | "performances": [ 5 | { 6 | "playID": "hamlet", 7 | "audience": 55 8 | }, 9 | { 10 | "playID": "as-like", 11 | "audience": 35 12 | }, 13 | { 14 | "playID": "othello", 15 | "audience": 40 16 | } 17 | ] 18 | } 19 | ] -------------------------------------------------------------------------------- /code/js/original/test/plays.json: -------------------------------------------------------------------------------- 1 | { 2 | "hamlet": {"name": "Hamlet", "type": "tragedy"}, 3 | "as-like": {"name": "As You Like It", "type": "comedy"}, 4 | "othello": {"name": "Othello", "type": "tragedy"} 5 | } -------------------------------------------------------------------------------- /code/js/original/test/testStatement.js: -------------------------------------------------------------------------------- 1 | const assert = require('chai').assert; 2 | const statement = require('../src'); 3 | const plays = require('./plays.json'); 4 | const invoices = require('./invoices.json'); 5 | 6 | const expected = `Statement for BigCo 7 | Hamlet: $650.00 (55 seats) 8 | As You Like It: $580.00 (35 seats) 9 | Othello: $500.00 (40 seats) 10 | Amount owned is $1,730.00 11 | You earned 47 credits 12 | `; 13 | 14 | describe('Statement', () => { 15 | describe('#statement()', () => { 16 | it('plait text result', function () { 17 | assert.equal(statement(invoices[0], plays), expected); 18 | }); 19 | }); 20 | }); -------------------------------------------------------------------------------- /code/js/result/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "refactoring", 3 | "version": "1.0.0", 4 | "description": "Refactoring Practice", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "mocha" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/MagicBowen/refactoring/tree/master/code/js/result" 15 | }, 16 | "keywords": [ 17 | "refactoring", 18 | "js", 19 | "practice" 20 | ], 21 | "author": "MagicBowen", 22 | "license": "ISC", 23 | "dependencies": {}, 24 | "devDependencies": { 25 | "mocha": "*", 26 | "chai": "*" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /code/js/result/src/calculator.js: -------------------------------------------------------------------------------- 1 | class PerformanceCalculator { 2 | constructor(aPerformance, aPlay){ 3 | this.performance = aPerformance; 4 | this.play = aPlay; 5 | } 6 | 7 | get volumeCredits() { 8 | return Math.max(this.performance.audience - 30, 0); 9 | } 10 | } 11 | 12 | class TragedyCalculator extends PerformanceCalculator { 13 | get amount() { 14 | let result = 40000; 15 | if (this.performance.audience > 30) { 16 | result += 1000 * (this.performance.audience - 30); 17 | } 18 | return result; 19 | } 20 | } 21 | 22 | class ComedyCalculator extends PerformanceCalculator { 23 | get amount() { 24 | let result = 30000; 25 | if (this.performance.audience > 20) { 26 | result += 10000 + 500 * (this.performance.audience - 20); 27 | } 28 | result += 300 * this.performance.audience; 29 | return result; 30 | } 31 | 32 | get volumeCredits() { 33 | return super.volumeCredits + Math.floor(this.performance.audience / 5); 34 | } 35 | } 36 | 37 | module.exports = function (aPerformance, aPlay) { 38 | switch(aPlay.type) { 39 | case "tragedy": return new TragedyCalculator(aPerformance, aPlay); 40 | case "comedy": return new ComedyCalculator(aPerformance, aPlay); 41 | default: throw new Error(`unknown type: ${aPlay.type}`); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /code/js/result/src/createStatementData.js: -------------------------------------------------------------------------------- 1 | const createPerformanceCalculator = require("./calculator"); 2 | 3 | 4 | module.exports = function (plays, invoice) { 5 | const result = {}; 6 | result.customer = invoice.customer; 7 | result.performances = invoice.performances.map(enrichPerformance); 8 | result.totalAmount = totalAmount(result); 9 | result.totalVolumeCredits = totalVolumeCredits(result); 10 | return result; 11 | 12 | function enrichPerformance(aPerformance) { 13 | const calculator = createPerformanceCalculator(aPerformance, playFor(aPerformance)); 14 | const result = Object.assign({}, aPerformance); 15 | result.play = calculator.play; 16 | result.amount = calculator.amount; 17 | result.volumeCredits = calculator.volumeCredits; 18 | return result; 19 | } 20 | function playFor(aPerformance) { 21 | return plays[aPerformance.playID]; 22 | } 23 | function totalAmount(data) { 24 | return data.performances.reduce((total, p) => total + p.amount, 0); 25 | } 26 | function totalVolumeCredits(data) { 27 | return data.performances.reduce((total, p) => total + p.volumeCredits, 0); 28 | } 29 | } -------------------------------------------------------------------------------- /code/js/result/src/formatter.js: -------------------------------------------------------------------------------- 1 | class TextFormatter { 2 | constructor() { 3 | this._result = ""; 4 | } 5 | 6 | get result(){ 7 | return this._result; 8 | } 9 | 10 | onTitle(customer) { 11 | this._result += `Statement for ${customer}\n`; 12 | } 13 | 14 | onPerformance(playName, audience, usdAmount) { 15 | this._result += ` ${playName}: ${usdAmount} (${audience} seats)\n`; 16 | } 17 | 18 | onSummary(usdTotalAmount, totalVolumeCredits) { 19 | this._result += `Amount owned is ${usdTotalAmount}\n`; 20 | this._result += `You earned ${totalVolumeCredits} credits\n`; 21 | } 22 | 23 | onPerformancesBegin() {} 24 | onPerformancesEnd() {} 25 | } 26 | 27 | class HtmlFormatter { 28 | constructor() { 29 | this._result = ""; 30 | } 31 | 32 | get result(){ 33 | return this._result; 34 | } 35 | 36 | onTitle(customer) { 37 | this._result += `

Statement for ${customer}

\n`; 38 | } 39 | 40 | onPerformance(playName, audience, usdAmount) { 41 | this._result += ` ${playName}${audience}`; 42 | this._result += `${usdAmount}\n`; 43 | } 44 | 45 | onSummary(usdTotalAmount, totalVolumeCredits) { 46 | this._result += `

Amount owed is ${usdTotalAmount}

\n`; 47 | this._result += `

You earned ${totalVolumeCredits} credits

\n`; 48 | } 49 | 50 | onPerformancesBegin() { 51 | this._result += "\n"; 52 | this._result += "\n"; 53 | } 54 | onPerformancesEnd() { 55 | this._result += "
playseatscost
\n"; 56 | } 57 | } 58 | 59 | module.exports.TextFormatter = TextFormatter; 60 | module.exports.HtmlFormatter = HtmlFormatter; 61 | -------------------------------------------------------------------------------- /code/js/result/src/index.js: -------------------------------------------------------------------------------- 1 | const createStatementData = require("./createStatementData"); 2 | const TextFormatter = require("./formatter").TextFormatter; 3 | const HtmlFormatter = require("./formatter").HtmlFormatter; 4 | 5 | function render(data, formatter) { 6 | formatter.onTitle(data.customer); 7 | formatter.onPerformancesBegin(); 8 | for (let perf of data.performances) { 9 | formatter.onPerformance(perf.play.name, perf.audience, usd(perf.amount)); 10 | } 11 | formatter.onPerformancesEnd(); 12 | formatter.onSummary(usd(data.totalAmount), data.totalVolumeCredits); 13 | return formatter.result; 14 | } 15 | 16 | function usd(aNumber) { 17 | return new Intl.NumberFormat("en-US", { 18 | style: "currency", currency: "USD", minimumFractionDigits: 2}).format(aNumber/100); 19 | } 20 | 21 | module.exports.statement = function (invoice, plays) { 22 | return render(createStatementData(plays, invoice), new TextFormatter()); 23 | } 24 | 25 | module.exports.htmlStatement = function (invoice, plays) { 26 | return render(createStatementData(plays, invoice), new HtmlFormatter()); 27 | } 28 | -------------------------------------------------------------------------------- /code/js/result/test/invoices.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "customer": "BigCo", 4 | "performances": [ 5 | { 6 | "playID": "hamlet", 7 | "audience": 55 8 | }, 9 | { 10 | "playID": "as-like", 11 | "audience": 35 12 | }, 13 | { 14 | "playID": "othello", 15 | "audience": 40 16 | } 17 | ] 18 | } 19 | ] -------------------------------------------------------------------------------- /code/js/result/test/plays.json: -------------------------------------------------------------------------------- 1 | { 2 | "hamlet": {"name": "Hamlet", "type": "tragedy"}, 3 | "as-like": {"name": "As You Like It", "type": "comedy"}, 4 | "othello": {"name": "Othello", "type": "tragedy"} 5 | } -------------------------------------------------------------------------------- /code/js/result/test/testHtmlStatement..js: -------------------------------------------------------------------------------- 1 | const assert = require('chai').assert; 2 | const htmlStatement = require('../src').htmlStatement; 3 | const plays = require('./plays.json'); 4 | const invoices = require('./invoices.json'); 5 | 6 | const expected = `

Statement for BigCo

7 | 8 | 9 | 10 | 11 | 12 |
playseatscost
Hamlet55$650.00
As You Like It35$580.00
Othello40$500.00
13 |

Amount owed is $1,730.00

14 |

You earned 47 credits

15 | `; 16 | 17 | describe('HtmlStatement', () => { 18 | describe('#htmlStatement()', () => { 19 | it('html text', function () { 20 | assert.equal(htmlStatement(invoices[0], plays), expected); 21 | }); 22 | }); 23 | }); -------------------------------------------------------------------------------- /code/js/result/test/testStatement.js: -------------------------------------------------------------------------------- 1 | const assert = require('chai').assert; 2 | const statement = require('../src').statement; 3 | const plays = require('./plays.json'); 4 | const invoices = require('./invoices.json'); 5 | 6 | const expected = `Statement for BigCo 7 | Hamlet: $650.00 (55 seats) 8 | As You Like It: $580.00 (35 seats) 9 | Othello: $500.00 (40 seats) 10 | Amount owned is $1,730.00 11 | You earned 47 credits 12 | `; 13 | 14 | describe('Statement', () => { 15 | describe('#statement()', () => { 16 | it('plain text', function () { 17 | assert.equal(statement(invoices[0], plays), expected); 18 | }); 19 | }); 20 | }); -------------------------------------------------------------------------------- /effective-refactoring-1.md: -------------------------------------------------------------------------------- 1 | # Effective Refactoring 1 2 | 3 | ## 引言 4 | 5 | Martin Fowler的《重构:改善既有代码的设计》一书从问世至今已有十几年时间了,按照计算机领域日新月异的变化速度,重构已经算是一门陈旧的技术了。但是陈旧并不代表不重要,恰恰随着演进式设计被越来越广泛的使用,重构技术已经被认为是现代软件开发中的一项必备的基本技能!所以今天在任何软件开发团队中,你都会不时听到或看到和重构相关的代码活动。然而对于这样一种被认为应该是如同“软件开发中的空气和水”一样的技术,在现实中却比比皆见对重构的错误理解和应用。首先是不知道重构使用的正确场合,总是等到代码已经腐化到积重难返的时候才想起重构;其次面对一堆的代码坏味道没有选择标准、无从下手;接下来修改代码的过程中不懂得安全、小步的重构手法,总是大刀阔斧地将代码置于危险的境地,很难再收回来;最后要么构建、测试失败后无法恢复只能推到重来,或者最终结果只是将代码从一种坏味道修改到了另一种坏味道! 6 | 7 | 总结以上问题,一部分原因是因为没有正确的理解重构,不知道重构的起点和目标,对重构的对象和目标没有衡量和比较的标准;其次是因为没有掌握形式化的重构手法和步骤,重构过程往往只是跟着感觉走;最后实践重构的过程中,没有先理顺自己的开发、构建和测试环境,导致重构成本很高! 8 | 9 | 本文站在作者多年来实践重构的基础上,为大家梳理重构技术,带领大家重新认识重构的目标和起点,重构手法背后的原理以及实践方式。最后会以`C/C++`语言为例,介绍在实践中高效实施重构的经验、技巧和工具。 10 | 11 | ## 什么是重构? 12 | 13 | ### 重构的定义 14 | 15 | Martin Fowler在《重构:改善既有代码的设计》一书中给出了重构的两个定义. 16 | 17 | 第一个是名词形式: 18 | > Refactoring: 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本. 19 | 20 | 第二个是动词形式: 21 | > Refactor: 使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构. 22 | 23 | ### 重构的目标 24 | 重构的目标是什么? 重构的目标绝不是将代码从别人的taste改成自己的taste,也不是将代码从一种坏味道改到另一种坏味道! 25 | 26 | Matin Fowler利用上面两个定义,指出了重构的目标: 27 | - 不改变软件可观察行为 28 | - 提高软件可理解性 29 | - 降低软件修改成本 30 | 31 | 而对于上述目标,我们再深入一点分析,发现其实已经有更经典的定义. 那就是Kent Beck的简单设计四原则: 32 | > - Pass All Test: 通过全部测试; 33 | > - No Duplication: 没有重复(DRY) 34 | > - Reveals Intent: 程序表达意图,易于理解 35 | > - Has no superfluous parts: 没有冗余,或者YAGNI原则 36 | > 37 | > 上述四条的重要程度依次降低. 38 | 39 | 到目前为止,简单设计四原则是对"什么是好的软件设计"最好的定义! 40 | 41 | 简单设计四原则第一条定义好的软件首先应该通过所有测试,即正确满足所有功能需求.而重构的目标中最基本的就是"不改变软件的可观察行为",也就是说: 42 | 1) **重构后的软件不能破坏原来所有测试!** 43 | 44 | Matin定义的重构的其它两条目标,对应了简单设计原则的第2和第3条: 45 | 2) **重构应该消除重复: 降低软件修改成本;** 46 | 3) **重构应该让程序显示表达意图: 提高软件可理解性;** 47 | 48 | 最后,我们把简单设计四原则的最后一条也加入重构的目标: 49 | 4) **重构应该消除冗余:降低软件不必要的复杂度**. 50 | 51 | 所以以后当我们再来讨论重构的目标,或者评判重构有没有收益的时候,就用简单设计四原则来衡量它. 52 | 53 | 54 | ### 从哪里开始? 55 | 对于重构的目标达成一致后,我们回到起点:什么样的软件需要重构? 以及什么时候进行重构? 56 | 57 | 对于第一个问题,由于我们重构的目标是使软件满足简单设计四原则,那么任何违反简单设计四原则的代码都应该是我们重构的目标.例如**1)代码很容易出现bug,导致测试失败! 或者 2)代码存在知识重复使得不易修改! 或者 3)代码写的晦涩非常难以理解! 或者 4)代码存在过度设计,存在冗余导致复杂! ** 58 | 59 | 现实中可能有一堆的代码问题等待我们解决,而时间、成本、人力是有限的,所以我们需要从最有价值,最没有争议的部分开始重构. 由于简单设计四原则的重要程度是依次降低的,对于四条原则的判定从上往下也是逐渐主观化,所以我们选择重构的代码的优先级顺序也是按照它们破坏简单四原则的顺序依次降低! 如果一坨代码存在很多重复,另外一坨代码不易理解,那么我们优先选择去解决重复代码的问题,因为按照简单四原则消除重复更重要,也更容易被客观评价. 60 | 61 | 在《重构》一书中Martin为了避免引起所谓编程美学的含混争辩,总结了代码的22条坏味道. 在实践中我们一般都是从某一代码坏味道着手重构的,但是对于优先重构哪个坏味道,我们遵守上面描述的原则. 62 | 63 | 对于进行重构的时机,Matin给出: 64 | - 重复地做某一件事情的时候 (三次法则) 65 | - 添加新功能的时候 66 | - 修改Bug的时候 67 | - Code Review的时候 68 | 69 | 事实上在我的工作过程中,重构是随时随地进行的. 尤其对于采用演进式设计方法论,重构和代码开发是紧密结合难以分割的,甚至很多时候只有依托重构才能完成代码的开发. 70 | 71 | 72 | ### 重构的手法 73 | 74 | 明白了起点和目标,下来最重要的就是掌握完成这一过程的手段! 而重构的手法则是带领我们正确到达目标的工具. 75 | 76 | 很多人认为学习重构只要掌握背后的思想就足够了,其详细繁琐的操作手法并不重要.于是乎现实中我们看到很多人在实际操作重构的过程中章法全无,一旦开始半天停不下来,代码很多时候处于不可编译或者测试不能通过的状态,有时改的出错了很难再使代码回到初始状态,只能推倒重来! 实际上重构是一项非常实践性的技术,能够正确合理地使用重构操作,安全地,小步地,高效地完成代码修改,是评价重构能力的核心标准. 77 | 78 | 那么什么才是正确的重构手法? 79 | 80 | Martin对重构的第二个定义中提到`使用一系列的重构手法`,但是对这一系列的重构手法却没有概括. 81 | 82 | 而William Opdyke在他的论文"Refactoring Objected-Oriented Frameworks"里面对重构给出了如下定义: 83 | > 重构:行为保持(Behavior Preservation)的程序重建和程序变换. 84 | 85 | 在论文里面将重构手法定义为一些程序重建或者程序变换的操作,这些操作满足行为保持(Behavior Preservation)的要求. 论文里面对行为保持的定义如下: 86 | > Behavior Preservation : For the same set of input values,the resulting set of output values should be the same before and after the refactoring. 87 | 88 | 也就是说存在一系列代码变换的操作,应用这些操作之后,在相同的输入条件下,软件的输出不会发生变化. 我们把满足上述要求的代码操作称之为代码等价变换操作. 在William Opdyke的论文中针对C++提出了26种低层次的代码等价变换操作(例如: 重命名变量,为函数增加一个参数,删除一个不被引用的类...). 按照一定设计好的顺序组合上述低层次的代码等价变换操作,我们可以完成一次安全的代码重构,保证代码重构前后的行为保持要求. 89 | 90 | 这里代码等价变换的过程. 类似于初等数学中的多项式变换.例如对于如下公式变化: 91 | 92 | ![](./pics/formular.png) 93 | 94 | 每一步我们运用一次多项式等价变换公式,一步一步地对多项式进行化简,每次变换前后多项式保持等价关系. 95 | 96 | 在多项式化简的这个例子中,承载简化过程的是已经被数学证明过的多项式等价变换的公式. 同理承载重构的则是被证明过的一个个代表代码等价变换操作的重构手法. 97 | 98 | 另外,由于完成一项重构需要使用一系列的重构手法,这些手法的使用顺序也是至关重要的! 99 | 100 | 我们学习重构,就是要来学习每种场景下所使用的小步安全的重构手法及其使用顺序,并不断加以练习! 能够灵活而流畅地使用一系列重构手法完成一项重构,是衡量重构能力的一个非常重要的指标. 101 | 102 | 而本文后面的一个重点就是对常用的重构手法以及运用顺序进行提炼,降低大家的学习难度. 103 | 104 | 最后,既然重构中使用的是安全小步的代码等价变换手法,为什么我们还需要测试? 首先是因为我们是人,我们总会犯错! 另外由于编程语言的复杂性导致所谓的等价变换是受上下文约束的,例如在C++中为一个存在继承关系的类的成员方法重命名,有可能导致新的方法名和它某一父类中有默认实现的虚方法重名,而即使编译器也不能发现该错误. 105 | 106 | ### 高效地重构 107 | 虽然我们了解了如何/何时开始,目标,以及重构的手法,但是如果我们有了下面这些因素的辅助,会让我们更加安全和高效. 108 | - 覆盖良好\高效的自动化测试 109 | - 合适的IDE,最好提供基本的自动化重构菜单 110 | - 良好的工程设置 111 | - 高效的构建环境 112 | - 良好的编码习惯 113 | 114 | 对于上面这些,不同语言面临的现状不同,针对C++语言我们后面会专门总结. 115 | 116 | ### 哪些不是重构? 117 | 针对上面的讨论,我们站在严格的重构定义上来看看下面这些反模式: 118 | - "我把bug重构掉了!" 119 | - "Debug一下刚才的重构那里出错了" 120 | - "昨晚重构出来的Bug到现在还没有查出来" 121 | - "先把代码重构好,再看测试为啥不过" 122 | - "我把软件架构由集中式重构成分布式了" 123 | 124 | 想想上面的场景哪里存在问题? 125 | 126 | 在实际的开发过程中,我们还经常面临另外一种场景,那就是对某一已经开发完成的软件模块进行整体重构. 在这样的过程中,虽然也存在频繁地使用重构手法对原有模块代码进行修改,但是更多的是进行大量的架构和设计方案上的修改.为了与我们要讨论的重构进行区分,对于这样的过程,我们称其为reengineering(软件重建). 127 | 128 | 软件重建一般是站在之前开发、测试的基础上,伴随着对软件要解决的问题和解决方式本身有了更深入的理解,通过修改软件把这些学习成果反映到软件的结构中去,使得软件可以更好、更精炼的解决业务问题。站在DDD(领域驱动设计)的角度,软件重建一般是对领域模型的进一步精练,使得软件更加贴合业务的本质!虽然成功的软件重建往往能对组织带来较大的收益,但是由于软件重建的开销普遍较大,而软件开发又是一项商业活动,所以需要对软件重建谨慎评估其成本收益比以及过程风险后才能决定是否启动。而本文中的重构技术,则只是一项日常编码中频繁使用的安全、高效的代码修改技术,被普遍认为是现代软件开发技术中必备的一项基本技能,是演进式软件设计或者软件重建目标达成的一项必要手段! 129 | 130 | ### 关于本文 131 | 我们总结一下,重构有三个要点,见下图: 132 | 133 | ![](./pics/3factor.jpeg) 134 | 135 | 1. 你要有一个敏感的鼻子,能够嗅出代码中的坏味道; 一般只要发现不符合简单设计四原则的Code,就是我们需要重构的目标对象. 而Martin总结的22条代码坏味道给我们一个很好的实践起点. 136 | 2. 你要知道重构的目标,就是让代码逐渐靠近简单设计四原则. 137 | 3. 需要掌握小的安全的重构手法,以及在不同场景下合理的使用顺序,以便安全高效地承载重构目标的达成. 138 | 139 | 由于重构手法和实施顺序是学习重构的关键,所以本文后面会重点讲述这个主题. 另外,在实践中如何高效和安全的进行重构,和具体使用的编程语言及其开发、构建、测试环境关系也很密切.本文最后会以C++语言为例总结这方面相关问题. 140 | 141 | --- 142 | 143 | > - 作者:王博 144 | > - Email:e.wangbo@gmail.com 145 | > - Github:https://github.com/MagicBowen 146 | > - 个人简书主页:https://www.jianshu.com/u/92b7d9879f20 147 | > 148 | > - **转载请注明作者信息,谢谢!** 149 | -------------------------------------------------------------------------------- /effective-refactoring-2.md: -------------------------------------------------------------------------------- 1 | # Effective Refactoring 2 2 | 3 | ## 如何实施重构 4 | 5 | 稍微复杂的重构过程,都是由一系列的基本重构手法组成. 《重构》一书中针对各种重构场景,给出了大量的重构手法.这些手法有的复杂,有的简单,如果不加以系统化的整理和提炼,很容易迷失在细节中. 6 | 另外,在不同场景下重构手法的使用是非常讲究其顺序的.一旦顺序不当,很容易让重构失去安全性,或者干脆让某些重构变得很难完成. 7 | 本节是个人对重构手法的整理和提炼,帮助大家跳出细节,快速掌握重要的重构手法并且能够尽快在自己的重构实践中进行使用.随后我们整理了重构手法应用顺序的背后思想,帮助大家避免死记硬背,可以根据自己的重构场景推导出合理的重构顺序. 8 | 9 | ### 基本手法 10 | 11 | 根据2-8原则,我们平时80%的工作场景中只使用到20%的基本重构手法. 而往往复杂的重构手法,也都是由一些小的基本手法组合而成. 熟练掌握基本手法,就能够完成绝大多数重构任务. 再按照一定顺序对其加以组合,就能够完成大多数的复杂重构. 12 | 13 | 经过对《重构》一书中的所有重构手法进行分析,结合日常工作中的使用情况,我们认为以下几类重构手法为基本手法: 14 | - 重命名 (rename) 15 | - 提炼 (extract) 16 | - 内联 (inline) 17 | - 移动 (move) 18 | 19 | 以上每一类的命名皆是动词,其宾语可以是变量,函数,类型。对于某些语言,例如C/C++,还应该再包含文件(特质物理重构). 20 | 例如重命名,包含重命名变量,重命名函数,重命名类,以及重命名文件,它们皆为基本重构手法,都属于重命名这一类. 21 | 22 | 其它所有的重构手法大多数都是上述基本手法的简单变异,或者干脆由一系列基本手法组成. 23 | 例如常用的`Self Encapsulate Field(自封装字段)`,本质上就是简化版的`Extract Method`. 24 | 再例如稍微复杂的`Replace Condition with Polymorphism(以多态取代条件表达式)`,就是由`Extract Method` 和 `Move Method`组成的. 25 | 26 | 所以我们学习重构手法,只要能够熟练掌握上面四类基本手法,就可以满足日常绝大多数重构场景.通过对基本重构手法的组合,我们就能完成复杂的重构场景. 27 | 28 | ### 原子步骤 29 | 在我们提炼出了上述四类基本手法后,我们还是想问,既然重构手法都是代码等价变化操作,它们背后是否存在哪些共性的东西? 因为即使是四类基本手法,展开后也包含了不少手法,而且要去死记硬背每种手法的具体操作过程,也是相当恼人的. 30 | 事实上每种重构手法为了保证代码的等价变化,必须是安全且小步的,其背后的操作步骤都是相似的.我们对组成每种基本重构手法的步骤加以整理和提炼,形成一些原子步骤.一项基本重构手法是由原子步骤组成的.每一个原子步骤实施之后我们保证代码的功能等价性. 31 | 32 | 我们可以认为,基本上重构手法都是由以下两个有序的原子步骤组成: 33 | 1. **setup** 34 | - 根据需要创建一个新的代码元素. 例如:变量,函数,类,或者文件 35 | - 新创建的代码元素需要有好的名称,更合适的位置和访问性.更好体现出设计意图 36 | - 新的代码元素的实现,可以**copy**原有代码,在copy过来的代码基础之上进行修改.(注意是**copy**) 37 | - 这一原子步骤的操作过程中,不会对原有代码元素进行任何修改. 38 | - 这一过程的安全性只需要**编译**的保证 39 | 40 | 2. **substitute** 41 | - 将原子步骤1中新创建的代码元素替换回原有代码 42 | - 这一过程需要搜索待替换元素在老代码中的所有引用点 43 | - 对引用点进行逐一替换; 一些场景下为了方便替换,需要先创建引用"锚点" 44 | - 这一过程是一个修改源代码的过程,所以每一次替换之后,都应该由**测试**来保证安全性 45 | 46 | 原子步骤1,2的交替进行,可以完成一项基本重构或者复杂重构. 在这里1和2可以称之为原子步骤,除了因为大多数的重构手法可以拆解成这两个原子步骤.更是因为每个原子步骤也是一项代码的等价变换(只是层次更低),严苛条件下我们可以按照原子步骤的粒度进行代码的提交或者回滚.然而我们之所以不把原子步骤叫做手法,是因为原子步骤的单独完成往往不能独立达成一项重构目标. 灵活掌握了原子步骤的应用,我们除了不用死记硬背每种重构手法背后的繁琐步骤,更可以使自己的重构过程更安全和小步,做到更小粒度的提交和回滚,快速恢复代码到可用状态. 47 | 48 | 以下以两段C++代码做示例,展示如何应用原子步骤完成基本重构手法: 49 | 1. 重命名变量(Rename Variable) 50 | ~~~cpp 51 | unsigned int start = Date::getTime(); 52 | // load program ... 53 | unsigned int offset = Date::getTime() - start; 54 | cout << "Load time was: " << offset/1000 << "seconds" << endl; 55 | ~~~ 56 | 在上面的示例代码中,变量`offset`的含义太过宽泛,我们将其重命名为`elapsed`,第一步我们执行原子步骤setup,创建一个新的变量`eclapsed`,并且将其初始化为`offset`.在这里为了更好的体现设计意图,我们将其定义为const. 57 | ~~~cpp 58 | unsigned int start = Date::getTime(); 59 | // load program ... 60 | unsigned int offset = Date::getTime() - start; 61 | const unsigned int elapsed = offset; 62 | cout << "Load time was: " << offset/1000 << "seconds" << endl; 63 | ~~~ 64 | 经过这一步,我们完成了原子步骤1. 在这个过程中,我们只是增加了新的代码元素,并没有修改原有代码.新增加的代码元素体现了更好的设计意图. 最后我们编译现有代码,保证这一过程的安全性. 65 | 接下来我们进行原子步骤substitute,首先找到待替换代码元素的所有引用点.对于我们的例子就是所有使用变量offset的地方.对于每个引用点逐一进行替换和测试. 66 | ~~~cpp 67 | unsigned int start = Date::getTime(); 68 | // load program ... 69 | unsigned int offset = Date::getTime() - start; 70 | const unsigned int elapsed = offset; 71 | cout << "Load time was: " << elapsed/1000 << "seconds" << endl; 72 | ~~~ 73 | 最后别忘了变量定义之处的替换: 74 | ~~~cpp 75 | unsigned int start = Date::getTime(); 76 | // load program ... 77 | const unsigned int elapsed = Date::getTime() - start; 78 | cout << "Load time was: " << elapsed/1000 << "seconds" << endl; 79 | ~~~ 80 | 每一次替换之后都需要运行测试,保证对源代码修改的安全性. 81 | 82 | 在上述例子中,对于变量start和elapsed可以有更好的命名,这两个变量最好能够体现其代表时间的单位,例如可以叫做 startMs以及elapsedMs,大家可以自行练习替换. 另外程序中存在魔术数字1000,可以自行尝试用原子步骤进行`extract variable`重构手法,完成用变量对1000的替换. 83 | 84 | 2. 提炼函数 (Extract Method) 85 | ~~~cpp 86 | void printAmount(const Items& items) 87 | { 88 | int sum = 0; 89 | 90 | for(auto item : items) 91 | { 92 | sum += item.getValue(); 93 | } 94 | 95 | cout << "The amount of items is " << sum << endl; 96 | } 97 | ~~~ 98 | 99 | 上述函数完成了两件事,首先统计一个items的集合的所有元素value的总和,然后对总和进行打印. 100 | 为了把统计和打印职责分开,我们提炼一个函数`calcAmount`用来专门对一个给定的Items集合求总和.为了完成Extract Method重构手法,我们首先使用原子步骤setup. 101 | 102 | 首先建立`calcAmount`函数的原型, 103 | ~~~cpp 104 | int calcAmount(const Items& items) 105 | { 106 | return 0; 107 | } 108 | 109 | void printAmount(const Items& items) 110 | { 111 | int sum = 0; 112 | 113 | for(auto item : items) 114 | { 115 | sum += item.getValue(); 116 | } 117 | 118 | cout << "The amount of items is " << sum << endl; 119 | } 120 | ~~~ 121 | 接下来完成`calcAmount`函数的实现.这一步需要将源函数中相关部分copy到`calcAmount`中并稍加修改.切记由于原子步骤1中不能修改源代码,所以这里千万不要用剪切,否则一旦重构出错,是很难快速将代码回滚到正确的状态的,这点新手尤其需要注意! 122 | ~~~cpp 123 | int calcAmount(const Items& items) 124 | { 125 | int sum = 0; 126 | 127 | for(auto item : items) 128 | { 129 | sum += item.getValue(); 130 | } 131 | 132 | return sum; 133 | } 134 | 135 | void printAmount(const Items& items) 136 | { 137 | int sum = 0; 138 | 139 | for(auto item : items) 140 | { 141 | sum += item.getValue(); 142 | } 143 | 144 | cout << "The amount of items is " << sum << endl; 145 | } 146 | ~~~ 147 | 148 | 到目前为止,原子步骤1就已經OK了,我们运行编译,保证新增加的代码元素是可用的. 149 | 接下来我们进行原子步骤substitute.将新函数`calcAmount`替换到每一个对Items计算总量的地方.对于我们的例子,只有一个地方就是`printAmount`函数(相信对于真实代码,这类对Items求总量的计算会到处都是,写法各异). 150 | ~~~cpp 151 | int calcAmount(const Items& items) 152 | { 153 | int sum = 0; 154 | 155 | for(auto item : items) 156 | { 157 | sum += item.getValue(); 158 | } 159 | 160 | return sum; 161 | } 162 | 163 | void printAmount(const Items& items) 164 | { 165 | cout << "The amount of items is " << calcAmount(items) << endl; 166 | } 167 | ~~~ 168 | 替换之后运行测试.到目前为止我们的Extract Method已经完成了. 169 | 170 | 如果更进一步,我们发现可以运用基本重构手法Move Method将`calcAmount`函数移入到Items类中,然后使用Rename Method手法将其重命名为`getAmount`会更好. 对于Move Method和Rename Method大家可以发现,它们都是由我们总结的原子步骤组成. 例如Move Mehod,我们首先应用原子步骤setup,在Items类中创建public成员方法`calcAmount`,然后将函数的具体实现copy过去修改好,保证编译OK. 接下来应用原子步骤substitute,用新创建的Items成员函数替换老的CalcAmount,测试OK后,我们就完成了Move Method重构. 171 | 172 | 重构的最终效果如下,大家可自行练习. 173 | ~~~cpp 174 | struct Items 175 | { 176 | int getAmount() const; 177 | ... 178 | }; 179 | 180 | void printAmount(const Items& items) 181 | { 182 | cout << "The amount of items is " << items.getAmount() << endl; 183 | } 184 | ~~~ 185 | 在这里我们没有将`printAmount`也移入到Items中,是因为`getAmount`作为Items的接口是稳定的.但是如何打印往往是表示层关注的,各种场景下打印格式各异,所以没有将其移入Items中. 186 | 187 | 通过上面的示例,我们演示了如何用原子步骤组合出基本的重构手法. 实际上,对于所有的rename和普通的extract重构,一般的`C++ IDE`都提供了直接的自动化重构快捷键供我们使用,平时开发直接使用重构快捷键即高效又安全.但是这并不影响我们掌握原子步骤的使用.由于`C++`语言的复杂性,大多数重构手法都是没有自动化重构快捷键支持的,即便有重构快捷键支持,一旦上下文稍微复杂一点(例如对有很多临时变量的函数执行Extract Method),自动化重构的结果也往往不能让人满意. 这里不仅对C++语言,对于一些动态类型语言(例如ruby),自动化重构更是匮乏.所以我们要掌握重构手法背后的思想,熟练掌握原子步骤,学会安全高效地手动重构. 188 | 189 | 这里总结的原子步骤是非常普适的! 不仅我们列举出的基本重构手法都是由原子步骤组成.对于许多复杂的重构手法,除了会直接使用基本重构手法,甚至也会直接使用原子步骤. 190 | 例如对于Martin描述的手法"Replace Type Code with Class(以类取代类型码)",里面基本是在反复使用原子步骤,我们摘录原书中的操作描述: 191 | 192 | > 1. 为类型码建立一个类 193 | > 2. 修改源类的实现,让它使用新建的类 194 | > 3. 编译,测试 195 | > 4. 对于源类中每一个使用类型码的函数,相应建立一个函数,让新函数使用新建的类 196 | > 5. 逐一修改源类用户,让它们使用新接口 197 | > 6. 每修改一个用户,编译并测试 198 | > 7. 删除使用类型码的旧接口,并删除保存旧类型码的静态变量 199 | > 8. 编译,测试 200 | 201 | 对于该重构,其中步骤1是原子步骤setup,步骤2是原子步骤substitute(第3步是对第2步的编译和测试,对第1步的编译过程作者省略了). 然后第4步又是setup,第5步开始执行substitute. 每一次按照原子步骤的要求进行执行,都可以保证重构是安全的,甚至严苛条件下,代码可以按照每一次原子步骤进行提交或者回滚. 另外对于每一种重构手法的操作描述,如果把它们统一成对原子步骤的组合的描述,也会极大的方便记忆. 掌握了原子步骤,即使忘记了某一项重构手法的具体操作,也可以方便的自行推导出来. 202 | 203 | ### 锚点的使用 204 | 在前面介绍原子步骤substitute的时候提到,为了方便替换,可以使用引用锚点. 205 | 对于重构,最容易出错的地方就在替换.可以借助IDE帮助自动搜索到对旧码元素的所有的引用点.但是搜索的质量往往和IDE以及语言特性相关.例如对于C++宏内代码元素的搜索,IDE就很难搜索准确. 另外对于引用点很多的情况,逐一替换/测试也是相当累人的. 206 | 所谓**锚点,就是先增加一个中间层,把所有对旧代码元素的引用汇聚到一点,编译测试过后,然后在这一个点完成新旧代码元素的统一替换.** 207 | 完成替换后,可以保留锚点,或者用inline重构手法再去掉锚点. 208 | 209 | 有两类手法经常被用来创建锚点,`Encapsulate field(自封装字段)`和`Replace Constructor with Factory Method(以工厂函数取代构造函数)`。 210 | 211 | Encapsulate field一般在修改类的某一成员字段的实现方式的重构场景下使用。例如:`Move Field(在类间搬移字段)`和`Replace Type Code with Subclasses(以子类取代类型码)`。Encapsulate field对于欲修改字段创建引用锚点,将类内部对该成员字段的使用汇聚到一起,方便对字段的实现方式进行替换。它的操作方式也是由我们前面介绍的两个原子步骤组成: 212 | - Setup:在类内创建两个方法分别对于要修改字段的读取和设值(就是面向对象初学者最爱写的`get`和`set`成员函数)。编译。 213 | - Substitute:将类内所有直接使用字段的地方替换为调用对应的函数. 读取的地方替换为调用`get`,设值的地方替换为调用`set`。执行测试! 214 | 215 | 当执行完Encapsulate field后,类内再无对欲修改字段的直接使用了,这时就可以方便地在`get`和`set`方法内对于字段的实现方式进行修改了。 216 | 217 | 例如对于"Move Field(在类间搬移字段)"重构操作,首先就需要在源类内使用Encapsulate field对需要搬移的字段创建引用锚点. 当执行完Encapsulate field后,源类内再无对欲搬移字段的直接使用了,这个时候再在目标类中创建对应字段,将源类内`get`和`set`方法内对于自身字段的使用修改为使用目标类中的字段. 测试通过后,再删除源类内的搬移字段. 最后对于源类内提取出来的`get`和`set`方法可以再inline回去. 218 | 219 | 可以看到锚点将引用点汇聚到一处,每个客户都调用一个中间层,不再面对具体的待替换代码细节. 锚点的使用简化了替换过程,并且可以让替换更加安全. 220 | 221 | 在某些场合下使用锚点还有更重要的意义,一些重构手法必须借助锚点才能完成,尤其是对一些需要子类化的重构! 例如对于 `Replace Constructor with Factory Method(以工厂函数取代构造函数)`,在该重构手法里面,工厂函数就是锚点,它将类的创建汇聚到工厂函数里面,对客户代码隐藏了类的构造细节,后面如果进行某些子类化的重构就非常容易实施. 222 | 223 | 下面我们以一个例子作为对原子步骤和锚点的总结。 224 | ~~~cpp 225 | // Shoes.h 226 | enum Type 227 | { 228 | REGULAR, 229 | NEW_STYLE, 230 | LIMITED_EDITION 231 | }; 232 | 233 | struct Shoes 234 | { 235 | Shoes(Type type, double price); 236 | double getCharge(int quantity) const; 237 | 238 | private: 239 | Type type; 240 | double price; 241 | }; 242 | ~~~ 243 | ~~~cpp 244 | // Shoes.cpp 245 | #include "Shoes.h" 246 | 247 | Shoes::Shoes(Type type, double price) 248 | : type(type), price(price) 249 | { 250 | } 251 | 252 | double Shoes::getCharge(int quantity) const 253 | { 254 | double result = 0; 255 | 256 | switch(type) 257 | { 258 | case REGULAR: 259 | result += price * quantity; 260 | break; 261 | 262 | case NEW_STYLE: 263 | if(quantity > 1) 264 | { 265 | result += (price + (quantity - 1) * price * 0.8); 266 | } 267 | else 268 | { 269 | result += price; 270 | } 271 | 272 | break; 273 | 274 | case LIMITED_EDITION: 275 | result += price * quantity * 1.1; 276 | break; 277 | } 278 | 279 | return result; 280 | } 281 | ~~~ 282 | 283 | 以上代码中有一个`Shoes`类,它的`type`字段指明一个`Shoes`对象的具体类型:`REGULAR`、`NEW_STYLE`或者`LIMITED_EDITION`。`Shoes`类的接口`getCharge`根据传入的数量`quantity`来计算总费用。`getCharge`根据不同类型按照不同方法进行计算。对于普通款(REGULAR),总价等于单价乘以数量;对于新款(NEW_STYLE),从第二双开始打八折;对于限量版(LIMITED_EDITION),每一双需要多收10%的费用。 284 | 285 | 由于`Shoes`类的对象在其生命周期中`type`不会发生变化,所以可以为不同的类型码建立`Shoes`类型的子类,将不同`type`的计算行为放到相应子类中去,这样代码就可以满足开放封闭性,以后再增加新类型的`Shoes`,只用独立地再增加一个子类,不会干扰到别的类型的计算。 286 | 于是我们决定使用重构手法`Replace type code with subclasses(以子类取代类型码)`来完成重构目标。(注意:倘若在`Shoes`的对象生命周期内`type`可以变化,就不能使用该重构手法,而应该使用`Replace type code with strategy/state(以策略或者状态模式取代类型码)`)。 287 | 288 | 以下是具体的重构过程,我们着重展示如何使用原子步骤以及锚点。 289 | 1. 由于我们要以子类取代类型码`type`,所以首先使用Encapsulate field对`type`创建引用锚点,方便后面对`type`的替换。 290 | ~~~cpp 291 | Type Shoes::getType() const 292 | { 293 | return type; 294 | } 295 | 296 | double Shoes::getCharge(int quantity) const 297 | { 298 | double result = 0; 299 | 300 | switch(getType()) 301 | { 302 | case REGULAR: 303 | result += price * quantity; 304 | break; 305 | 306 | case NEW_STYLE: 307 | if (quantity > 1) 308 | { 309 | result += (price + (quantity - 1) * price * 0.8); 310 | } 311 | else 312 | { 313 | result += price; 314 | } 315 | 316 | break; 317 | 318 | case LIMITED_EDITION: 319 | result += price * quantity * 1.1; 320 | break; 321 | } 322 | 323 | return result; 324 | } 325 | ~~~ 326 | 上面我们将`getCharge`中对`type`的直接使用替换为调用新创建的私有成员方法`getType()`。到最后`Shoes`内只有构造函数中仍然直接使用`type`,接下来再处理构造函数。 327 | 2. 为了屏蔽`Shoes`类型被子类化后对客户代码的影响,我们对`Shoes`创建工厂函数作为构造函数的引用锚点,用来对客户屏蔽不同种类`Shoes`的具体构造。首先执行原子步骤setup,创建出工厂函数。 328 | ~~~cpp 329 | struct Shoes 330 | { 331 | static Shoes* create(Type type, double price); 332 | 333 | Shoes(Type type, double price); 334 | double getCharge(int quantity) const; 335 | 336 | private: 337 | Type getType() const; 338 | 339 | private: 340 | Type type; 341 | double price; 342 | }; 343 | ~~~ 344 | ~~~cpp 345 | Shoes* Shoes::create(Type type, double price) 346 | { 347 | return new Shoes(type, price); 348 | } 349 | ~~~ 350 | 编译通过后,接下来执行原子步骤substitute。将原来客户代码直接调用`Shoes`构造函数的地方替换为调用工厂函数。替换完成后将`Shoes`的构造函数修改为`protected`(子类要用)。执行测试! 351 | ~~~cpp 352 | struct Shoes 353 | { 354 | static Shoes* create(Type type, double price); 355 | 356 | double getCharge(int quantity) const; 357 | 358 | protected: 359 | Shoes(Type type, double price); 360 | 361 | private: 362 | Type getType() const; 363 | 364 | private: 365 | Type type; 366 | double price; 367 | }; 368 | ~~~ 369 | ~~~cpp 370 | // client code 371 | 372 | // Shoes* shoes = new Shoes(REGULAR, 100.0); 373 | Shoes* shoes = Shoes::create(REGULAR, 100.0); 374 | ~~~ 375 | 为了简化,这里假设客户代码需要对创建出来的`Shoes`对象进行显示内存管理。 376 | 377 | 至此内外部锚点都已经创建OK。内部锚点`getType`在类内屏蔽对`type`的直接使用,方便后续以子类对`type`进行替换。外部锚点`create`向客户隐藏`Shoes`的具体构造,方便以子类的构造替换具体类型`Shoes`的构造。 378 | 3. 接下来,我们逐一创建`Shoes`的子类,用于对`Shoes`中类型码的替换。 379 | 首先将`Shoes`内的`getType`函数修改为虚方法。然后执行原子步骤setup,创建类`RegularShoes`继承自`Shoes`,它覆写了`getType`方法,返回对应的类型码。 380 | ~~~cpp 381 | struct RegularShoes : Shoes 382 | { 383 | RegularShoes(double price); 384 | 385 | private: 386 | Type getType() const override; 387 | }; 388 | ~~~ 389 | ~~~cpp 390 | RegularShoes::RegularShoes(double price) 391 | : Shoes(REGULAR, price) 392 | { 393 | } 394 | 395 | Type RegularShoes::getType() const 396 | { 397 | return REGULAR; 398 | } 399 | ~~~ 400 | 下面执行原子步骤substitute,用`RegularShoes`替换`Shoes`的构造函数中对于`REGULAR`类型的构造。 401 | ~~~cpp 402 | Shoes* Shoes::create(Type type, double price) 403 | { 404 | if(type == REGULAR) return new RegularShoes(price); 405 | return new Shoes(type, price); 406 | } 407 | ~~~ 408 | 同样的方式创建`NewStyleShoes`和`LimitedEditionShoes`,并替换进工厂函数中。 409 | ~~~cpp 410 | NewStyleShoes::NewStyleShoes(double price) 411 | : Shoes(NEW_STYLE, price) 412 | { 413 | } 414 | 415 | Type NewStyleShoes::getType() const 416 | { 417 | return NEW_STYLE; 418 | } 419 | ~~~ 420 | ~~~cpp 421 | LimitedEditionShoes::LimitedEditionShoes(double price) 422 | : Shoes(LIMITED_EDITION, price) 423 | { 424 | } 425 | 426 | Type LimitedEditionShoes::getType() const 427 | { 428 | return LIMITED_EDITION; 429 | } 430 | ~~~ 431 | ~~~cpp 432 | Shoes* Shoes::create(Type type, double price) 433 | { 434 | switch(type) 435 | { 436 | case REGULAR: 437 | return new RegularShoes(price); 438 | case NEW_STYLE: 439 | return new NewStyleShoes(price); 440 | case LIMITED_EDITION: 441 | return new LimitedEditionShoes(price); 442 | } 443 | 444 | return nullptr; 445 | } 446 | ~~~ 447 | 在`Shoes::create`方法中,类型都不匹配的情况下返回了`nullptr`,当然你也可以创建`Shoes`的一个`NullObject`在此返回。 448 | 449 | 至此,`Shoes`中就不再需要类型码`type`了。为了安全的删除,我们执行原子步骤setup,先为`Shoes`添加一个无需`type`参数的构造函数`Shoes(double price)`,然后执行原子步骤substitute,将子类中调用的`Shoes(Type type, double price)`全部替换掉。在这里为了避免子类间构造函数的重复,我们使用了C++11的继承构造函数特性。编译测试通过后,我们可以安全地将`type`和`Shoes(Type type, double price)`一起删除,同时将`Shoes`中的`getType`修改为纯虚函数,删除其在cpp文件中的函数实现。 450 | ~~~cpp 451 | struct Shoes 452 | { 453 | static Shoes* create(Type type, double price); 454 | 455 | Shoes(double price); 456 | virtual ~Shoes(){} 457 | double getCharge(int quantity) const; 458 | 459 | private: 460 | virtual Type getType() const = 0; 461 | 462 | private: 463 | double price; 464 | }; 465 | ~~~ 466 | ~~~cpp 467 | struct RegularShoes : Shoes 468 | { 469 | using Shoes::Shoes; 470 | 471 | private: 472 | Type getType() const override; 473 | }; 474 | ~~~ 475 | 4. 重构到现在,我们已经成功地用子类替换掉了类型码。但是这不是我们的目的,我们最终希望能够把`getCharge`中的计算行为分解到对应子类中去。这就是`Replace Condition with Polymorphism(以多态取代条件表达式)`。下面我们以原子步骤的方式完成它。 476 | 首先将`getCharge`声明为虚方法。然后使用原子步骤setup在子类中创建`getCharge`的覆写函数,将对应子类的计算部分copy过去。这里为了能够编译通过,需要将`Shoes`中的`price`成员变量修改为`protected`。 477 | ~~~cpp 478 | struct Shoes 479 | { 480 | static Shoes* create(Type type, double price); 481 | 482 | Shoes(double price); 483 | virtual ~Shoes(){} 484 | 485 | virtual double getCharge(int quantity) const; 486 | 487 | private: 488 | virtual Type getType() const = 0; 489 | 490 | protected: 491 | double price; 492 | }; 493 | ~~~ 494 | ~~~cpp 495 | struct RegularShoes : Shoes 496 | { 497 | using Shoes::Shoes; 498 | 499 | private: 500 | double getCharge(int quantity) const override; 501 | Type getType() const override; 502 | }; 503 | ~~~ 504 | ~~~cpp 505 | double RegularShoes::getCharge(int quantity) const 506 | { 507 | return price * quantity; 508 | } 509 | ~~~ 510 | 然后执行原子步骤substitute,用子类的`getCharge`对父类中的实现进行替换。这里只用删除`Shoes::getCharge`中对应`REGULAR`的分支。执行测试。 511 | ~~~cpp 512 | double Shoes::getCharge(int quantity) const 513 | { 514 | double result = 0; 515 | 516 | switch(getType()) 517 | { 518 | case NEW_STYLE: 519 | if (quantity > 1) 520 | { 521 | result += (price + (quantity - 1) * price * 0.8); 522 | } 523 | else 524 | { 525 | result += price; 526 | } 527 | 528 | break; 529 | 530 | case LIMITED_EDITION: 531 | result += price * quantity * 1.1; 532 | break; 533 | } 534 | 535 | return result; 536 | } 537 | ~~~ 538 | 同样的方式,将`Shoes::getCharge`中其余部分分别挪入到另外两个子类中,完成对`Shoes::getCharge`的替换,最后删除`Shoes::getCharge`的实现部分,将其声明为纯虚函数。这时继承体系上的`getType`也不再需要了,一起删除。 539 | ~~~cpp 540 | struct Shoes 541 | { 542 | static Shoes* create(Type type, double price); 543 | 544 | Shoes(double price); 545 | virtual ~Shoes(){} 546 | virtual double getCharge(int quantity) const = 0; 547 | 548 | protected: 549 | double price; 550 | }; 551 | ~~~ 552 | ~~~cpp 553 | Shoes::Shoes(double price) 554 | : price(price) 555 | { 556 | } 557 | 558 | Shoes* Shoes::create(Type type, double price) 559 | { 560 | switch(type) 561 | { 562 | case REGULAR: 563 | return new RegularShoes(price); 564 | case NEW_STYLE: 565 | return new NewStyleShoes(price); 566 | case LIMITED_EDITION: 567 | return new LimitedEditionShoes(price); 568 | } 569 | 570 | return nullptr; 571 | } 572 | ~~~ 573 | ~~~cpp 574 | struct RegularShoes : Shoes 575 | { 576 | using Shoes::Shoes; 577 | 578 | private: 579 | double getCharge(int quantity) const override; 580 | }; 581 | 582 | double RegularShoes::getCharge(int quantity) const 583 | { 584 | return price * quantity; 585 | } 586 | ~~~ 587 | ~~~cpp 588 | struct NewStyleShoes : Shoes 589 | { 590 | using Shoes::Shoes; 591 | 592 | private: 593 | double getCharge(int quantity) const override; 594 | }; 595 | 596 | double NewStyleShoes::getCharge(int quantity) const 597 | { 598 | double result = price; 599 | 600 | if (quantity > 1) 601 | { 602 | result += ((quantity - 1) * price * 0.8); 603 | } 604 | 605 | return result; 606 | } 607 | ~~~ 608 | ~~~cpp 609 | struct LimitedEditionShoes : Shoes 610 | { 611 | using Shoes::Shoes; 612 | 613 | private: 614 | double getCharge(int quantity) const override; 615 | }; 616 | 617 | double LimitedEditionShoes::getCharge(int quantity) const 618 | { 619 | return price * quantity * 1.1; 620 | } 621 | ~~~ 622 | 623 | 至此我们的重构目标已经达成。通过上例我们再次展示了如何使用原子步骤进行安全小步的代码修改,并且展示了锚点的使用。对于本例可能有人会说,`getCharge`中的`switch-case`被分解到每个子类中去了,但是我们在`Shoes::create`中又创建出了一个`switch-case`结构。但其实这两个`switch-case`背后的意义是不同的,重构后的仅仅是在做构造分发,职责清晰、简单明了; 如果后续再有根据类型执行不同算法的行为,就直接实现在具体子类中,而不用增加新的`switch-case`结构了! 实际操作中,我们其实是在变化发生的时候先来进行上述重构,然后再增加新的代码。 624 | 625 | ### 重构的顺序 626 | 627 | 通过之前的例子可以看到稍微复杂一点的重构都是由一系列的基本手法或者原子步骤组成. 在实践中对基本手法或者原子步骤的使用顺序非常重要,如果顺序不当,有时甚至会让整个重构变得很难实施. 628 | 关于重构的顺序,最基本的一点原则是**自底向上**! 我们只有先从最细节的重构开始着手,才能使得较大的重构轻易完成. 629 | 例如对于消除兄弟类型之间的重复表达式,我们只有先运用Extract Method将重复部分和差异部分分离,然后才能将重复代码以Pull Up Method重构手法推入父类中. 630 | 631 | 对于稍微复杂的重构,当我们确定了重构目标后,接下来就可以进行重构顺序编排,顺序编排的具体操作方法是:**从目标开始反向推演,为了使当前目标达成的这一步操作能够非常容易、安全和高效地完成,它的上一步状态应该是什么? 如此递归,一直到代码的现状.** 632 | 633 | 下面我们以复杂一点的Extract Method为例,应用上述原则,完成重构顺序的编排. 634 | 635 | 我们知道Extract Method在函数有较多临时变量的时候,是比较难以实施的.原函数内较多的临时变量可能会导致提取出的函数有大量入参和出参,增加提炼难度,并且降低了该重构的使用效果. 636 | 所以为了使得提炼较为容易实施,我们一般需要在原函数中解决临时变量过多的问题. 637 | 在这里我们把函数内的临时变量分为两类,一类是**累计变量**(或者收集变量),其作用是收集状态,这类变量的值在初始化之后在函数内还会被反复修改.累计变量一般包括循环变量或者函数的出参. 除过累计变量外,函数内其它的变量都是**查询变量**.查询变量的特点是可以在初始化的时候一次赋值,随后在函数内该变量都是只读的,其值不再被修改. 638 | 639 | 接下来我们逐步反推如何使得Extract Method较为容易实施: 640 | 1. 目标状态: 新提取出来的方法替换原方法中的旧代码,并且测试通过! 641 | 2. 为了使得1容易完成,我们现在应该已经具有新提炼出来的新方法,该方法命名良好,其返回值和出入参是简单明确的,其内部实现已经调整OK.并且这个新提炼出来的方法是编译通过的. 642 | 3. 为了使得2容易完成,我们需要先为其创建一个方法原型,将老代码中要提炼部分copy进去作为其实现,根据copy过去的代码非常容易确定新函数的返回值类型和出入参. 643 | 4. 为了使得3容易完成,原有代码中被提炼部分和其余部分应该具有很少的临时变量耦合. 644 | 5. 为了满足4,我们需要消除原有函数中的临时变量. 对于所有的查询变量,我们可以将其替换成查询函数(应用重构手法:以查询替代临时变量). 645 | 6. 为了使得5容易做到,我们需要区分出函数内的累计变量和查询变量.如果某一查询变量被多次赋值,则将其分解成多个查询变量,保证每个查询变量都只被赋值一次.并且对每个查询变量定义即初始化,而且要定义为const类型. 646 | 7. 为了使6方便做到,我们需要容易地观察到变量是否被赋值多次,这时变量应该被定义在离使用最近的地方.所以我们应该对变量的定义位置进行调整,让其最靠近其使用的位置.(对C语言程序这点尤其重要). 647 | 648 | 有了上面的分析,我们将其反过来,就是对于稍微复杂的Extract Method重构手法每一步的操作顺序: 649 | 1. 修改原函数内每一个局部变量的定义,让其最靠近其使用的地方,并尽量做到定义即初始化; 650 | 2. 区分查询变量和累计变量.对于查询变量有多次赋值的情况,将其拆分成多个查询变量.保证每个查询变量只被赋值一次. 651 | 3. 对每个查询变量的类型定义加上const,进行编译. 652 | 4. 利用"以查询替代临时变量"重构,消除所有查询变量.减少原函数中临时变量的数目. 653 | 5. 创建新的方法,确定其原型. 654 | 6. 将原函数中待提炼代码拷贝到新的函数中,调整其实现,保证编译通过. 655 | 7. 将新的函数替换回原函数,保证测试通过. 656 | 657 | 下面是一个例子: 658 | ~~~cpp 659 | bool calcPrice(Product* products,U32 num,double* totalPrice) 660 | { 661 | U32 i; 662 | double basePrice; 663 | double discountFactor; 664 | 665 | if(products != NULL) 666 | { 667 | for(i = 0; i < num; i++) 668 | { 669 | basePrice = products[i].price * products[i].quantity; 670 | 671 | if(basePrice >= 1000) 672 | { 673 | discountFactor = 0.95; 674 | } 675 | else 676 | { 677 | discountFactor = 0.99; 678 | } 679 | 680 | basePrice *= discountFactor; 681 | 682 | *totalPrice += basePrice; 683 | } 684 | 685 | return true; 686 | } 687 | 688 | return false; 689 | } 690 | ~~~ 691 | 上面是一段C风格的代码. 函数`calcPrice`用来计算所有product的总price. 其中入参为一个Product类型的数组,长度为num. 每个product的价格等于其单价乘以总量,然后再乘以一个折扣. 当单价乘以总量大于等于1000的时候,折扣为0.95,否则折扣为0.99. 出参totalPrice为最终计算出的所有product的价格之和. 计算成功函数返回true,否则返回false并且不改变totalPrice的值. 692 | 693 | Product是一个简单的结构体,定义如下 694 | 695 | ~~~cpp 696 | typedef unsigned int U32; 697 | 698 | struct Product 699 | { 700 | double price; 701 | U32 quantity; 702 | }; 703 | ~~~ 704 | 705 | `calcPrice`函数的实现显得有点长.我们想使用Extract Method将其分解成几个小函数.一般有注释的地方或者有大的if/for分层次的地方都是提炼函数不错的入手点.但是对于这个函数我们无论是想在第一个if层次内,或者for层次内提炼函数,都遇到不小的挑战,主要是临时变量太多导致函数出入参数过多的问题. 706 | 下面是我们借助eclipse的自动重构工具将for内部提取出一个函数的时候给出的提示: 707 | 708 | ![](./pics/extractExample.jpeg) 709 | 710 | 可以看到它提示新的函数需要有5个参数之多. 711 | 712 | 对于这个问题,我们采用上面总结出来的Extract Method的合理顺序来解决. 713 | 1. 将每一个局部变量定义到最靠近其使用的地方,尽量做到定义即初始化; 714 | ~~~cpp 715 | bool calcPrice(Product* products,U32 num,double* totalPrice) 716 | { 717 | if(products != NULL) 718 | { 719 | for(U32 i = 0; i < num; i++) 720 | { 721 | double basePrice = products[i].price * products[i].quantity; 722 | 723 | double discountFactor; 724 | if(basePrice >= 1000) 725 | { 726 | discountFactor = 0.95; 727 | } 728 | else 729 | { 730 | discountFactor = 0.99; 731 | } 732 | 733 | basePrice *= discountFactor; 734 | 735 | *totalPrice += basePrice; 736 | } 737 | 738 | return true; 739 | } 740 | 741 | return false; 742 | } 743 | ~~~ 744 | 在上面的操作中,我们将变量`i`,`basePrice`和`dicountFactor`的定义位置都挪到了其第一次使用的地方.对于`i`和`basePrice`做到了定义即初始化. 745 | 2. 对于查询变量有多次赋值的情况,将其拆分成多个查询变量.保证每个查询变量只被赋值一次. 746 | 在这里我们辨识出`totalPrice`和`i`为累计变量,其它都是查询变量.对于查询变量`basePrice`存在多次赋值的情况,这里我们把它拆成两个变量(增加`actualPrice`),保证每个变量只被赋值一次.对于所有查询变量尽量加上const加以标识. 747 | ~~~cpp 748 | bool calcPrice(const Product* products,const U32 num,double* totalPrice) 749 | { 750 | if(products != NULL) 751 | { 752 | for(U32 i = 0; i < num; i++) 753 | { 754 | const double basePrice = products[i].price * products[i].quantity; 755 | 756 | double discountFactor; 757 | if(basePrice >= 1000) 758 | { 759 | discountFactor = 0.95; 760 | } 761 | else 762 | { 763 | discountFactor = 0.99; 764 | } 765 | 766 | const double actualPrice = basePrice * discountFactor; 767 | 768 | *totalPrice += actualPrice; 769 | } 770 | 771 | return true; 772 | } 773 | 774 | return false; 775 | } 776 | ~~~ 777 | 3. 利用"以查询替代临时变量"重构,消除所有查询变量.减少原函数中临时变量的数目. 778 | 在这里先从依赖较小的`basePrice`开始. 779 | ~~~cpp 780 | double getBasePrice(const Product* product) 781 | { 782 | return product->price * product->quantity; 783 | } 784 | 785 | bool calcPrice(const Product* products,const U32 num,double* totalPrice) 786 | { 787 | if(products != NULL) 788 | { 789 | for(U32 i = 0; i < num; i++) 790 | { 791 | double discountFactor; 792 | if(getBasePrice(&products[i]) >= 1000) 793 | { 794 | discountFactor = 0.95; 795 | } 796 | else 797 | { 798 | discountFactor = 0.99; 799 | } 800 | 801 | const double actualPrice = getBasePrice(&products[i]) * discountFactor; 802 | 803 | *totalPrice += actualPrice; 804 | } 805 | 806 | return true; 807 | } 808 | 809 | return false; 810 | } 811 | ~~~ 812 | 4. 下来搞定`discountFactor` 813 | ~~~cpp 814 | double getBasePrice(const Product* product) 815 | { 816 | return product->price * product->quantity; 817 | } 818 | 819 | double getDiscountFactor(const Product* product) 820 | { 821 | return (getBasePrice(product) >= 1000) ? 0.95 : 0.99; 822 | } 823 | 824 | bool calcPrice(const Product* products,const U32 num,double* totalPrice) 825 | { 826 | if(products != NULL) 827 | { 828 | for(U32 i = 0; i < num; i++) 829 | { 830 | const double actualPrice = getBasePrice(&products[i]) * getDiscountFactor(&products[i]); 831 | *totalPrice += actualPrice; 832 | } 833 | 834 | return true; 835 | } 836 | 837 | return false; 838 | } 839 | ~~~ 840 | 5. 下来消灭`actualPrice`: 841 | ~~~cpp 842 | double getBasePrice(const Product* product) 843 | { 844 | return product->price * product->quantity; 845 | } 846 | 847 | double getDiscountFactor(const Product* product) 848 | { 849 | return (getBasePrice(product) >= 1000) ? 0.95 : 0.99; 850 | } 851 | 852 | double getPrice(const Product* product) 853 | { 854 | return getBasePrice(product) * getDiscountFactor(product); 855 | } 856 | 857 | bool calcPrice(const Product* products,const U32 num,double* totalPrice) 858 | { 859 | if(products != NULL) 860 | { 861 | for(U32 i = 0; i < num; i++) 862 | { 863 | *totalPrice += getPrice(&products[i]); 864 | } 865 | 866 | return true; 867 | } 868 | 869 | return false; 870 | } 871 | ~~~ 872 | 6. 到目前为止,我们最初的目标已经达成了.如果你觉得`getBasePrice`调用过多担心造成性能问题,可以在`getDiscountFactor`和`getPrice`函数中使用inline function重构手法将其再内联回去.但是`getBasePrice`可以继续保留,假如该方法还存在其它客户的话.另外是否性能优化可以等到有性能数据支撑的时候再进行也不迟. 873 | 874 | 7. 最后,可以使用重构手法对`calcPrice`做进一步的优化: 875 | ~~~cpp 876 | double getTotalPrice(const Product* products,const U32 num) 877 | { 878 | double result = 0; 879 | 880 | for(U32 i = 0; i < num; i++) 881 | { 882 | result += getPrice(&products[i]); 883 | } 884 | 885 | return result; 886 | } 887 | 888 | bool calcPrice(const Product* products,const U32 num,double* totalPrice) 889 | { 890 | if(products == NULL) return false; 891 | 892 | *totalPrice = getTotalPrice(products,num); 893 | 894 | return true; 895 | } 896 | ~~~ 897 | 针对最后是否提取`getTotalPrice`函数可能会有争议. 个人认为将其提取出来是有好处的,因为大多数情况下只关注正常场景计算的函数是有用的.例如我们可以单独复用该函数完成对计算结果的打印: 898 | ~~~cpp 899 | Product products[10]; 900 | ... 901 | printf("The total price of products is %f\n",getTotalPrice(product,10)); 902 | ~~~ 903 | 904 | 通过上面的例子可以看到按照合理顺序进行重构的重要性. 当我们实施重构的时候,如果到某一步觉得很难进行,就要反思自己的重构顺序到底对不对,首先看看是不是自底向上操作的,再思考一下如果要让当前步骤变得简单,它之前还应该做什么.具有这样的思维后,以后碰到各种场景就都能游刃有余了. 905 | 906 | ### 总结 907 | 本节我们总结了四类基本的重构手法.复杂的重构基本上可以借助基本手法的组合来完成. 更进一步我们提炼了重构的原子步骤,将大家从学习重构的繁琐步骤中解放出来. 只要掌握了两个原子步骤及其要求,就可以组合出大多数的重构手法. 而且原子步骤是安全小步的,代码的提交和回滚可以以原子步骤为单位进行. 为了使substitute容易进行,我们讨论了锚点以及其经常使用的场合. 最后我们总结了重构操作顺序背后的思想,借助合理的顺序,可以让我们的重构变得轻松有序. 908 | 909 | --- 910 | 911 | > - 作者:王博 912 | > - Email:e.wangbo@gmail.com 913 | > - Github:https://github.com/MagicBowen 914 | > - 个人简书主页:https://www.jianshu.com/u/92b7d9879f20 915 | > 916 | > - **转载请注明作者信息,谢谢!** 917 | -------------------------------------------------------------------------------- /effective-refactoring-3.md: -------------------------------------------------------------------------------- 1 | # Effective Refactoring 3 2 | 3 | ## 高效实施重构 4 | 5 | 当我们熟练掌握了重构技术后,还不能就此说自己在实践中已经可以安全而高效地实施重构了! 因为落到真正的工程实践环境,安全和高效的重构过程还需要好用的IDE工具,成熟的自动化测试套件,快速高效的开发构建环境,以及良好的编码习惯来支撑. 6 | 本节以C/C++语言为例,总结了一些和高效重构相关的语法及工程实践方面的关注点,这些点同样可以供别的编程语言参考。 7 | 8 | ### 语言相关 9 | 不同编程语言的设计和发展历程不同,导致语法层面有些方面对重构非常友好,但也都有不利于开展重构的语法特性。例如`C++`,由于它语法相对复杂,导致业界对`C++`的自动化重构工具支持一直非常不理想,很多重构到目前为止只能手工进行。重构工具对编程语言的支持,其中一个重要的点在于对代码引用点分析查找的可靠性。 `C++`中有很多语法会干扰到这一分析,导致难以进行自动化重构. 10 | 11 | `C++`中会让重构难以进行的语法包含以下: 12 | - 宏 13 | - 指针 14 | - 函数重载 15 | - 操作符重载 16 | - 默认参数 17 | - 类型强转 18 | - 隐式转换 19 | - 友元 20 | - 模板 21 | 22 | 还有一些不合理的编码规范,也会阻碍到重构的进行,例如 23 | - 匈牙利命名 24 | - 函数单一出口 25 | - 大而全的common头文件 26 | - ... 27 | 28 | 上述语法或者规范不仅会让自动化重构难以进行,也会干扰到手动重构. 对于不好的编码规范要及时摒弃! 对于会妨碍重构的语言元素要慎重使用,最好保持其影响在一定范围内,缩小重构时查找引用不确定性的范围. 29 | 30 | 当然C++语言中也有一些语法非常有利于重构,例如: 31 | - `const` 32 | - 访问限制修饰符`private`,`protected` 33 | - 匿名`namespace` 34 | - 局部变量延迟定义 35 | 36 | 这些语言元素有些会让代码语义更明确,有些会限制代码元素的可见度,缩小引用查找范围. 对于这些语言元素在开发中尽可能多的去使用,可以让重构变得更加容易. 37 | 38 | 最后,保持好的编码习惯,例如一开始就不要将文件\类\函数搞得过大,尽可能多的对外隐藏细节,对复杂语法的使用保持慎重态度,这样每次重构的成本都会小很多. 39 | 40 | ### 自动化测试 41 | 42 | 重构的安全性依赖于快速的自验证测试用例。基本每种编程语言都有对应的xUnit测试套件,可以对软件进行自动化测试用例的开发和执行. 43 | 44 | 针对C++,可供选择的xUnit套件有`Gtest`,`Testngpp`,`CppUnit`,`CXXTest`以及`Boost Test`等。目前最多人使用的应该是`Gtest`,由于使用广泛所以非常成熟,支持跨平台,方便简单容易上手,提供了丰富的断言机制和成熟的用例管理. 当然`Gtest`也有其问题,主要是测试注册机制不够优雅导致限制过多,测试用例运行期不能隔离导致互相影响,测试工程的组织不能模块化导致单元测试不能显示化物理依赖,无法低成本完成测试依赖文件的物理替换. 而`Testngpp`和`Gtest`一样跨平台,断言丰富,用例管理成熟.。它最大的强大之处是支持沙盒模式,测试用例运行期间互相隔离,不会彼此影响. 另外,`Testngpp`支持模块化管理测试工程,支持方便的对测试依赖文件进行物理替换,这一特性对于没有虚接口情况下的mock非常实用. 但是由于`Testngpp`的强大,也导致了它某些情况下不如`Gtest`方便使用,受众相对小,文档也不全面。 45 | 46 | 在使用xUnit的同时再能结合一款合适的Mock框架,可以大大降低单元测试的成本。对于`C++`目前最好用Mock框架是`Mockcpp`。事实上mock技术在测试中经常容易被滥用,导致编写过多的面向行为的脆弱测试用例. 其实在测试中直接打桩进行状态验证成本并不高,所以现今我已经很少使用mock工具了。但是学会一款mock工具留着在适合的时候偶尔使用一下也是不错的. 47 | 48 | 关于C/C++语言中如何做好自动化测试以及挑选合适的测试框架,可以参考这篇文章:["C/C++如何做好单元测试"](https://www.jianshu.com/p/9b2d0ed18211)。 49 | 50 | 最后,我们都知道重构需要自动化测试的支持! 但是并不是没有自动化测试,重构就不能开展! 有些历史遗留代码,一开始很难加进去测试,必须要先着手修改,才能逐渐添加测试. 这时起步需要的就是对安全小步的重构手法的熟练掌握,以及利用原有手工测试进行安全保证. 当对软件进行修改从而可以添加自动化测试后,重构慢慢就上轨道了. 51 | 52 | ### IDE 53 | 为了提高重构效率,我们需要一款智能的IDE。它需要支持基本的自动化重构,能够高效准确的搜索到代码的引用点,支持良好的面向对象风格代码浏览,高效流畅地快捷键... 54 | 55 | 对于Java来说无论`eclipse`或者`IntelliJ IDEA`都相当不错,而对于类似C/C++这样相对复杂的静态语言,或者动态脚本语言,都相对难找到一款高效好用的全面支持自动化重构的IDE。 56 | 57 | 对于C++ IDE,这里做以下对比和推荐: 58 | - **Visual studio C++** 59 | 自身的重构工具并不完善,但是加上番茄小助手就很不错了.支持自动重命名,自动提取函数,重命名类还会自动把类的头文件和所有的`#include`点的文件名都做一修改.最大的缺点是不能跨平台,而且耗费机器资源较多。据说从Visual studio 2015以后已经内置了很多重构功能,不过本人没有试过. 60 | - **clion** 61 | Jetbrains公司出品的跨平台C/C++ IDE,号称要做最强大的C++ IDE. 目前是收费的,免费的只能试用30天。工程构建只支持CMake,另外只能在64位机器上使用。 Clion内置的自动化重构菜单是最强大(支持rename,inline,extract,Pull member up/down ...)。本人试用过1.0版本,比较耗性能,当时用户也还不是特别多。 62 | - **eclipse-cdt** 63 | 个人使用频率比较高的`C++` IDE。 最大的好处是支持跨平台. 用eclipse在类的继承引用关系之间跳转非常流畅.。支持简单的重命名,提炼函数,提炼常量等自动化重构.。从luna版本开始支持重命名文件或者移动文件的时候会自动替换所有`#include`中的路径名. 个人目前最满意的C++ IDE. 64 | 65 | 关于如何为C/C++语言挑选好用的IDE,具体可以参考["C++ IDE口味谈"](https://www.jianshu.com/p/23ac257a6e0c)和["Effective Eclipse CDT"](https://www.jianshu.com/p/dafcdce1f9cb)系列文章。 66 | 67 | ### 物理重构 68 | 69 | 物理设计往往容易被人们忽视,但其实它对软件的易理解性和可维护性也非常重要! 好的物理设计不仅可以减少物理依赖,还会有利于软件的构建和发布。所以程序员除了掌握一般的代码元素级别的重构,还需要熟悉常见的物理级别的重构,包括文件重命名,文件提炼,文件移动,或者目录结构调整等等。对于C/C++语言,物理重构比较繁琐的地方在于经常需要同步修改makefile或者头文件的`#include`引用点。 70 | 71 | 以下以C/C++举例,看看一些和物理重构相关的经验技巧(make相关的见下节): 72 | 73 | - 每个文件只包含一个类,并且文件名和类名相同. 这样方便重命名类的时候IDE自动对文件进行重命名. 74 | - 利用IDE提供的自动化重命名或者移动菜单进行文件/目录的重命名或者移动,以便IDE可以对涉及文件的所有`#include`引用点进行路径自动替换. 75 | - 使用IDE提供的自动化头文件添加功能来添加物理依赖,例如在eclipse中是`ctrl+shift+n` 76 | - 配置IDE的头文件模板,让自动生成头文件的Include Guard使用Unique Identifier,避免每次头文件重命名后还得要修改Include Guard(见下). (最新的eclipse mars版本中在Windows -> Preferences -> C/C++ -> Code Style -> Name Style -> Code -> Include Guard 中选择 Unique Identifier,较老版本需要在workspace中修改配置文件,可以自行google修改办法). 77 | ~~~cpp 78 | // Runtime.h 79 | #ifndef HDEA41619_5212_4A92_8A09_3989000E6BAE 80 | #define HDEA41619_5212_4A92_8A09_3989000E6BAE 81 | 82 | struct Runtime 83 | { 84 | void run(); 85 | }; 86 | 87 | #endif 88 | ~~~ 89 | 90 | ### 编译构建 91 | 不好的编译构建工程不仅干扰到物理重构,更重要的可能导致编译效率底下从而让重构无法正常开展. 没人愿意在每次构建都要几十分钟到数小时的工程上进行频繁重构. 尤其是对于大型的C/C++工程,需要对其编译构建过程进行持续地分析优化,不断提高版本的编译构建速度。 92 | 93 | 以下是一些针对C/C++工程的编译构建实践经验: 94 | - makefile中尽可能使用模式规则.自动搜索文件.不要显示使用源文件名,否则每次重命名文件后都得要修改makefile. 95 | - makefile中为预编译目标文件设置规则,不仅有利于解决一些宏展开的问题,更重要的可以快速解决一些头文件包含上的编译难题. 96 | ~~~makefile 97 | # makefile example 98 | ... 99 | SRCS += $(abspath($(shell find $(SOURCE_HOME) -name "*.cpp"))) 100 | OBJS = $(subst $(SOURCE_HOME),$(TARGET_HOME),$(SRCS:.cpp=.o)) 101 | ... 102 | $(TARGET_HOME)%.i : $(SOURCE_HOME)%.cpp 103 | $(CXX) -E -o $@ -c $< 104 | $(TARGET_HOME)%.o : $(SOURCE_HOME)%.cpp 105 | $(CXX) -o $@ -c $< 106 | $(TARGET):$(OBJS) 107 | @$(generate-cmd) 108 | ~~~ 109 | - makefile采用自底向上组织,保证每个源代码文件单独可编译,每个模块单独可编译. 最终产品版本的构建调用每个模块的makefile生成编译中间产物后进行链接. 不要采用自顶向下传递make参数的makefile工程管理模式,否则每次编译任何一个文件或者模块都要全编译所有代码. 110 | - 尽可能使用并行编译. 在Visual Studio下可以使用IncrediBuild分布式编译工具. 对于make使用`-j`选项,并且可以使用distcc来进行分布式编译.另外可以使用ccache来做缓存加速编译。 111 | - 将测试工程的编译构建和真实产品版本的构建分离,测试工程的编译构建可以采用更好的工具:例如cmake,rake等. 112 | - 保证增量编译可用,并且是可靠的. 113 | 114 | 很多项目虽然有增量编译,但是都不够可靠,尤其是当有头文件删除的时候! 另外每次无论是否有依赖变化,makefile都要重新生成加载.d文件,效率也很低下. 所以大多数时候增量编译功能是关闭的! 115 | 《GNU Make项目管理》一书中提供了很多大型工程中make组织的优秀实践,其中有一段对增量编译可靠高效的makefile片段,我将其提炼成了一段make函数,见下面,大家可以使用. 116 | ~~~makefile 117 | # make_depend.mak 118 | # this file implement the makefile function for dependencies rules 119 | 120 | # $(call make-depend,source_file,object-file,depend-file) 121 | define make-depend 122 | mkdir -p $(dir $3); 123 | $(CC) $(CFLAGS) $(CPPFLAGS) -MM $(INCLUDE) $(TARGET_ARCH) $1 > $3.tmp1; 124 | $(SED) 's,\($(notdir $*)\)\.o[ :]*,$2 $3 : ,g' < $3.tmp1 > $3.tmp 125 | $(SED) -e 's/#.*//' \ 126 | -e 's/^[^:]*: *//' \ 127 | -e 's/ *\\$$//' \ 128 | -e '/^$$/ d' \ 129 | -e 's/$$/ :/' $3.tmp >> $3.tmp 130 | $(MV) $3.tmp $3 131 | $(RM) $3.tmp1 132 | endef 133 | 134 | # should use the make-depend as follow 135 | # %.o: %.c 136 | # $(call make-depend,$<,$@,$(subst .o,.d,$@)) 137 | # $(CC) -o $@ $< 138 | ~~~ 139 | 140 | ## 后记 141 | 142 | 作为一名软件技术咨询师,过去些年指导过许多大型软件系统的重构工作。 这篇文章基本上是自己多年工作实践的一些心得,包括对如何高效实施重构的一些精炼和总结。当然关于重构的很多具体实施方法还是在《重构》一书的每处细节中,励志学好和用好重构的同学对于《重构》一书最好能够反复阅读实践。 143 | 144 | 对于再大型的软件重构,无论你的目标架构多么漂亮,最终还必须是一行一行的代码修改。如何安全可靠并且高效地修改代码,必然是落地的基本技能! 145 | 146 | 对于日常开发也是如此, 保持代码符合简单设计是一项日常行为,重构是达成它的唯一方式. 对重构的使用应该融入到代码开发的每时每刻,到最后不必强行区分是在开发还是重构,就像《重构》的序言中所说"变得像空气和水一样普通". 希望每个学习重构的同学都能体会到这种感觉! 147 | 148 | 千里之行,始于足下! 149 | 150 | --- 151 | 152 | > - 作者:王博 153 | > - Email:e.wangbo@gmail.com 154 | > - Github:https://github.com/MagicBowen 155 | > - 个人简书主页:https://www.jianshu.com/u/92b7d9879f20 156 | > 157 | > - **转载请注明作者信息,谢谢!** -------------------------------------------------------------------------------- /pics/3factor.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MagicBowen/refactoring/749e756a007d9fcd16ab1e96383d7dc693a17eda/pics/3factor.jpeg -------------------------------------------------------------------------------- /pics/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MagicBowen/refactoring/749e756a007d9fcd16ab1e96383d7dc693a17eda/pics/code.png -------------------------------------------------------------------------------- /pics/cover2-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MagicBowen/refactoring/749e756a007d9fcd16ab1e96383d7dc693a17eda/pics/cover2-small.png -------------------------------------------------------------------------------- /pics/cover2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MagicBowen/refactoring/749e756a007d9fcd16ab1e96383d7dc693a17eda/pics/cover2.jpg -------------------------------------------------------------------------------- /pics/extractExample.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MagicBowen/refactoring/749e756a007d9fcd16ab1e96383d7dc693a17eda/pics/extractExample.jpeg -------------------------------------------------------------------------------- /pics/formular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MagicBowen/refactoring/749e756a007d9fcd16ab1e96383d7dc693a17eda/pics/formular.png -------------------------------------------------------------------------------- /pics/select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MagicBowen/refactoring/749e756a007d9fcd16ab1e96383d7dc693a17eda/pics/select.png -------------------------------------------------------------------------------- /refactoring-2nd.md: -------------------------------------------------------------------------------- 1 | # 重构的重构 - 《重构》第二版导读 2 | 3 | ![](./pics/cover2-small.png) 4 | 5 | 近20年过去了,Martin Fowler先生终于推出了新版的《重构》。本人有幸于ThoughtWorks技术雷达十周年峰会现场率先拿到了此书的国内发行版。 6 | 7 | 在这20年中,软件开发技术发生了很多重要的变化。新的编程语言不断涌现,老的编程语言也加快迭代。主流编程语言大都支持了多种编程范式,函数式编程和面向对象一样成了主流编程语言的标配。对并发的更好支持也已成为主流编程语言新的核心竞争力。于此同时各种软件开发工具也日益现代化,常用的编程IDE都具备了面向重构、测试甚至容器化发布的自动化工具和快捷键。 8 | 9 | 基于上,很多人都认为新版的重构会迎合时代的变化,焕然一新。然而当我用一整天时间读完全书后,却不禁如释重负。正如本书中文译者熊节先生所说“Flowler先生不仅没有拔高,反而把功夫做的更扎实了”。 10 | 11 | 确实,无论编程语言的语法如何变化、编程范型如何多元化、工具如何发展,软件设计的目标并没有变:那就在保证软件满足功能和非功能需求的前提下,如何更易应对变化以及更易让人理解和维护。由此所推导出来的软件设计原则也是几十年都没有变,如`高内聚、低耦合`,如`SOLID原则`等。甚至连`GOF设计模式`至今依然生命力旺盛,除了偶有在一些新的编程范型中出现的新模式以及对原有模式的更简单实现。此刻再回顾重构技术,它所传授的如何识别代码中的坏味道,以及如何采用小步安全的重构手法逐步将代码演化到更易理解、更易应对变化的状态,正是为了满足软件设计的核心诉求!所以重构应该和设计模式一样,是一项软件开发中历久而弥新的核心能力。 12 | 13 | 基于此,新版《重构》在主体内容上和第一版相似。首先从一个示例开始,先让读者从整体上体会重构的过程和效果。然后给出了重构的具体概念和原则。之后Martin老先生给读者列出了一份重要的代码坏味道清单并逐一诠释。随后用了一章篇幅来讲述如何搭建对重构来说至关重要的“自动化测试体系”。最后Martin用整本书近四分之三的篇章详细阐述了几十种关键的重构手法。 14 | 15 | 而在所谓与时俱进的方面,Martin则将更多的精力放在了对细节的持续优化上。首先直接可见的是新版删除了第一版中的最后几章:“大型重构”、“重构,复用与现实”,“重构工具”等,一方面是因为这几章中有些内容在今天看来已不是那么重要,其次所谓的“大型重构”其实仍旧是一系列小的重构手法的合理组合和持续应用。在第二版中Martin将重点放到了对重构手法的持续优化上:首先他将原本**22种**代码坏味道调整为**24种**,然后对所有重构手法进行了重新分类和排布,以便更加内聚和操作连贯。新版保留了第一版中大部分的手法,增加了一些更加具体和有用的手法,同时对所有的描述和示例都进行了更加精致的优化。 16 | 17 | 除这些之外,整本书的示例语言也从Java换成了JavaScript。 18 | 19 | 接下来我们具体看一下新版中的种种变化。 20 | 21 | ## JavaScript 22 | 23 | 不少人认为第二版中将重构的示例语言由Java变为JavaScript,并不是一种妥当的选择。毕竟JavaScript作为一门动态脚本语言,并不适合较大规模的工业级软件产品。但我个人认为选择JavaScript是经过Martin深思熟虑的结果。 24 | 25 | 自从NodeJs将JavaScript带进了服务端开发,JavaScript就变成了一门前后端通吃的语言。此外,ES6标准将JavaScript变成了一门现代语言,语法上同时支持面向对象和函数式范式。最后JavaScript内置多种支持并发编程的特性,它的包管理工具和生态也都建设得非常好。相比其它语言,JavaScript的受众以及可应用的领域相对更加广泛一些。 26 | 27 | JavaScript作为一门动态类型语言,IDE对其自动化重构的支持并没有Java那么好。正因如此,书中介绍的**手动**重构步骤才会显得更加有意义。而熟练掌握手动重构技巧,不仅能更好的理解重构的本质,对在其它一些主流编程语言下实施重构也非常有价值(例如C、C++、Python等)。 28 | 29 | 站在另一方面,借助JavaScript的多范式编程以及动态类型的语法特性,很多重构手法可以做的更加简单和灵活。例如JavaScript支持在函数内嵌套定义函数,所以应用`Extract Method`手法时,可以先把新提来出来的子函数定义在调用它的函数内部,这样借助函数的闭包性可以减少函数间传递的参数数量,等到真正需要复用该子函数的时候再把它挪到外面。全书中类似的例子还有很多。 30 | 31 | 在书中,Martin使用了ES6的语法,不仅因为ES6标准为JavaScript引入了类和继承的语法糖,而且在很多细节上也能让代码更加清晰。例如书中对`const`和`let`关键字的大量使用等。但同时Martin也尽量避免使用其他程序员不太熟悉的JavaScript编程风格,例如JS独特的对象模型以及`generator`、`promise`和`await/async`等语法。 32 | 33 | 因为使用JavaScript的缘故,新版对“构筑测试体系”一章做了较大修改。Martin演示了如何使用JavaScript的测试框架`Mocha`和断言库`Chai`构筑自动化测试系统,以支持重构的安全实施。 34 | 35 | ## 代码坏味道 36 | 37 | 学好重构的关键在于掌握三个点:起点、手法和目标(可以参考我之前写的一篇文章:[《高效重构(一):正确理解重构》](https://www.jianshu.com/p/3b34f337eaee))。 38 | 39 | 代码的坏味道清单为我们指出了重构的起点。当发现代码中有类似清单中所列的坏味道时,就暗示着这是一个应该实施重构的地方。由此可见代码坏味道清单的重要性。 40 | 41 | Martin在新版中对这份清单进行了重新梳理。下表对比了两版之间的差异。 42 | 43 | |**第一版**|**第二版**|**变化概要**| 44 | |:-:|:-:|-| 45 | |✗|神秘命名
`Mysterious Name`|新版增加,突出了好的命名对于代码的重要性| 46 | |重复代码
`Duplicataed Code`|重复代码
`Duplicataed Code`|新版删除了对”毫不相干的类中出现的重复“的描述,避免不合时宜的提取非本质重复| 47 | |过长函数
`Long Method`|过长函数
`Long Function`|基本未变| 48 | |过长参数列表
`Long Parameter List`|过长参数列表
`Long Parameter List`|新版提供了更多的重构解决方案| 49 | |✗|全局数据
`Global Data`|新版突出了全局数据对代码耦合性的恶劣影响| 50 | |✗|可变数据
`Mutable Data`|新版突出了适宜的不可变性对代码可维护性上的优势| 51 | |发散式变化
`Divergent Change`|发散式变化
`Divergent Change`|新版提供了更多应对发散式变化的重构方案| 52 | |霰弹式修改
`Shotgun Surgery`|霰弹式修改
`Shotgun Surgery`|新版提供了更多应对霰弹式修改的重构方案| 53 | |依恋情结
`Feature Envy`|依恋情结
`Feature Envy`|新版对封装单元的表述不再仅仅针对类,扩展到函数和模块| 54 | |数据泥团
`Data Clumps`|数据泥团
`Data Clumps`|基本未变| 55 | |基本类型偏执
`Primitive Obsession`|基本类型偏执
`Primitive Obsession`|新版增加了”类字符类型变量“的危害,并精简了描述| 56 | |Switch表达式
`Switch Statement`|重复的Switch
`Repleated Switch`|新版承认第一版有些“矫枉过正”!这项重构的核心是消除**重复**的switch| 57 | |✗|循环语句
`Loops`|凸显了在函数式下,循环有了更多的更具语义性的表达方式| 58 | |平行继承体系
`Parallel Inheritance Hierarchies`|✗|平行继承体系其实是散弹式修改的特殊形式。为了不再突出面向对象,新版删去| 59 | |冗赘类
`Lazy Class`|冗赘的元素
`Lazy Element`|新版中体现了冗余的未必只是类,可能是函数、模块等| 60 | |夸夸其谈通用性
`Speculative Generality`|夸夸其谈通用性
`Speculative Generality`|基本未变| 61 | |临时字段
`Temporary Field`|临时字段
`Temporary Field`|删除”为了特殊算法引入临时字段“的情况| 62 | |过长的消息链
`Message Chains`|过长的消息链
`Message Chains`|基本未变| 63 | |中间人
`Middle Man`|中间人
`Middle Man`|基本未变| 64 | |狎昵关系
`Inappropriate Intimacy`|内幕交易
`Insider Trading`|新版中不再仅限于类之间的不恰当耦合,将描述范围扩大到模块| 65 | |过大的类
`Large Class`|过大的类
`Large Class`|旧版中描述的”GUI大类“已经有些过时了,所以删除了| 66 | |异曲同工的类
`Alternative Classes with Different Interfaces`|异曲同工的类
`Alternative Classes with Different Interfaces`|新版对所谓类的"异曲同工"解释的更加清晰| 67 | |不完美的类库
`Incomplete Library Class`|✗|类库属于可扩展不可修改的代码,新版中把对类库的坏味道辨别和重构删掉了| 68 | |纯数据类
`Data Class`|纯数据类
`Data Class`|新版提出了例外情况:当纯数据类不可修改且仅用于传递信息时| 69 | |被拒绝的遗赠
`Refused Bequest`|被拒绝的遗赠
`Refused Bequest`|基本未变| 70 | |注释
`Comments`|注释
`Comments`|基本未变| 71 | 72 | 总结一下,新版在”代码的坏味道“这一章中除了一些语言上的调整外,主要的变化如下: 73 | - 更加注重对细节的打磨。新增一些小的明确的坏味道(如`Mysterious Name`、`Global Data`),去除了一些大的、稍显模糊或者可被分解的坏味道(如`Parallel Inheritance Hierarchies`、`Inappropriate Intimacy`); 74 | - 修正了一些之前不准确的描述。例如将`Switch Statement`更改为`Repleated Switch`。 75 | > 这个修改非常有意思,Martin在书中承认当年有些矫枉过正了!那时多少人看完重构,致力于消除代码中的每一处`if-else`和`swtich`!殊不知对于缺乏反射的静态语言,即使采用多态替换了条件分支,但是最后在工厂方法里拼装对象的时候还是会存在一个条件分支。这次总算是拨乱反正了! 76 | - 弱化了面向对象和类的分量,更多使用兼顾通用化的定义(例如使用`模块`一词替代`类`)。 77 | > 关于这点,Martin Fowler在博客中曾如是说: 78 | > "在写这本书第一版的时候,把`类`视为是构建代码机制的主要结构已经成为主流。然而,现在我们看到其它结构发挥了更大的作用。在我看来,类仍然很有价值,但是重构已经比较少地以类为中心;我们要意识到,随着代码不断被重构,类也是可以变化的。" 79 | - 在某些局部的点上,优先推荐了一些函数式的解决方案,例如`把loops替换为管道`。 80 | > 对于在新版中直接将`loops`定义为一种坏味道,个人认为是Martin新的一次矫枉过正。虽然管道和流在某些场景下比loops语义更清晰,但是不可否认在一些简单的场景下loops更加符合一般程序员的思维习惯。所以这得看具体的场景,一刀切有些不妥!不知道下一版中Martin会不会承认这类似第一版中的`Switch Statement`一样,是一次故意的“矫枉过正”。 81 | 82 | ## 重构手法 83 | 84 | 正确的实施重构手法是安全高效的达成重构目标的保障。 85 | 86 | 很多人认为学习重构只要掌握背后的思想就足够了,其详细繁琐的操作手法并不重要。于是乎现实中很多人在实际操作重构的过程中章法全无,一旦开始半天停不下来,代码很多时候处于不可编译或者测试不能通过的状态。有时改的出错了很难再使代码回到正确状态,只能推倒重来! 87 | 88 | 实际上重构是一项非常注重操作过程的技术,能够正确合理地使用重构手法,安全、小步、高效地完成代码修改,是评价重构能力的核心标准。否则Martin老先生也不值得花费多达四分之三的篇章对重构手法浓墨重彩了。 89 | 90 | 新版将重构手法从原来的68种调整到61种。由于具体的重构手法数量较多,这里就不一一对比了,只介绍一些重要的变化点。 91 | 92 | - 为了让重构手法间衔接得更加顺畅,新版中重新调整了重构手法的编排顺序; 93 | > 新版中将基本的常用重构放到了一章,起名叫做”第一组重构“,然后将其余的手法按照”封装“、”搬迁“、”数据“、”逻辑“、”API“以及”继承关系“进行了分类和排序; 94 | 95 | - 为了保持概念的内聚和一致,对一些重构手法进行了重命名或者合并; 96 | > 例如将`引入Null对象(Introduce Null Object`改为`引入特例(Introduce Special Case)` 97 | > 将`函数改名(Rename Method)`、`添加参数(Add Parameter)`和`移除参数(Remove Parameter)`统一合并为`改名函数声明(Change Function Declaration)`; 98 | 99 | - 通过调整原有的重构命名或者新增新的重构手法,让重要的重构同时存在正向和反向手法,以便可以在实践时更加灵活地决定重构的方向; 100 | > 例如将原来的`内联临时变量(Inline Temp)`和`引入解释性变量(Introduce Explaining Variable) `改名为`内联变量(Inline Variable)`和`提炼变量(Extract Variable)`; 101 | 将`以函数取代参数(Replace Parameter with Methods)`修改为`以查询取代参数(Replace Parameter with Query)`,并为其引入反向重构`以参数取代查询(Replace Query with Parameter)`; 102 | 103 | - 为了凸显重构之间的关联关系,每种手法的介绍里面增加了`曾用名`、`反向重构`和`示意图` 104 | > - `曾用名`指出该重构在第一版中的原有命名; 105 | > - `反向重构`指出该重构对应的的反向操作的手法名称,为重构之间建立起互逆关系; 106 | > - `示意图`是Martin为每个重构手法画的一副小图,形象化的展现每个重构的效果; 107 | > - 另外,建议在阅读时,除了关注每个重构手法的`做法`和`范例`外,最好能把`动机`部分也好好阅读下。`动机`部分介绍了”为什么要做“以及”什么时候不该做“这项重构,里面的很多思考都具有启发意义。 108 | 109 | - 考虑到编程范式的多元化,新版的描述中弱化了面向对象中的类的概念,同时引入了和函数式相关的一些重构; 110 | > 在很多描述中,不再以`类`作为封装的主要手段,取而代之以`模块`作为主要称呼。 111 | > 增加`以管道取代循环(Replace Loop with Pipeline)`的重构; 112 | 113 | - 考虑到”组合优于继承“,调整删除了一些和"重构到继承"相关的重构手法; 114 | > 例如删除了`提炼子类(Extract Subclass)`和`以继承取代委托(Replace Delegation with Inheritance)`等 115 | 116 | - 由于JavaScript鸭子类型的语法特性,删除了一些不再适用的重构手法; 117 | > 例如删除了`提炼接口(Extract Interface)`、`塑造模板函数(Form Template Method)`和`封装向下转型(Encapsulate Downcast)`等 118 | 119 | - 在某些重构的操作步骤里,利用了JavaScript的语法特性简化重构的操作过程; 120 | > 书中经常使用两类JavaScript的语法特性: 121 | - **函数式**。正如Martin Fowler在博客中所说:”JavaScript将函数作为一等公民无疑是最正确的决策“。JavaScript允许将函数作为参数和返回值,允许在函数里面嵌套定义函数以及支持匿名函数和闭包。所以在`提炼函数(Extract Function)`里面,先将提炼出来的新函数定义在调用它的函数的内部,借助函数的闭包性可以减少函数间传递的参数数量,等到真正需要复用该子函数的时候再将它挪出;而`以管道取代循环(Replace Loop with Pipeline)`重构,则完全依赖于匿名函数和闭包性。 122 | - **动态类型**。JavaScript支持灵活的对象模型,新版中很多重构手法会在操作过程中往对象里临时加入新的属性,以方便后面的重构过程。例如在`函数组合成变换(Combine Functions into Transform)`和`引入特例(Introduce Special Case)`中,都会看到引入了类似`enrichXXX`名称的函数对原有的对象进行动态扩充; 123 | 124 | 对于重构手法这部分,Martin进行了精心的调整,修改和增加了很多描述和代码示例。上述只是其中一些比较明显的变动,更多细致的变化还请君仔细品读原书。 125 | 126 | 最后要说的是,书中介绍了多达六十多种重构手法,普通人很难把所有的重构步骤都牢记于心。此书可以当做一本重构名录,在使用时再反复翻书查阅。相信经过不断阅读和实践,慢慢会掌握重构手法背后的普遍原理和规律,逐渐做到”手中无剑,心中有剑“。我曾把所有的重构手法归结为四类基本手法,最后浓缩成了两个核心操作,并给出了在重构时合理编排操作步骤的方法规律以及指出如何利用`锚点`来简化重构操作。具体见[《高效重构(二):掌握重构手法》](https://www.jianshu.com/p/58f4c61b1cb3)。 127 | 128 | ## 重构示例 129 | 130 | 给初学重构的朋友一个建议,那就是在读完全书后,最好能把第一章"重构,第一个示例"中的例子再自行做一遍。这个例子规模适中,代码很具样板性,非常适合用来演练重构。 131 | 132 | 在第二版中,Martin考虑到很多新时代人类完全没有在影片出租店租影片的体验,所以将例子改为”为戏剧演出的客户打印账单“的场景。这个例子和第一版中”影片出租店打印小票“的结构基本类似。大家在练习的时候,可以仔细品味以下方面: 133 | - 由**出现新的变化方向**驱动重构的展开; 134 | - 将大的重构目标**分解**为一个个针对小的对代码坏味道的重构过程; 135 | - 在重构过程中执行**安全小步**的代码调整动作; 136 | - 依赖**自动化测试**保护重构的安全性; 137 | - 在重构的最后,代码面向新的变化方向重新变得满足**开放封闭性**; 138 | 139 | 最后再提一点:在书中,面对”账单不同打印格式“的变化方向,重构到最后产生了两个函数`renderPlainText`和`renderHtml`,它们分别用来计算普通文本和HTML格式的账单字符串。对于像我这样一个对重复代码”吹毛求疵“的程序员,往往会进一步消除这两个函数中出现的模式重复。这两个函数有相同的打印算法,都是先输出账单标题,然后逐一输出每个演出的具体细则,最后输出账单的汇总信息,而且每一步中所依赖的数据是完全一致的。基于此我们可以做进一步的重构:将打印算法的重复和打印格式的不同进行分离。完成这个重构后,代码将会变成一个策略模式:由通用的打印算法组合不同的打印策略。现实中这个重构是否值得做,完全在于在不同的账单格式下”打印算法相同“这个假设是否足够稳定。 140 | 141 | 感兴趣的同学可以在[https://github.com/MagicBowen/refactoring/tree/master/code/js](https://github.com/MagicBowen/refactoring/tree/master/code/js)中找到上述重构的代码示例. 142 | 143 | ## 写在最后 144 | 145 | 本文简单地介绍了第二版《重构》的一些变化,更多的精彩细节还是推荐大家阅读原书。这次中文版本还是由熊节先生主译,所以书的翻译质量依然很高。第二版由异步图书出版,书质还是蛮不错的,又变成了硬皮封面,自然价格也有不小的上涨。但是相信对于想要提高编码能力的读者来说,此书绝对是物超所值的。 146 | 147 | 不过,还是有点小小的遗憾在这里吐槽一下。作为一个对代码整洁度要求较高的人,书中偶尔出现的代码缩进不对齐的问题会让我觉得有些刺眼。如果说有些缩进不对齐只关乎美感的话,那么如下的不对齐就会让人困惑于函数的定义位置了(没法一眼看出函数`playFor`是定义在顶层,还是在`enrichPerformance`的内部)。 148 | ![](./pics/code.png) 149 | 150 | 最后我们以一个小小的测验结束本文吧。Martin先生为每个重构手法都绘制了一副示意图,尝试猜猜下面每幅图对应的重构手法名称,以及将对应的正反手法连接起来吧:) 151 | 152 | ![](./pics/select.png) 153 | 154 | --- 155 | 156 | > - 作者:王博 157 | > - Email:e.wangbo@gmail.com 158 | > - Github:https://github.com/MagicBowen 159 | > - 个人简书主页:https://www.jianshu.com/u/92b7d9879f20 160 | > 161 | > - **转载请注明作者信息,谢谢!** 162 | --------------------------------------------------------------------------------