I think it is necessary to consider a lot of "nothing" when writing code in practice, such as the value does not exist in a certain situation, or the result is not obtained even though the database is referenced.
After all, there are many techniques for expressing things that don't exist, such as null
, ʻOptional, and
NullObject pattern`.
I would like to summarize what I think about how to handle them.
Sure, I love ʻOptional and ʻEither
, but this time I'm not talking about technique.
Recently, even if I say "no" in one word, I feel that there are some types. This time, I would like to summarize the types of "nothing".
It's just a "classification I've considered at this point", but it makes the scope of responsibility for each package clear. We also believe that it will be easier to get closer to a design that allows proper unit testing, and to organize and maintain logic.
Of course, there are many different designs, so I can't say anything, but here we assume the following outline.
Should it be understood as Yurufuwa DDD style layer design?
... ??
name | Overview | Remarks |
---|---|---|
api | It is the starting point of processing and uses service It can be a URL or a command line tool |
Will not appear this time |
service | Handle domain and realize the processing that the system should do | |
domain | Describe business logic | |
repository | Interface for interacting with databases and external systems | Place as interface in domain |
mapper | Implement repository | Not implemented this time |
As usual, we will prepare a convenient theme. This time, it seems that a certain telecommunications carrier manages contracts for fixed lines and mobile lines.
We will realize a system that manages contracts with certain members.
Members can have up to one mobile line and one fixed line. (Fixed line will not appear this time)
Mobile line has up to 1 voice option, The voice option comes with up to one answering machine option.
Based on the above data structure, the following requirements are fulfilled.
Achieve the following four requirements.
Enter Member ID
for all processing.
Blatantly "not" is studded ...
domain
First of all, simply express mobile line-> voice option-> answering machine option
with ʻOptional. (This time, the technique of "nothing" does not have to be ʻOptional
, but since it is the easiest, I will describe everything below with ʻOptional. The essence is almost the same even with
null`.)
MobileLine.java
@AllArgsConstructor
public class MobileLine {
@Getter
private final MobileLineId id;
@Getter
private final MonthlyFee fee = new MonthlyFee(1500);
@Getter
private final Optional<VoiceOption> voiceOption;
}
VoiceOption.java
@AllArgsConstructor
public class VoiceOption {
@Getter
private final VoiceOptionId id;
@Getter
private final MonthlyFee fee = new MonthlyFee(700);
@Getter
private final Optional<AnswerPhoneOption> answerPhoneOption;
}
AnswerPhoneOption.java
@AllArgsConstructor
public class AnswerPhoneOption {
@Getter
private final AnswerPhoneOptionId id;
@Getter
private final MonthlyFee fee = new MonthlyFee(200);
}
It's simple.
It has a price you might need and up to one sub-element.
(MonthlyFee
and XxxId
are just primitive wrappers)
repository
Next is repository
. All you have to do is refer to the root class.
MobileLineRepository.java
public interface MobileLineRepository {
Optional<MobileLine> findMobileLine(UserId userId);
}
Members may not have a mobile line, so it is ʻOptional`. Image of foreign key reference.
service
Finally, there is service
, but here we just connect them together.
MobileLineService.java
public class MobileLineService {
private MobileLineRepository repository;
// 1.Check if you can apply for a mobile line
public boolean checkMobileLineApplicable(UserId userId) {
Optional<MobileLine> mobileLineOptional = repository.findMobileLine(userId);
return !mobileLineOptional.isPresent();
}
// 2.Check if you can cancel the answering machine option
public boolean checkAnswerPhoneCancellable(UserId userId) {
Optional<MobileLine> mobileLineOptional = repository.findMobileLine(userId);
MobileLine mobileLine = mobileLineOptional
.orElseThrow(() -> new RuntimeException("Mobile line not found"));
if (mobileLine.getVoiceOption().isPresent()) {
return mobileLine.getVoiceOption().get().getAnswerPhoneOption().isPresent();
} else {
return false;
}
}
// 3.Get the items you need to cancel your voicemail option
public AnswerPhoneOptionCancellation getAnswerPhoneOptionIdForCancellation(UserId userId) {
Optional<MobileLine> mobileLineOptional = repository.findMobileLine(userId);
MobileLine mobileLine = mobileLineOptional
.orElseThrow(() -> new RuntimeException("Mobile line not found"));
VoiceOption voiceOption = mobileLine.getVoiceOption()
.orElseThrow(() -> new RuntimeException("Voice option not found"));
AnswerPhoneOption answerPhoneOption = voiceOption.getAnswerPhoneOption()
.orElseThrow(() -> new RuntimeException("Answering machine option not found"));
return new AnswerPhoneOptionCancellation(
mobileLine.getId(),
voiceOption.getId(),
answerPhoneOption.getId()
);
}
// 4.Refer to the total value of all contracted monthly usage fees
public MonthlyFee totalMonthlyFee(UserId userId) {
Optional<MobileLine> mobileLineOptional = repository.findMobileLine(userId);
MobileLine mobileLine = mobileLineOptional
.orElseThrow(() -> new RuntimeException("Mobile line not found"));
if (mobileLine.getVoiceOption().isPresent()) {
if (mobileLine.getVoiceOption().get().getAnswerPhoneOption().isPresent()) {
return MonthlyFee.sum(
mobileLine.getFee(),
mobileLine.getVoiceOption().get().getFee(),
mobileLine.getVoiceOption().get().getAnswerPhoneOption().get().getFee()
);
} else {
return MonthlyFee.sum(
mobileLine.getFee(),
mobileLine.getVoiceOption().get().getFee()
);
}
} else {
return mobileLine.getFee();
}
}
}
You're done.
(You can move the code to MobileLine
a little more, but I've written it solidly in the service to make it easier to understand the whole picture of the process.)
I don't think there are many people who see this and feel "a wonderful code!" However, some people may not be able to figure out what is wrong and what to do.
So, from here, I would like to improve this code while interpreting the subject and explaining the code.
It's a bit of a leap, but I've been writing code for 1-2 years and I've been thinking a lot lately.
There are three types of "no"! That is. Let's think about each of them with the above theme and code.
The first is this.
In this example, the mobile line in "1. Check if you can apply for a mobile line" and The voice option and answering machine option of "4. Refer to the total value of all contracted monthly usage charges".
If these are not present, they will be treated as ** one pattern of normal system **. Basically this case is ** no exception **.
This case corresponds to ** one error pattern ** if it fits the above phrase. However, if you say an error in one word, it is indistinguishable from the third type described later, so we will call it a ** business error ** in the sense of ** an error expected in the service specifications **.
It may be a service error. In short, I think it's good if there are no discrepancies in your relatives. I call it a business error because it is an error that should be detected by business logic.
This error writes logic to detect and guard. I can't say for sure because it may depend on the design, but there are no exceptions in this case. ** **
In this example, the answering machine option in "2. Check if you can cancel the answering machine option" is equivalent. If there is an answering machine option, it is "normal", and if it is not, it is "one of the many other reasons why you cannot cancel".
The difference is the difference in subsequent processing.
There is no difference in the flow because A is a normal system with or without it. On the other hand, if there is B, do XX, if not, do XX instead, or if not, skip to ~ ~, and so on.
B is an image of branching with yes / no
in the flow diagram.
The last is an error ** that is not assumed (defined) in the service specifications. I call it ** system error ** because this happens mostly for system reasons.
For example, the DB connection fails, the external system call times out, or the value that should have been corrupted in the DB cannot be referenced. There will be data corruption / loss due to double submission or erroneous operation.
In a nutshell, it is an error that does not occur if the service is operating normally and you use it as expected.
In this case ** with exceptions ** would be appropriate. I don't catch it one by one.
In this example, the mobile line of "2. Check if you can cancel the answering machine option" Mobile line and voice option and answering machine option in "3. Get the items required to cancel the answering machine option", The mobile line in "4. Refer to the total value of all contracted monthly usage charges" is equivalent.
For example, in 2., the mobile line is (thematically) a certain premise, so it is unexpected that it is not there anymore. I don't know what happened. (A certain premise is, for example, a process called from a screen that can only be transitioned to a person with a mobile line. It is not unusual, isn't it?)
Unlike B, C does not appear in the flow chart with yes / no
.
For example, "yes / no with status inconsistency" or "yes / no with database connection failure" is not specified in the service specification flow diagram, right?
Also, unlike B, C is often unable to perform subsequent processing. Doesn't it make sense to check for voice options when you fail to browse your mobile line? In many cases, if it does occur, nothing will happen.
In other cases, B is reproduced but C is not.
B says, for example, "If you don't have the answering machine option, you can check how many times you can cancel ** and you will get the same result **". C may have something like "I was attacked and failed to connect to the database due to heavy load, but I dealt with it ** and succeeded when I reapplied **".
Even if I simply say "presence or absence of voice option", I found that the handling is different at that time.
So, this time, I would like to take the plunge and take the policy of "making a different class each time."
I will try to organize the necessary information as "No".
It doesn't do any calculations, so it's enough to know if it really exists.
There is no particular calculation here either, so there seems to be no required value.
Assuming that everything is here, we need ʻID` for all elements.
Here it is normal even if it is not below the mobile line, and in some cases the price of the element is required.
In this way, the MobileLine
that I made first actually looks like a union of four requirements.
MobileLine.java
@AllArgsConstructor
public class MobileLine {
@Getter
private final MobileLineId id;
@Getter
private final MonthlyFee fee = new MonthlyFee(1500);
@Getter
private final Optional<VoiceOption> voiceOption;
}
However, it turns out that ʻID and monthly charges are less likely to be required, and that
VoiceOption is not always ʻOptional
.
Certainly, if you have all the elements and set it to ʻOptional` in consideration of the possibility that it does not exist, you can meet all the requirements. However, in reality, this doesn't have much merit, and it just increases in spiciness. (I will touch on that at the end)
Therefore, from here, I will introduce "a class that focuses on the minimum elements to the last minute".
The number of classes will be very large, but I decided to take the plunge, so I will create a large number of classes.
No class required. The presence or absence of boolean
is sufficient.
MobileLineForAnswerPhoneCancellableCheck.java
@AllArgsConstructor
public class MobileLineForAnswerPhoneCancellableCheck {
@Getter
private final Optional<VoiceOptionForAnswerPhoneCancellableCheck> voiceOption;
}
VoiceOptionForAnswerPhoneCancellableCheck.java
@AllArgsConstructor
public class VoiceOptionForAnswerPhoneCancellableCheck {
@Getter
private final Optional<AnswerPhoneOptionForAnswerPhoneCancellableCheck> answerPhoneOption;
}
AnswerPhoneOptionForAnswerPhoneCancellableCheck.java
@AllArgsConstructor
public class AnswerPhoneOptionForAnswerPhoneCancellableCheck {
}
With the current specifications, it is possible to judge whether or not to apply based on the presence or absence, so there is no value at all. (It feels a little useless, but in reality, various statuses, contract continuation years, and various other values will be required, and I hope that such delusions will complement your brain.)
MobileLineForAnswerPhoneCancellation.java
@AllArgsConstructor
public class MobileLineForAnswerPhoneCancellation {
@Getter
private final MobileLineId id;
@Getter
private final VoiceOptionForAnswerPhoneCancellation voiceOption;
}
VoiceOptionForAnswerPhoneCancellation.java
@AllArgsConstructor
public class VoiceOptionForAnswerPhoneCancellation {
@Getter
private final VoiceOptionId id;
@Getter
private final AnswerPhoneOptionForAnswerPhoneCancellation answerPhoneOption;
}
AnswerPhoneOptionForAnswerPhoneCancellation.java
@AllArgsConstructor
public class AnswerPhoneOptionForAnswerPhoneCancellation {
@Getter
private final AnswerPhoneOptionId id;
}
This has a different atmosphere than before.
I have a ʻID that is only needed for this requirement, and all ʻOptional
s are missing.
MobileLineForTotalMonthlyFee.java
@AllArgsConstructor
public class MobileLineForTotalMonthlyFee {
@Getter
private final MonthlyFee fee = new MonthlyFee(1500);
@Getter
private final Optional<VoiceOptionForTotalMonthlyFee> voiceOption;
}
VoiceOptionForTotalMonthlyFee.java
@AllArgsConstructor
public class VoiceOptionForTotalMonthlyFee {
@Getter
private final MonthlyFee fee = new MonthlyFee(700);
@Getter
private final Optional<AnswerPhoneOptionForTotalMonthlyFee> answerPhoneOption;
}
AnswerPhoneOptionForTotalMonthlyFee.java
@AllArgsConstructor
public class AnswerPhoneOptionForTotalMonthlyFee {
@Getter
private final MonthlyFee fee = new MonthlyFee(200);
}
It also has a monthly fee that is only needed for this requirement and has a subordinate element in ʻOptional`.
Since the returned class has changed, so does the repository method.
MobileLineRepository.java
public interface MobileLineRepository {
boolean isMobileLineApplicable(UserId userId); //No need to get the class back
MobileLineForAnswerPhoneCancellableCheck findForCancellable(UserId userId);
MobileLineForAnswerPhoneCancellation findForCancel(UserId userId);
MobileLineForTotalMonthlyFee findForTotalMonthlyFee(UserId userId);
}
ʻOptional` is gone!
Where did ʻOptional disappear here, for example, from
MobileLineForAnswerPhoneCancellation.java`?
Instead of moving somewhere instead, it's really gone.
In the implementation class of Repository
, if a state inconsistency occurs, it will be an exception.
The part that was doing .orElseThrow ()
in the service layer is checked for inconsistency before being wrapped in ʻOptional`, and if it occurs, it will be an exception.
You must be able to get ()
later anyway, so let's make an exception when you find that you can't.
That way you don't have to keep it in domain
as ʻOptional` and check it later.
It's easy if you have all this.
I even think that the easiest one is service
in the first place.
You just use the domain
that you've worked so hard on.
MobileLineService.java
public class MobileLineService {
private MobileLineRepository repository;
// 1.Check if you can apply for a mobile line
public boolean checkMobileLineApplicable(UserId userId) {
return repository.isMobileLineApplicable(userId);
}
// 2.Check if you can cancel the answering machine option
public boolean checkAnswerPhoneCancellable(UserId userId) {
MobileLineForAnswerPhoneCancellableCheck mobileLine = repository.findForCancellable(userId);
return mobileLine.getVoiceOption().map(it -> it.getAnswerPhoneOption().isPresent()).orElse(false); //A little ingenuity with map or Else
}
// 3.Get the items you need to cancel your voicemail option
public AnswerPhoneOptionCancellation cancelAnswerPhoneOption(UserId userId) {
MobileLineForAnswerPhoneCancellation mobileLine = repository.findForCancel(userId);
return new AnswerPhoneOptionCancellation(
mobileLine.getId(), //No need to check isPresent
mobileLine.getVoiceOption().getId(), //The value is confirmed because the repository did not raise an exception
mobileLine.getVoiceOption().getAnswerPhoneOption().getId()
);
}
// 4.Refer to the total value of all contracted monthly usage fees
public MonthlyFee totalMonthlyFee(UserId userId) {
MobileLineForTotalMonthlyFee mobileLine = repository.findForTotalMonthlyFee(userId);
return MonthlyFee.sum(
mobileLine.getFee(), //A little ingenuity with map or Else
mobileLine.getVoiceOption().map(it -> it.getFee()).orElse(MonthlyFee.zero()), //It may be a little easier if you add it obediently as 0 yen
mobileLine.getVoiceOption().flatMap(it -> it.getAnswerPhoneOption()).map(it -> it.getFee()).orElse(MonthlyFee.zero())
);
}
}
The big difference from the first MobileLineService
is that it" does not check for system errors ".
"There shouldn't be an element" believes in the class returned by repository
.
So you can focus only on the calculations that have to be done for your requirements.
Now that I've come this far, there's one more thing I want to fix.
So you can focus only on the calculations that must be done for your requirements.
This is the business logic. You might think of it as "the necessary calculations to meet the service specifications".
You want to test the calculation, right? You should want to. It's impossible not to do it, right? Yes, I want to.
Here or so!
return mobileLine.getVoiceOption().map(it -> it.getAnswerPhoneOption().isPresent()).orElse(false);
Here or so!
return MonthlyFee.sum(
mobileLine.getFee(),
mobileLine.getVoiceOption().map(it -> it.getFee()).orElse(MonthlyFee.zero()),
mobileLine.getVoiceOption().flatMap(it -> it.getAnswerPhoneOption()).map(it -> it.getFee()).orElse(MonthlyFee.zero())
);
Are you confident in deploying without checking the operation? Is it absolutely okay?
If you want to pass all the patterns in the service
test, you have to prepare a lot of repository
return values.
It's mocking or registering dummy data in the database, but you can't do that.
Therefore, we should pay attention to the following description in the table of the first layer description.
domain Describe business logic
I will post it only once. This is the final improvement.
MobileLineForAnswerPhoneCancellableCheck.java
@AllArgsConstructor
public class MobileLineForAnswerPhoneCancellableCheck {
private final Optional<VoiceOptionForAnswerPhoneCancellableCheck> voiceOption;
public boolean isAnswerPhoneCancellable() {
return voiceOption.map(it -> it.isAnswerPhoneCancellable()).orElse(false);
}
}
VoiceOptionForAnswerPhoneCancellableCheck.java
@AllArgsConstructor
public class VoiceOptionForAnswerPhoneCancellableCheck {
private final Optional<AnswerPhoneOptionForAnswerPhoneCancellableCheck> answerPhoneOption;
public boolean isAnswerPhoneCancellable() {
return answerPhoneOption.isPresent();
}
}
AnswerPhoneOptionForAnswerPhoneCancellableCheck.java
public class AnswerPhoneOptionForAnswerPhoneCancellableCheck {
}
Ask the root class "Can you apply?"
MobileLineForAnswerPhoneCancellation.java
@AllArgsConstructor
public class MobileLineForAnswerPhoneCancellation {
private final MobileLineId mobileLineId;
private final VoiceOptionId voiceOptionId;
private final AnswerPhoneOptionId answerPhoneOptionId;
public AnswerPhoneOptionCancellation cancel() {
return new AnswerPhoneOptionCancellation(
mobileLineId,
voiceOptionId,
answerPhoneOptionId
);
}
}
I took the plunge and decided to stop the nesting structure and make it a flat class. I need them all anyway.
I just said "a collection of materials for information necessary for cancellation". If you tell the root class "Create the information necessary for cancellation", it will be assembled using the materials. You can't see what the material is specifically on the outside. It's encapsulated.
MobileLineForTotalMonthlyFee.java
@AllArgsConstructor
public class MobileLineForTotalMonthlyFee {
private final MonthlyFee fee = new MonthlyFee(1500);
private final Optional<VoiceOptionForTotalMonthlyFee> voiceOption;
public MonthlyFee getTotalMonthlyFee() {
return MonthlyFee.sum(
fee,
voiceOption.map(it -> it.voiceOptionAndAnswerPhoneOptionFee()).orElse(MonthlyFee.zero())
);
}
}
VoiceOptionForTotalMonthlyFee.java
@AllArgsConstructor
public class VoiceOptionForTotalMonthlyFee {
private final MonthlyFee fee = new MonthlyFee(700);
private final Optional<AnswerPhoneOptionForTotalMonthlyFee> answerPhoneOption;
public MonthlyFee voiceOptionAndAnswerPhoneOptionFee() {
return MonthlyFee.sum(
fee,
answerPhoneOption.map(it -> it.answerPhoneOptionFee()).orElse(MonthlyFee.zero())
);
}
}
AnswerPhoneOptionForTotalMonthlyFee.java
@AllArgsConstructor
public class AnswerPhoneOptionForTotalMonthlyFee {
private final MonthlyFee fee = new MonthlyFee(200);
public MonthlyFee answerPhoneOptionFee() {
return fee;
}
}
I was a little confused here, but prepare methods that return the sum of the prices below themselves, and the upper class will add to itself. Well, the details inside are trivial, and here too, all you have to do is say "summ up" to the root class.
I think it's better to divide the repository according to the requirements of the class to be returned. It happened when I proceeded with package organization. (Recently, I'm still not very confident.)
Therefore, the repository is also divided into as many classes as the number of created classes.
I will omit the code.
MobileLineService.java
public class MobileLineService {
private MobileLineRepository mobileLineRepository;
private MobileLineForAnswerPhoneCancellableCheckRepository forAnswerPhoneCancellableCheckRepository;
private MobileLineForAnswerPhoneCancellationRepository forAnswerPhoneCancellationRepository;
private MobileLineForTotalMonthlyFeeRepository totalMonthlyFeeRepository;
// 1.Check if you can apply for a mobile line
public boolean checkMobileLineApplicable(UserId userId) {
return mobileLineRepository
.isMobileLineApplicable(userId);
}
// 2.Check if you can cancel the answering machine option
public boolean checkAnswerPhoneCancellable(UserId userId) {
return forAnswerPhoneCancellableCheckRepository.find(userId)
.isAnswerPhoneCancellable();
}
// 3.Get the items you need to cancel your voicemail option
public AnswerPhoneOptionCancellation cancelAnswerPhoneOption(UserId userId) {
return forAnswerPhoneCancellationRepository.find(userId)
.cancel();
}
// 4.Refer to the total value of all contracted monthly usage fees
public MonthlyFee totalMonthlyFee(UserId userId) {
return totalMonthlyFeeRepository.find(userId)
.getTotalMonthlyFee();
}
}
Now you just request the root class from the repository
and request the root class to do the math! !!
That's the only service.
You have separated the calculation and processing.
The calculation test that moved to the domain class can be done manually without inputting dummy data in the database.
You can new
directly under the root class and test as many as you like.
I posted the code three times.
domain
: I have everything and consider everythingrepository
: Just an accessor to domain
service
: System error checking, processing, calculationdomain
: Specialized in requirements and has the required values in the proper formrepository
: System error checkingservice
: Processing, calculationdomain
: Calculationrepository
: System error checkingservice
: ProcessingLooking at it like this, you can see that the responsibility was gradually separated from the service and moved to the domain. Or rather, the first service, after all, was too hard.
Let's look at it from a slightly different perspective
get ()
, ʻorElseThrow () `@ Getter
in the domain classget ()
, ʻorElseThrow () `is gone@ Getter
is goneAs a general rule, ʻOptional should not do
get () . (I will omit the details, but basically it is better to look for some improvement plan with the runner-up after
get ()`.)
As we improved, the get ()
and ʻorElseThrow () disappeared, so there were no exceptions around ʻOptional
.
Also, by taking the form of commanding the root class, @ Getter
has disappeared.
This means that the encapsulation was successful.
The last class created is separated by requirement, so even if an additional value is required only for a certain requirement, the modification will affect only that class. The service class just says "~~" to the root class without knowing the elements inside.
Even if the application date is added to the cancellation judgment, there is no need to repair the service, and recombining evaluation is unnecessary just in case of price calculation.
There are no exceptions
Not specified as a branch in the service specification flow diagram
There are no exceptions
Specified as a branch in the service specification flow diagram
Subsequent processing after occurrence is also processed normally according to the specifications.
Since it is written in the service specification, it is detected by domain
which writes business logic
If the data status is the same, the same business error will always occur
Use exceptions
Not specified as a branch in the service specification flow diagram
In many cases, subsequent processing cannot be continued after it occurs.
Since it is not written in the service specification and involves exceptions, it is guarded by mapper
instead of detecting by domain
which writes business logic.
Even if the data status is the same, it may or may not occur due to load or communication interruption. (If the data is inconsistent, it will always occur in the same way)
First of all, in the case of specialized classes, modifying one requirement does not affect another.
The union class sets ʻOptionalto all values that may only be held for certain requirements. (Or
List`)
This is, for example, "The cancellation application date is ʻempty except when canceling, but when canceling cancellation, there should be a value on the cancellation application date." ʻOptional
that requires a large amount of prerequisite knowledge such as" It is always an empty list immediately after application, but when canceling it should be a list containing 1-3 elements "is created. (Or List
)
This is super spicy. (true story)
Considering application, change, cancellation, cancellation of them, optional items, etc., the union class is covered with ʻOptional. ʻOr Else Throw ("There should be ~ ~")
Can't you easily imagine being covered?
I don't think exceptions in domain
should be made.
Since it is a calculation to do with domain
, state inconsistency is out of the question,
This is because I think that the error should be expressed by the value because the error is expected as long as it is calculated as business logic.
Let's push all the exceptions to mapper
. (Slightly violent)
What about service
?
service
may be appropriate for catch
. For example, "If there is a system error, an alarm will be issued."
This time there was only one route class, but for example Like "I have a contract for an answering machine option" & "No unpaid amount for an answering machine option" & "Two years have passed since the contract" Other repositories and root classes will also appear if it is a compound condition.
I think it is the responsibility of the service layer to handle them and to handle large domains that require all of those domains. now.
It's like a class that doesn't know most of the specific calculations and system errors that the end should consider, but just knows the "processing order" and the "whole characters".
In the first example, it was necessary to input dummy data in the database in order to cover the essential calculation logic patterns.
In the last example, the calculation is separated into domain
, so you can new
and test it with any value you like.
I wrote at the beginning:
It's just a "classification I've considered at this point", but it clarifies the scope of responsibility for each package. We also believe that it will be easier to get closer to a design that allows proper unit testing, and to organize and maintain logic.
Actually, I didn't think much at this point, so I thought I'd erase it if I couldn't connect well, but I was relieved that it was unexpectedly connected.
System errors, processing and calculations are now in each layer, can be unit tested, and requirement changes no longer affect other requirements!
that's all.
Could you introduce that there are quite a lot of merits just by considering "nothing"?
ʻOptional is convenient, and above all, I love the failure type Soyu, but I don't make anything unreasonably ʻOptional
I think that when compared with the demands, it will naturally come closer to a good design.
Recommended Posts