It starts with an Order
that adds new Tests
with a name (string) using a TestFactory
class, that uses reflection to instantiate the Test
.
The TestFactory
looks up for all Tests
in the assembly that are marked using a TestAttribute
attribute with a Name.
- Using a
TestFactory
with reflection allows it to add new tests class and do not touch anything else, like plugins that extend functionality. - Using attributes instead of a property that must be overridden allows the
TestFactory
to have a dictionary with name and Tests without instantiating anything, only whenCreateTest
in theTestFactory
is executed then a new instance of the test is created. - There are 2 tests to look up for
Test
withoutTestAttribute
or multipleTests
with the same name. Note: using a more conservative way, it could be done using an abstract Name property that must be overridden inTest
instances, but imply thatTestFactory
must instantiate all tests (with a new parameterless constructor) only to discover theTest
names. Developers must implement a name to compile but repeatedTest
name errors are not discovered in compilation time, so a test must be placed as well. The attribute solution in my opinion is more elegant.
Test
is an abstract class that has some common properties, each test type (BiochemistryTest
, ImmunologyTest
, MicrobiologyTest
, HematologyTest
) are abstract classes as well derived from Test
. Each real test will be a class implementing one of the test types.
The Test
declares an abstract SetCalculation
method that all instances must implement. This decision is not in the documentation but I understood that Operations
set a result that an actual Test
must interpret, so Operations
call the SetCalculation
to give the result and the Test
set it's inner properties in consequence.
To a Test
it can be added Operations
in the same way, using the operation name that an OperationFactory
will look up in the assembly to add a new instance. The Operation
has an abstract Execute
method that receives a generic Test
, an implementation of Operation
will implement an Execute
that decides if a common operation is done to the Test
or maybe cast the Test
to an actual test type -like BiochemistryTest
- to do a more specialized operation.
I do not split Operation
classes in something like GenericOperation
, BiochemistryOperation
, ImmunologyOperation
... because:
- a developer could create a specific operation and forget to create the generic one causing that adding an operation to a test then no operation is found.
- An operation code will be spread in some different classes, it will introduce some complexity.
- Introduces a Parallel Inheritance Hierarchies code smell.
- I don't have more information, if the operation code is big or the same operation for a Biochemistry or Immunology doesn't have anything in common then a hierarchy of
Operation
mimicking the test type class will be ok.
When the Order
is fully created it can be Executed
, that calls the Execute
in all tests that call the Execute
in all operations that invoke the SetCalculation
in the Test
that sets the inner properties of the tests.
To maintain the solution simple, I didn't include the test module that would manage all Orders
, neither the Patient's sample that is related to the Tests
.
- New tests and operations are just new classes. No more changes are needed. Reflection will do instantiating and unit testing will ensure correctness.
- Each operation code has all code and are defined independently.
- Each particular tests has the code to interpret the results (
SetCalculation
).- Test type hierarchy has only properties, maybe it will expand if test types evolve.
- Bridge: instead of having tests with the operations code (properties and behaviours all together), they are two different classes:
Test
usesOperations
with an abstraction that have all the operations allowed. I know that the pattern does not fit perfectly because test call operations that call tests again, but the main idea remains, each class is separated to not bloat the class. - Factory: to allow creating new elements (and with reflection to do not tight the factory with the instances)
- Plugin structure, it is not a design pattern, but guides the general structure: an interface -a abstract class in this case- that must be implemented to extend the functionality.
- SOLID
- TDD (started with inside out and changed to outside in, the github history will show)
- Semantic commit messages
- Object callisthenics
Using DDD, the domain is quite small (one bounding context) so I only used to guide the folder structure: Domain, Domain services and Exceptions. I do not include Repositories or Proxies.
Class names are took from the documentation.
Disclaimer: I completely invented the current procedural code, no refactor strategy could be done without seeing the proper code.
In some conditions, no static or "new" code, or third party code (like saving to a database) must be executed, testing cannot be done with it
This code has to be mocked in tests so the idea is move the code to a new class with an interface, the class will be injected to the code, and the code will use the interface.
In some cases, it is necessary to create seams for code that cannot be moved.
Some tests must be placed (characterization test or golden master, depending of the code). It will use mocks/stubs to provide or collect information and to disallow doing permanent operations like saving data or sending emails. Use any code coverage tool to look up relevant code not tested: SonarQube, OpenCover, Coverlet, dotCover... and repeat until a good safety net is placed.
Note: each operation will be a small one, no new functionality are introduced. The main idea is do not break the tests, so each operation must live with the old code implying that in one moment everything could be duplicated (in some new and old code)
The code will mimic the new structure using only one test (with all test code) and one operation (with all operations code), after that, some specialized tests or operations will be created moving code from the generic code.
- Join operations code as much as possible
- Join test properties
- Join SetCalculation code (the part of operations that set test parameters)
- Move test and SetCalculation to a new generic Test class (move code to a new class): it will have all tests properties with all SetCalculation code, the old code will create instances of this test, run the operations and set the result to it.
- Inject the Test Factory
- Call to test factory to create the test, if no test could be created use the previous generic test class
- Create specific tests, one at a time, moving the code from the generic test to specialized tests- Inject the Operation Factory
- Call the operation factory, if no code is returned then old code is executed
- Move operations to new operation's classes, one at a time, using the same strategy that tests: inject the operation factory, call it, if no operation is found use the old code, move common operation code to the new specialized classes