This really depends. If the values your helpers operate on are primitives, then static methods are a good choice, as Péter pointed out.
If they are complex, then SOLID applies, more specifically the S, the I and the D.
Example:
class CookieJar {
function takeCookies(count:Int):Array<Cookie> { ... }
function countCookies():Int { ... }
function ressuplyCookies(cookies:Array<Cookie>
... // lot of stuff we don't care about now
}
class CookieFan {
function getHunger():Float;
function eatCookies(cookies:Array<Cookie>):Smile { ... }
}
class OurHouse {
var jake:CookieFan;
var jane:CookieFan;
var cookies:CookieJar;
function makeEveryBodyAsHappyAsPossible():Void {
//perform a lot of operations on jake, jane and the cookies
}
public function cookieTime():Void {
makeEveryBodyAsHappyAsPossible();
}
}
This would be about your problem. You can make makeEveryBodyAsHappyAsPossible
a static method, that will take in the necessary parameters. Another option is:
interface CookieDistributor {
function distributeCookies(to:Array<CookieFan>):Array<Smile>;
}
class HappynessMaximizingDistributor implements CookieDistributor {
var jar:CookieJar;
function distributeCookies(to:Array<CookieFan>):Array<Smile> {
//put the logic of makeEveryBodyAsHappyAsPossible here
}
}
//and make a change here
class OurHouse {
var jake:CookieFan;
var jane:CookieFan;
var cookies:CookieDistributor;
public function cookieTime():Void {
cookies.distributeCookies([jake, jane]);
}
}
Now OurHouse
need not know about the intricacies of cookie distribution rules. It must only now an object, which implements a rule. The implementation is abstracted away into an object, who's sole responsibility is to apply the rule. This object can be tested in isolation. OurHouse
can be tested with using a mere mock of the CookieDistributor
. And you can easily decide to change cookie distribution rules.
However, take care that you don't overdo it. For example having a complex system of 30 classes act as the implementation of CookieDistributor
, where each class merely fulfills a tiny task, doesn't really make sense. My interpretation of the SRP is that it doesn't only dictate that each class may only have one responsibility, but also that a single responsibility should be carried out by a single class.
In the case of primitives or objects you use like primitives (for example objects representing points in space, matrices or something), static helper classes make a lot of sense. If you have the choice, and it really makes sense, then you might actually consider adding a method to the class representing the data, e.g. it's sensible for a Point
to have an add
method. Again, don't overdo it.
So depending on your problem, there are different ways to go about it.
I don't subscribe to the school of thought which says "exceptions should only be for exceptional cases!" People are scared of exceptions for some reason. If you can't do what you said you're going to do, throw an exception - even if it's a common or expected failure.
I like this for a few reasons. It's impossible for the caller to ignore (someone could easily forget to inspect the return value of a method that indicates failure by a return code.) It's simple (no special placeholder values for missing results or hierarchies which need downcasting.) It's atomic (either my change succeeded or you tell me to get lost; I'm left in no doubt as to which it is.) It's granular (you can attach as much information as you like to the exception, and throw different exceptions for different failures.)
So I'd design your method like this:
public void ChangePassword(string password)
{
HttpResponse response = RequestPasswordChange();
if (AuthFailed(response))
throw new AuthorizationException();
if (PasswordWasNotChanged(response))
throw new InvalidNewPasswordException(
WasPasswordLongEnough(response),
DidPasswordContainNumbers(response)
);
}
"Succeed-or-throw" is a fine rule of thumb, and people are used to it, no matter how much they wave their arms. After all, Dictionary<TKey, TValue>
throws KeyNotFoundException
when you fail to index into it. A non-exceptional use of exceptions.
Best Answer
I would suggest to introduce a special result type, something along the lines of
I guess the usage in
BookRentCheck
is clear, it needs to return aRentalCheckResult
object instead of a string. This will make it possible to write unit tests forBookRentCheck
which are independent from spelling corrections or translations.RentalCheckResult
itself is simple enough that it does not require any unit tests for itself. If it seems necessary, theenum
can be replaced to a class hierarchy with subclassesRentalCheckResultPaymentUnclear
,RentalCheckResultCreditLimitReached
, and so on, whereNoOfBooks
will only exist as a member ofRentalCheckResultQuotaExceeded
.