Refactoring strategy
In addition to the comments by dietbuddha and ThorbjørnRavnAnderson, another way to refactor build scripts is to separate them into multiple files. How you do this depends on the build system.
For Make, it's as simple as using the include
command, as recommended in "Recursive Make Considered Harmful". This directive works just like #include
in the C preprocessor and processes the included file as if it had been cut and pasted in place of the include
command. Using the include
command, it's possible to refactor your main Makefile
by moving modular pieces into sub-Makefile
s.
CMake has a similar command.
SCons requires a similar type of approach with different commands. The basic idea of splitting up the build script into a master script and several smaller sub-scripts stays the same, but instead of directly including the text of the smaller scripts in the master build script, SCons treats the smaller scripts like separate namespaces (because SCons uses Python instead of the shell). SCons Environment
objects have a method called SConscript()
that enables you to import objects from an SConstruct
file to subsidiary files called SConscript
files that can be used to refactor your build script. The basic idea of SConscript
files and the SConscript()
command can be found here on the SCons Wiki. Examples of how to use SConscripts
in hierarchical builds can be found here in the SCons User Guide.
Extrapolating from these three examples, it seems like the general strategy is to refactor a build script by splitting it into a master script that calls several files. How one does this is idiomatic to the particular build automation software used.
SCons Example, Revisited
Taking the SConstruct
file above, I moved all of the configuration information into a module called build_config.py
. All literals live in the global namespace, which could be dangerous for Python, but that's easily (though somewhat tediously) fixable. I checked ahead to make sure that I had no name clashes with __builtin__
(the Python built-in namespace, so I'm not overwriting any important objects).
## \file build_config.py
# \brief Sets configuration of file locations manually.
#
## Flags for compilers
#
flags = []
## Compile using debug versions?
#
debug = True
debugString = '-debug'
debugFlags = ['-ggdb']
dynamicLinkFlag = '-Wl,-rpath,'
if debug:
flags += debugFlags
## Configuration information for GTest
#
GTestVersion = '1.6.0'
GTestStem = 'gtest-' + GTestVersion
GTestBuildIncDir = [GTestStem,
GTestStem + '/include',
]
GTestIncDir = GTestStem + '/include/gtest'
GTestLibDir = 'lib'
GTestLibFlags = ['gtest', 'gtest_main', 'pthread']
## Configuration information for Armadillo matrix library
#
ArmadilloLibFlags = ['armadillo'];
## Locations of libraries installed on system in standard locations
#
StdIncDir = '/usr/include'
StdLibDir = '/usr/lib'
## Configuration information for COIN libraries
#
CoinUtilsVersion = '2.6.4'
ClpVersion = '1.12.0'
OsiVersion = '0.103.0'
CbcVersion = '2.5.0'
## Standard directory locations of COIN libraries, with slashes added for
# for convenience.
#
CoinLibLocation = '/usr/local/COIN/'
StdCoinIncDir = '/include/coin'
StdCoinLibDir = '/lib'
CoinUtilsStem = 'CoinUtils-' + CoinUtilsVersion
ClpStem = 'Clp-' + ClpVersion
OsiStem = 'Osi-' + OsiVersion
CbcStem = 'Cbc-' + CbcVersion
if debug:
CoinUtilsStem += debugString
CbcStem += debugString
ClpStem += debugString
OsiStem += debugString
## Build up include directory names for COIN projects from constituent parts.
#
CoinUtilsIncDir = CoinLibLocation + CoinUtilsStem + StdCoinIncDir
ClpIncDir = CoinLibLocation + ClpStem + StdCoinIncDir
OsiIncDir = CoinLibLocation + OsiStem + StdCoinIncDir
CbcIncDir = CoinLibLocation + CbcStem + StdCoinIncDir
## Build up library names from COIN projects from constituent parts
#
CoinUtilsLibDir = CoinLibLocation + CoinUtilsStem + StdCoinLibDir
ClpLibDir = CoinLibLocation + ClpStem + StdCoinLibDir
OsiLibDir = CoinLibLocation + OsiStem + StdCoinLibDir
CbcLibDir = CoinLibLocation + CbcStem + StdCoinLibDir
## CPLEX
#
CpxStem = '/opt/ibm/ILOG/CPLEX_Studio_Academic123/cplex/'
CpxIncDir = CpxStem + 'include/ilcplex'
CpxLibDir = CpxStem + 'lib/x86-64_sles10_4.1/static_pic'
## Gurobi
#
GrbStem = '/opt/gurobi460/linux64/'
GrbIncDir = GrbStem + 'include'
GrbLibDir = GrbStem + 'lib'
OsiLibFlags = ['Osi', 'CoinUtils']
ClpLibFlags = ['Clp', 'OsiClp']
CbcLibFlags = ['Cbc', 'Cgl']
OsiCpxLibFlags = ['OsiCpx']
OsiGrbLibFlags = ['OsiGrb']
CpxLibFlags = ['cplex', 'ilocplex', 'pthread', 'm']
GrbLibFlags = ['gurobi_c++', 'gurobi46', 'pthread', 'm']
milpIncDirs = [CoinUtilsIncDir,
ClpIncDir,
OsiIncDir,
CbcIncDir,
CpxIncDir,
GrbIncDir,
GTestIncDir,
]
milpLibDirs = [CoinUtilsLibDir,
ClpLibDir,
OsiLibDir,
CbcLibDir,
CpxLibDir,
GrbLibDir,
GTestLibDir,
]
milpLibFlags = [OsiCpxLibFlags,
OsiGrbLibFlags,
CbcLibFlags,
ClpLibFlags,
OsiLibFlags,
CpxLibFlags,
GrbLibFlags,
GTestLibFlags,
]
## Configuration information for Chemkin source directories and files
#
ChemkinSourceDir = '/mnt/hgfs/DataFromOldLaptop/Data/ModelReductionResearch/Papers/AdaptiveChemistryPaper/AdaptiveChemistry/NonOpenSource/ChemkinII/';
ChemkinSourceList = ['cklib.f', 'pcmach.f','tranlib.f']
ChemkinSourceList = [ChemkinSourceDir + FileName
for FileName in ChemkinSourceList]
## Configuration information for Cantera
#
CanteraStem = '/usr/local/cantera'
if debug:
CanteraStem += debugString
CanteraIncDir = CanteraStem + '/include/cantera'
CanteraLibDir = CanteraStem + '/lib'
CanteraTestingFlags = ['kinetics', 'thermo', 'tpx', 'ctbase', 'm',]
CanteraLibFlags = ['user', 'oneD', 'zeroD', 'equil', 'kinetics', 'transport',
'thermo', 'ctnumerics', 'ctmath', 'tpx', 'ctspectra',
'converters', 'ctbase', 'cvode', 'ctlapack', 'ctblas',
'ctf2c', 'ctcxx', 'ctf2c', 'm', 'm', 'stdc++']
CxxFortranFlags = ['g2c', 'gfortran'];
chemSolverIncDir = [CanteraIncDir,
StdIncDir,
'/usr/local/include',
GTestIncDir,
]
chemSolverLibDir = [StdLibDir,
CanteraLibDir,
GTestLibDir,
]
chemSolverLibFlags = [GTestLibFlags,
CxxFortranFlags,
CanteraLibFlags,
ArmadilloLibFlags,
]
canteraGTestLibFlags = CanteraTestingFlags + GTestLibFlags
#canteraGTestLibFlags = ['kinetics', 'thermo', 'tpx',
# 'ctbase', 'm', 'gtest', 'gtest_main', 'pthread']
The main SConstruct
file calls a bunch of SConscript
files to build modules containing the major features I have in my code. Moving most of the build commands to SConscript
files makes the SConstruct
file really simple:
## \file SConstruct
# \brief Compiles the library and compiles tests.
#
import SCons
from build_config import *
## \brief Build up directory names of each COIN library from package names
# and versions.
#
## Overall SCons environment
#
env = Environment();
## Compile Google Test from source using SConscript file.
#
GTestAllLib, GTestMainLib = env.SConscript('gtest.scons',
exports=['env'])
## Compile MILP solver module and tests from source using SConscript file.
#
milpSolverTest = env.SConscript('milpSolver.scons',
exports=['env'])
## Compile chemistry solver module and associated tests from source
# using SConscript file.
chemSolverTest, canteraGTest = env.SConscript('chemSolver.scons',
exports=['env'])
## Since all tests use GTest, make the dependency of the module
# tests on the GTest libraries explicit.
env.Depends(milpSolverTest, [GTestAllLib, GTestMainLib])
env.Depends(chemSolverTest, [GTestAllLib, GTestMainLib])
env.Depends(canteraGTest, [GTestAllLib, GTestMainLib])
#env.AddPostAction(milpSolverTest, milpSolverTest[0].abspath)
testAlias = env.Alias('test', [milpSolverTest, chemSolverTest])
AlwaysBuild(testAlias)
Then all of three SConscript
files have the extension .scons
and break up the project into modules representing different functionality.
Google Test SConscript
file:
## \file gtest.scons
# \brief SConscript file that contains information for SCons build of
# GTest. Use Python syntax highlighting for source.
from build_config import *
Import('env')
## Compile Google Test from scratch.
#
GTestAllLib = env.Library('lib/libgtest.a', 'gtest-1.6.0/src/gtest-all.cc',
CPPPATH = GTestBuildIncDir,
CXXFLAGS = flags)
GTestMainLib = env.Library('lib/libgtest_main.a',
'gtest-1.6.0/src/gtest_main.cc',
CPPPATH = GTestBuildIncDir,
CXXFLAGS = flags)
Return('GTestAllLib', 'GTestMainLib')
Mixed-integer linear programming solver SConscript
file:
## \file milpSolver.scons
# \brief SConscript file that contains information for SCons build of
# mixed-integer linear programming solver module. Use Python syntax
# highlighting for source.
from build_config import *
Import('env')
## Compile MILP solver module and tests.
#
##milpSolver = env.Object('milpSolver.cpp',
## CPPPATH = milpIncDirs,
## LIBPATH = milpLibDirs,
## CXXFLAGS = flags)
milpSolverTest = env.Program('milpSolverUnitTest',
['milpSolverTest.cpp',
'milpSolver.cpp'],
CPPPATH = milpIncDirs,
LIBPATH = milpLibDirs,
LIBS = milpLibFlags,
CXXFLAGS = flags,
LINKFLAGS = ['-Wl,-rpath,' + OsiLibDir])
Return('milpSolverTest')
Chemistry engine SConscript
file:
## \file chemSolver.scons
# \brief SConscript file that sets up SCons build of chemistry solver module.
# Use Python syntax highlighting for source.
from build_config import *
Import('env')
## Compile CHEMKIN interpreter.
#
ckInterp = env.Program('ckinterp', ChemkinSourceDir + 'ckinterp.f')
## Enforce explicit dependence of CHEMKIN library on CHEMKIN
# parameter file 'ckstrt.f' because SCons' scanner won't pick it up.
env.Depends('cklib.f','ckstrt.f')
chemSolverTest = env.Program('chemSolverUnitTest',
['chemSolverTest.cpp',
'chemSolver.cpp',
'ckwrapper.f90'] + ChemkinSourceList,
CPPPATH = chemSolverIncDir,
LIBPATH = chemSolverLibDir,
LIBS = chemSolverLibFlags,
CXXFLAGS = flags,
FORTRANFLAGS = flags,
F90FLAGS = flags)
canteraGTest = env.Program('canteraGTest',
'canteraGTest.cpp',
CPPPATH = chemSolverIncDir,
LIBPATH = chemSolverLibDir,
LIBS = canteraGTestLibFlags,
CXXFLAGS = flags)
canteraMemTestLibFlags = CanteraTestingFlags
canteraMemTest = env.Program('canteraMemTest',
'canteraMemTest.cpp',
CPPPATH = chemSolverIncDir,
LIBPATH = chemSolverLibDir,
LIBS = canteraMemTestLibFlags,
CXXFLAGS = flags)
Return('chemSolverTest', 'canteraGTest')
The combination of these five files is probably a bit longer than the original long file, but it's easier for me to manage because I can separate the build into a configuration file, and a bunch of mostly uncoupled units, so I don't have to keep track of the whole build in my mind at one time.
Best Answer
The thing to remember about GUI code is that it is event-driven, and event-driven code is always going to have the appearance of a mass of randomly organized event handlers. Where it gets really messy is when you try to shoehorn non-event-driven code into the class. Sure, it has the appearance of providing support for the event handlers and you can keep your event handlers nice and small, but all of that extra support code floating around makes your GUI source seem bloated and messy.
So what can you do about this, and how can you make things easier to refactor? Well, I would first change my definition of refactoring from something I do on occasion to something I do continuously as I code. Why? Because you want refactoring to enable you to more easily modify your code, and not the other way around. I'm not simply asking you to change the semantics here, but asking you instead to do a little mental calisthenics in order to see your code differently.
The three refactoring techniques that I find I use most commonly are Rename, Extract Method, and Extract Class. If I never learned a single other refactoring, those three would still enable me to keep my code clean and well structured, and from the content of your question, it sounds to me like you will probably find yourself using the same three refactorings almost constantly in order to keep your GUI code thin and clean.
You can have the best possible separation of GUI and Business logic in the world, and still the GUI code can look like a code mine has been detonated in the middle of it. My advice is that it doesn't hurt to have an extra class or two to help you to manage your GUI properly, and this does not necessarily have to be your View classes if you are applying the MVC pattern - although frequently you'll find the intermediary classes are so similar to your view that you'll often feel an urge to merge them for convenience. My take on this is that it doesn't really hurt to add an additional GUI-specific layer to manage all of the visual logic, however you probably want to weigh up the benefits and costs of doing so.
My advice therefore is: