How can I plan my software to avoid excessive rewriting and interdependencies
I'm writing a motor controller that has a couple of interfaces (buttons, Bluetooth, haptic knobs) which is a task that is steadi开发者_运维问答ly growing to be a larger than I figured. I've tried to just go at it by starting with low-level modules (e.g. write code to talk on the I2C bus), then ones above that (code to talk to a particular device on the I2C bus...), but all too often I have to dive back down to my lower modules to handle quirks I didn't accommodate. This either takes a long time or I get really hack-ish code.
My target is an 8-bit MCU, so bottom-up seems like I can exploit the hardware better. If I go top-down I don't have any structure to build on or test/debug.
I've tried drawing up some overall diagrams and ones for particular levels/drivers, but I'm not sure how to structure them so I can be very systematic about it and avoid missing the odd signal that needs to go up through 2-3 layers.
I guess this is the reason for a CS degree? I'm an electrical engineer :P
It sounds like you are on the right track. Sometimes no amount of planning is going to prevent you from having to redesign or refactor parts of a system at a later date. Try some of the following tips:
- Keep your code in modules separated by logical functions.
- Do not duplicate code, instead design reusable methods for shared functionality.
- Try to avoid the temptation to add hacks for special cases. Eventually this will become unmaintainable. Instead, adjust and refactor small sections as soon as possible. Trying to do a large re-design at the end will be more difficult.
- Don't try to over-design the system from the start, as you might just be wasting your time when you get to the actual implementation.
- Keep the lower levels as simple as possible, then build more advanced capabilities on top.
- Document your functions and write a some unit tests, especially after adding complex conditional statements.
- Try to catch errors as high up the stack as possible. For example doing input validation and checking return values. This will make debugging easier.
When working in multi-tiered code, it's tempting to dive into a lower tier when the API doesn't let you do exactly what you want it to do. This is especially difficult when writting mulitple tiers at the same time.
Here's a few suggestions:
- Treat all other tiers than the one you are working on as if they are sealed. Created by another company, developer, etc. Resist the urge to modify another tier to solve a problem in your current tier.
- Create a "sibling tier" to the one you are working on. This is hard to describe in abstract sense, but lets say your lower tier is a business layer, and the higher tier is a UI, create ANOTHER UI layer for a different application. Now having two consumers of the same API can help point out what should be in each layer.
- Try alternating the order in which you work on tiers. For example, in some apps I find it more useful to design the UI first, then work my way down to the business layer/database to make that UI work as designed. Other times, it makes better sense to stat with the data model and work up to a UI. But the point is, you "think" of an API differently in these two scenarios. And having looked at multi tiered code from both angles helps.
- Experience counts. Sometimes just making the mistake of overly coupled code is the only way to really learn to avoid it. Instead of planning on your app being perfect, plan on it being imperfect. By that I mean first set up a rapid develop/test/refactor cycle, so that you can quickly adapt to mistakes that you wont see until after you've made them. This is also an area where "throwaway prototyping" comes in handy. Make a simple rough draft, learn from it, and throw it away. The throw away part is important. Even if its amazing, start building another one from scratch. You will inevitable make it better (and in my expirience, more organized) based on what you learned from the prototype.
It's really more a matter of experience than your degree. If you are still learning things about how to control the hardware, then of course your code is going to change. I wouldn't agonize over that. However, since what you are really doing is prototyping, you should be ready to refactor the code once you have it working. Remove redundancies, compartmentalize data and functionality, and organize your interfaces so that it makes sense.
My experience is that device driver code needs top-down and bottom-up design/implementation, what I call outside-in. You know what the user is going to want to do and you can write those interfaces, and you know what the low-level driver needs to do and you write that. If they don't meet well in the middle, rethink your module layout.
For improvement, subject your design and code to review by people with more experience. Leave ego out of it and just get their take on the problem. You might also read a book on object-oriented analysis and design (I used to like Peter Coad's books. I don't know who's writing them now). A good one will show examples of how to partition a problem into objects with clear roles and responsibilities.
The other piece, once you have finished prototyping the drivers and know how to control the hardware, is to make sure you have detailed requirements. Nothing twists code more than discovering requirements while you're writing. You might also try learning UML and designing with diagrams before writing code. This doesn't work for everyone. Also note that you don't need to be coding in a language that supports object-oriented constructs to use OOD.
If your problem is how to build proper abstractions, which seems to be the case, I think that the most important thing you can do to learn (besides asking for design-code reviews/read books/read code) is TO THINK HARD BEFORE YOU START WRITING CODE.
It usually happens that you start with a rough idea of what you want and how it should be done and then go on to write code. Later on you discover that you didn't think things through and you have several gaping holes which now, because you have invested time in writing the code, are difficult to patch, which leads to either wasted time or hacky code.
Think hard about how to create a design that can easily handle changes. For example, encapsulate the data that needs to travel between layers, so if you later discover you missed a crucial parameter you can easily add it to the structure without having to modify code everywhere.
Try to "run the design in your head", several times, until you are reasoanly sure that you have considered most important details and you can tell (this is the most important part, because you'll always miss things or requirements will change) that if you missed something you can tweak the modules with relative ease.
UML can help you structure the way to think about the design. It's certainly no panacea but it shows the different criteria to consider when creating a software design.
It's a bit like the classic chess teacher advice: "sit on your hands" :-)
Drivers are amenable to a layer approach.
Your drivers could have several "classes":
- Input only
- Output only
- both I and O.
They should have a standardized interface, e.g.:
GetKnobLevel()
GetNextKeyboardButton
Or, another approach is to have something like
syscall(DRIVERNUMBER, command)
putting parameters/results in specified registers.
It is a simple, usable approach. A more complex variant might be to have circular queues between your hardware communicating code and your software communicating code, instead of registers.
Here is the mental model I am using:
---
Application
---
OS
---
Driver communicators
---
drivers
---
hardware
There is a tightly defined, no-variance interface between each layer (I keep imagining a layer cake with thick frosting between layers...). The OS may not exist for you, of course.
If your MCU supports software and hardware interrupts like the x86 CPU, you can use those to isolate the drivers from the driver communicators.
This is a bit of an "overengineery" solution, to be honest. But, in a situation where your complexity is getting significant, it's easier to have tight engineering than to have loose engineering.
If you are communicating between layers, you can use a global variable for each communication "channel", and access it in a disciplined fashion, using only functions to access it.
Typically you will want to do paper designs at some level, some exploratory work, and redesign before you really set out to code your project. Flowcharts and bus-transition diagrams work well here.
This is the approach I've favored in my embedded systems work and it works well for me.
Also - this problem space isn't well explored by traditional computer science curricula. It's much less forgiving than the Web or modern operating systems.
In my opinion, the most important aspects of well architectured code are low degree of coupling and separation of side-effects from state.
Also - code is a craft. Don't think you can make the perfect solution from the beginning. Be prepared to change your code as you learn new things. In fact - Make sure that you embrace change it self as part of your work.
Not to be too glib, but a quote from The Mythical Man-Month comes to mind: "Plan to throw one away; you will anyhow."
The corollary of which is "Make it work. Make it right. Make it fast."
I suppose this makes me an advocate of doing some design up-front, but not getting paralyzed by it. It doesn't have to be perfect the first time through. Plan to refactor. Hopefully you will have written things in such a way that you're not really throwing much code away, but rearranging things in a more pleasing manner.
精彩评论