How can I improve this design to achieve a more loosely coupled system and better testability?
https://softwareengineering.stackexchange.com/questions/410199
-
11-03-2021 - |
Question
My question
I built an inverted pendulum on an Arduino using C (ie. everything was done procedurally). I'm trying to self study application design and would like to refactor my code into a more OO approach with SOLID, loose coupling, and testability in mind.
How can I improve this design to achieve a more loosely coupled system and better testability?
UML
Brief summary of classes
MotorController
- An interface for motor controllers.
DrokL928
- Is the motor controller I use, implements MotorController
.
Cart
- Is just a convenient wrapper for DrokL928
. Allows for more intuitive control of the cart.
Encoder
- An external library for reading values from rotary encoders.
EncoderWrapper
- A wrapper for Encoder
. That way I encapsulate the external API into one place.
StateVector
- Holds the current state data.
StateUpdater
- Processes encoder values from EncoderWrapper
and assigns them to StateVector
.
LQRController
- Computes the PWM signal (based on the current state) to send to the Cart in order to stabilize the pendulum. see: Wikipedia's linear-quadratic-regulator.
Specific design questions
StateUpdater
uses constantsIDLER_PULLEY_RADIUS
andSYSTEM_LOOP_RATE
. These smell like they don't belong insideStateUpdater
, but they are relevant to the calculation of the state. I suppose all of those private methods insideStateUpdater
could be put into a separate classStateCalculator
?LQRController
uses the constantpendulumBound
. That is, the LQR controller should only calculate the input if the pendulum angle is within a certain bound. For some reason I feel like this doesn't belong here, but maybe I'm wrong on that. For the sake of completeness, maybe I should add a bound for each variable inStateVector
.As of right now I can't instantiate
EncoderWrapper
into a test harness because it requires a reference to anEncoder
. And by extension, I can't instantiate aStateUpdater
into a test harness. How can I fix this?In the most ideal case, I would like for
StateVector
to not be so hardcoded and the variables should be open to modification. Therefore,LQRController
should have againVector
of any length (right now it is hardcoded to 4). Is this too much abstraction? If not, how can I go about achieving this? Now that I think about it, I believe it would be too much abstraction because then I'm not sure how theStateUpdater
would calculate the state on arbitrary state variables, because the algorithm is very specific.
Solution
One of the hallmarks of good design is that it is mostly self-explanatory, even to someone without much knowledge about the domain. It so happens that I had no idea what an inverted pendulum was before reading your question, so I tried to gather some insights from your diagram, but unfortunately Cart
seemed to be the only object that was possibly related to your problem domain.
After quickly reading about inverted pendulums on Wikipedia, the details of your design started to make more sense but then I realized that you were missing abstractions such as Pendulum
, and instead had the state and behavior scattered between StateVector
and StateUpdater
.
My suggestion would be to think about your system outside-in and focus on the concepts you are trying to model rather than how they are implemented. For example, you mentioned:
Cart
- Is just a convenient wrapper forDrokL928
. Allows for more intuitive control of the cart.
Cart
isn't just a "convenient wrapper", it is one of the primary things you are trying to model. A cart has a position and velocity, and its movement can be controlled. Encoder
and MotorController
are implementation details used to obtain the current state and control the movement, respectively.
Similarly, you would have a Pendulum
class with angle and velocity properties, again using an Encoder
internally to obtain these values.
And finally, an InvertedPendulum
class that represents the entire system consisting of a Cart
and a Pendulum
, and uses a Controller
to stabilize the system.
This eliminates StateVector
and StateUpdater
from your design since all the pieces have been moved into more meaningful objects.
As of right now I can't instantiate
EncoderWrapper
into a test harness because it requires a reference to anEncoder
. ... How can I fix this?
By creating an interface just like you did for MotorController
. The real implementation will call the external library, and you can create one or more fake implementations that will simply return canned responses for your tests.
OTHER TIPS
Not a full-fledged answer. But since you are designing an embedded system (and by coincidence I just worked on an Arduino supported pendulum): These are usually large state machines. The class design is rather trivial since the controllers have anyway similar interfaces. So that's the minor part. The big one is the desgin of the state machine. And of course since you are dealing with a real-time application you will find that you can not always follow the gods of good design in favor of needed performance :-/
Regaring test: This is especially tricky for RT applications. By adding test you distort the behavior of the system. E.g. I added a fast buffer logger that could eventually be read post mortem. Doing fancy debugging is simply not possible since physics does not allow to stop motion when you want to look at it.