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.
Simple Example
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.
Summary
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.