Encapsulating Business Logic
How I might encapsulate some business logic? (Business logic defined here). I tend to follow a mental checklist of sorts in order to make sure I am implementing encapsulation correctly. Paraphrasing Einstein: if I understand something well enough then I should be able to explain it simply. So what follows below is my attempt to explain encapsulating business logic simply in order to see if I understand it well enough. Following the explanation is a quick example.
Encapsulating Business Logic Checklist
1. Clarify the logic
2. Capture the logic
3. Make the logic easily found
4. Make the logic easily testable
5. Make the logic hard to mess up
1. Clarify the Logic
Write something down. Even in an ideal word where all requirements and business rules are written down in formal documents, we often need to restate words in the context of the system to make sure we are on the same page as the business analyst. I have done everything from typing up an official document to just having the business expert look over my shoulder at the comments of a routine. Whatever fits you and your context is great, just do it.
2. Capture the Logic
Regardless of where I put the logic at the end of the day, first I just want to start capturing it in code, thinking about it from a code point of view. This is the first stab at design separated from all other concerns as I start writing code to implement the logic. The classes involved are determined as I go through typical black-box thinking: Given A, B, and C how do I determine D? This naturally leads to the next step of making this logic easily found.
3. Make the Logic Easily Found
I need to make this logic easily found for the next guy who is going to update this logic or gasp, fix my bugs. As an object-oriented developer, I know the business logic belongs in an object, but which one? The short answer is the obvious one. Often a particular object seems to be the most natural choice and we don’t give it much thought. However, when multiple objects are involved the general rule is to pick the object that has the most data involved. If multiple objects appear equally involved THEN JUST PICK ONE, you’re thinking about it too much now.
4. Make the Logic Easily Testable
Business logic is arguably the most important code you will write. It determines how much should be paid or charged, calculates values, determines answers, figures whether a missile should zig or zag, etc, etc, etc. If you are going to bother writing any tests, test your business logic. Furthermore, make the tests so fast and easy to run that no one has a problem with running the tests all the time. If you find you can’t easily and repeatedly test your business logic because it requires database connectivity, complex object creation, or whatever, then think about changing the design. It’s also true that making your logic easily testable with solid tests can go a long way towards clarifying the logic and design. This is one of the touted benefits of test-driven development.
5. Make the Logic Hard to Mess Up
Those who make the leap to programming objects as opposed to just programming with objects will see a huge improvement in quality simply because things become harder to mess up. There are many things we can do, but two common approaches include making objects for any business concept and making objects immutable. For example, if a password has to be more than 3 characters but less than 10 and include letters and numbers then create a Password object that encapsulates these business rules and has methods for accessing them. It should be impossible for invalid passwords to be bouncing around the business layer of an application. If an Email requires an IP address of an SMTP server to send itself, then do not make a property called IP address that may or may not be set at the whim of the caller, or get changed somehow during a process. Make the IP address required upon construction and unchangeable. The users of that Email object are much less likely to mess things up.
For the sake of discussion, a simple case (A simple cases because we can focus more on the process and less on the complexities of the example). If you use your imagination you could apply the checklist to more complex scenarios as well.
I have a class I’m creating called Message Header, and I need to increment a value (Message Number) each time I create an instance of the class based on a previous message number.
Clarify the logic
After a few emails we established the following details about the business logic
- At this point we don’t know where the last message number comes from
- Message Number is an attribute of Message Header
- The new number should always be the next even number (2 more than the previous)
- The new number should always be between 2 and 9998 (inclusive)
- After 9998 the message number should reset to 2
Capture the logic
I create a function called DetermineNextMessageNumber that takes in the previous message number. Code is written to encapsulate the logic above. Defensive code is added to throw an exception with the simple exception handling if expected assumptions are not met – like assuming the previous message number is always even.
Make the logic easily found
The obvious choice was the MessageHeader class, so I add the method as an internal/friend static/shared method to the MessageHeader class.
Make the logic easily testable
I create 5 quick tests to test the logic: a normal case - passing in the number 6, two edge cases – passing in zero and 9998, and two possible extremes – passing in -50 and 55223. This prompts me to ask the business analyst what to do in these extreme cases. She claims both are exceedingly unlikely. So I figure my defensive coding with the simple exception handling will be perfect for when the cases do happen. My tests require no database access and no complicated object building. No changes seem to be needed, the method is easily testable.
Make the logic hard to mess up
I don’t create objects with settable attributes by default so I know the user could not easily forget to set the Message Number. However, I realize that someone could create a Message Header and not realize there is business logic that determines the correct next number. To fix this I change the constructor on the Message Header class to take in the PreviousMessageNumber. The constructor will then call the DetermineNextMessageNumber making sure that a coder cannot generate a Message Header without applying the appropriate logic. Since the constructor handles the logic I realize I can reduce scope issues by making my DetermineNextMessageNumber routine private. For half a second I think that maybe I should make a MessageNumber object that will guarantee a valid integer between 2 and 9998 and thus separate concerns more, etc. However, the expert said it is rare, and I am throwing a simple exception in case it does happen, I think the design is good enough for now.
Get your logic right, get it in code in the correct place with good tests, and design so that the next guy will have a hard time messing it up.
When I see the code of developers trying to do more than just object-based code the most common blunder seems to be leaving business logic all over the place. The most common misplacement seems to be in user interfaces, however, well-meaning developers often move the logic out of user interfaces but then misplace the logic in factory code, service classes, stored procedures, etc.When object-thinking is applied I think logic starts to naturally fall into place without much thinking or time-consuming analysis.