Currently I don't understand the Use case concept enough good - too much abstract definitions but no particular examples. The section Use Cases of Chapter 20 Business Rules does not including any code or at least UML example.
The current question is discussing around below paragraph:
A use case is an object. It has one or more functions that implement the application-specific business rules. It also has data elements that include the input data, the output data, and the references to the appropriate Entities with which it interacts.
"Clean Architecture"
This raises a lot of questions, but according SO rules, I must to keep the topics focused on one question. I'll begin from: "Must the use cases extend the entities"?
Let us clarify the terminology first
- The business rules are consists from Enterprise (Critical) business rules and Application business rules.
- The only one type of Enterprise (Critical) business rules I know is the Entities. If nothing more exists, the Entity is also the synonym for Enterprise business rules and Critical business rules. In the languages supports OOP the Entities could and should be expressed by classes.
- The Entities include only data which existing without computerization. Same about actions expressed by the methods.
Entity is pure business and nothing more.
- The application business rules are containing the data which is not actual without computerization.
These rules would not be used in a manual environment, because the make sense only as part of an automated system.
I was though that Use cases in a synonym of application business rules. This answer let me know that I was wrong - the Use cases also including the interfaces of gateways (but not their implementations). What causes concerns is the "Use cases" and "Requests and response models" are separate sections of Chapter 20 Business rules.
I have not see the examples of Use cases classes. Below code examples are my improvisations based on a bare theory.
The examples where extending could be useful
Limitations of the system
Very strictly speaking, the these rules would make or save the business money, irrespective of whether the were implemented on a computer. They would make or save money even if they were executed manually.
Below TypeScript class is fully obeys to above concept because it contains only data which existing without computerization.
class Product {
public title: string;
public price__dollars__withoutTax: number; // The "__XX" is the Clarification postfix
public constructor(properties: Readonly<{ title: string; price__dollars__withoutTax: number; }>) {
this.title = properties.title;
this.price__dollars__withoutTax = properties.price__dollars__withoutTax;
}
}
Assume that I asked the customer "What the minimal and maximal characters count in the title"? and the customer has answered: "I have not thought about this. Decide it yourself while no inconveniences in the system you are developing".
I have checked the statistics and knew that most titles are less than, for example, 200 characters. With some margin, let it will be 300.
class Product {
public title: string;
public price__dollars__withoutTax: number;
public readonly TITLE_MINIMAL_CHARACTERS_COUNT: number = 2;
public readonly TITLE_MAXIMAL_CHARACTERS_COUNT: number = 300;
public constructor(properties: Readonly<{ title: string; price__dollars__withoutTax: number; }>) {
this.title = properties.title;
this.price__dollars__withoutTax = properties.price__dollars__withoutTax;
}
}
I expressed the above limitations with appropriate class fields, but these limitations does not exist without computerization - the enterprise (critical) business rules concept has been violated. To fix these issue, I must move these limitations to Use case class:
class Product {
public title: string;
public price__dollars__withoutTax: number;
public constructor(properties: Readonly<{ title: string; price__dollars__withoutTax: number; }>) {
this.title = properties.title;
this.price__dollars__withoutTax = properties.price__dollars__withoutTax;
}
}
class ProductUseCase extends Product {
public readonly TITLE_MINIMAL_CHARACTERS_COUNT: number = 2;
public readonly TITLE_MAXIMAL_CHARACTERS_COUNT: number = 300;
public constructor(properties: Readonly<{ title: string; price__dollars__withoutTax: number; }>) {
super(properties);
}
}
New fields required by the application
For some reasons, the identifiers are frequently required for entities in various applications. However, the ID is actual only for the application thus must appear in Use cases:
class Product {
public title: string;
public price__dollars__withoutTax: number; // The "__XX" is the Clarification postfix
public constructor(properties: Readonly<{ title: string; price__dollars__withoutTax: number; }>) {
this.title = properties.title;
this.price__dollars__withoutTax = properties.price__dollars__withoutTax;
}
}
class ProductUseCase extends Product {
public readonly ID: number = ProductUseCase.generateID();
private static counterForID_Generating: number = 0;
public constructor(properties: Readonly<{ title: string; price__dollars__withoutTax: number; }>) {
super(properties);
}
private static generateID(): number {
ProductUseCase.counterForID_Generating++;
return ProductUseCase.counterForID_Generating;
}
}
In this way, in most cases every entity from Critical business rules must be wrapped by Use case.
Analysis of example at figure 20.2 (Chapter 20, Use cases)
So, the "A use case is an object". It's hard to implement above use case with one function - it will be the class with one public and multiple private methods.
For this example, there is no need to extend LoanUseCase
from Loan
. The code draft will be like:
class Loan {
public principle: Loan.Principles;
public rate__percentage: number;
public period__months: number;
public constructor(constructorParameters: Loan.ContructorParameters) {
this.principle = constructorParameters.principle;
this.rate__percentage = constructorParameters.rate__percentage;
this.period__months = constructorParameters.period__months;
}
public makePayment(amount__dollars: number): void {
}
public applyInterest(): void { /* ... */ }
public chargeLetFee(): void { /* ... */ }
}
namespace Loan {
export type ContructorParameters = Readonly<{
principle: Loan.Principles;
rate__percentage: number;
period__months: number;
}>;
export enum Principles {
standard = "STANDARD",
forYoungFamilies = "FOR_YONG_FAMILIES"
}
}
type CustomerRawData = {
firstName: string;
lastName: string;
// ...
};
class LoanUseCase {
public static issueLoanOfPrimaryCourseIfPossible(customerRawData: CustomerRawData): Loan | null {
if (!LoanUseCase.isCustomerDataValid(customerRawData)) {
return null;
}
if (!LoanUseCase.isCreditScoreEnough(customerRawData)) {
return null;
}
// create new customer and save him to data store
return new Loan({
period__months: 12,
principle: Loan.Principles.standard,
rate__percentage: 15
});
}
private static isCustomerDataValid(customerRawData: CustomerRawData): boolean {
// validate name ...
// validate address, birthdate, etc.
return true;
}
private static isCreditScoreEnough(customerRawData: CustomerRawData): boolean {
// ...
return true;
}
}