I decided to build a basic eCommerce application in c++ as a learning experience. In the past, I've gone with a fat controller, skinny model approach, and while it has worked, my results always end up messy and the code is/was kind of individual to the applications. To try something new, I wanted to incorporate the single responsibility principle (as much as possible) and group my application into service layers.
My current code can be found on Github.
Continuing with the actual architecture:
Entities: Base data-holder class.
Services: Handle all operations for a particular entity. Building, creating, updating, finding, and more.
Validators: Check entity data to ensure it meets any requirements. Interface is currently a single method, Validate(void * v)
. Implementation casts the passed in pointer to the appropriate entity.
int main(int argc, char ** argv)
{
Pluto::Controller::AddSingleProduct * controllerAddSingleProduct = new Pluto::Controller::AddSingleProduct();
controllerAddSingleProduct->ProcessRequest();
return 0;
}
The ProcessRequest method called above:
void Pluto::Controller::AddSingleProduct::ProcessRequest()
{
unsigned int productId = 1;
std::string productName = "Test product.";
unsigned int productPrice = 10;
unsigned int productQuantity = 10;
unsigned int productType = 1;
unsigned int productStatus = 1;
Pluto::Service::Product * productService = new Pluto::Service::Product();
productService->SetProductValidator(new Pluto::Validator::Product());
if (productService->Create(productService->Build(productId, productName, productPrice, productQuantity, productType, productStatus)))
{
this->ErrorView();
return;
}
this->SuccessView();
}
The Pluto::Service::Product
methods called above:
Pluto::Entity::Product * Pluto::Service::Product::Build(unsigned int id, std::string name, unsigned int price, unsigned int quantity, unsigned int type, unsigned int status)
{
Pluto::Entity::Product * p = new Pluto::Entity::Product();
p->SetId(id);
p->SetName(name);
p->SetPrice(price);
p->SetQuantity(quantity);
p->SetType(type);
p->SetStatus(status);
return p;
}
unsigned int Pluto::Service::Product::Create(Pluto::Entity::Product * product)
{
if (product == NULL)
{
return 1;
}
if (this->productValidator)
{
if (this->productValidator->Validate(product))
{
return 1;
}
}
return 0;
}
void Pluto::Service::Product::SetProductValidator(Pluto::Validator::Product * productValidator)
{
this->productValidator = productValidator;
}
And finally, the Pluto::Validator::Product
methods:
unsigned int Pluto::Validator::Product::Validate(void * v)
{
Pluto::Entity::Product * p = (Pluto::Entity::Product *) v;
if (p == NULL)
{
this->messageContainer->AddMessage
(
Pluto::Core::Message::TYPE_ERROR,
Pluto::Validator::Product::ERROR_DEFAULT,
"Invalid product.",
(void *) p
);
return 1;
}
if (p->GetId() < 1)
{
this->messageContainer->AddMessage
(
Pluto::Core::Message::TYPE_ERROR,
Pluto::Validator::Product::ERROR_ID,
"Invalid ID.",
(void *) p
);
}
if (p->GetName().length() < 3)
{
this->messageContainer->AddMessage
(
Pluto::Core::Message::TYPE_ERROR,
Pluto::Validator::Product::ERROR_NAME,
"Invalid name.",
(void *) p
);
}
if (p->GetPrice() < 1)
{
this->messageContainer->AddMessage
(
Pluto::Core::Message::TYPE_ERROR,
Pluto::Validator::Product::ERROR_PRICE,
"Invalid price.",
(void *) p
);
}
if (p->GetQuantity() < 1)
{
this->messageContainer->AddMessage
(
Pluto::Core::Message::TYPE_ERROR,
Pluto::Validator::Product::ERROR_QUANTITY,
"Invalid quantity.",
(void *) p
);
}
if (p->GetType() < 0)
{
this->messageContainer->AddMessage
(
Pluto::Core::Message::TYPE_ERROR,
Pluto::Validator::Product::ERROR_TYPE,
"Invalid type.",
(void *) p
);
}
if (p->GetStatus() < 0)
{
this->messageContainer->AddMessage
(
Pluto::Core::Message::TYPE_ERROR,
Pluto::Validator::Product::ERROR_STATUS,
"Invalid status.",
(void *) p
);
}
if (this->messageContainer->GetNumMessages())
{
return 1;
}
return 0;
}
The messageContainer
within Pluto::Validator::Product
, and all validators, for that matter is a very basic array-like class. Calling AddMessage
places the data passed in at the current position and then increments. The details of it can be found in the repository linked above.
I feel my controllers are a lot more maintainable and manageable like this. The product service seems flexible in that I can switch the validator as necessary to test or deal with different validation rules by subclassing. I'm also comfortable with how the validator class worked out. It's really basic, but it seems isolated and only handles its one job.
The problem I'm running into is, how do I get messages/warnings/errors/etc back out to the controller efficiently? The message container class seen above was an attempt to solve this problem. It works great if I instantiate the validator in the controller and use it, then utilize the message container it provides in the views. I'm having trouble seeing a good way to do this in the product service, and then passing the data back out to the controller.
Below is a quick example of how I might have done validation before:
void AddProductController()
{
Pluto::Entity::Product * p = new Pluto::Entity::Product();
p->SetId(1);
p->SetName("Example Product");
// Continue populating product as before.
unsigned int validationErrors = 0;
unsigned int maxValidationErrors = 5;
unsigned int numValidationErrors = 0;
std::string errorStrings[maxValidationErrors];
if (p->GetId() < 1)
{
validationErrors = 1;
errorStrings[numValidationErrors] = "The product ID was invalid.";
numValidationErrors++;
}
if (p->GetName().length() < 3)
{
validationErrors = 1;
errorStrings[numValidationErrors] = "The product name was too short.";
numValidationErrors++;
}
// Continue checking the product data.
if (validationErrors)
{
for (unsigned int i = 0; i < numValidationErrors; i++)
{
std::cout << "Error: " << errorStrings[i] << "\n";
}
return;
}
std::cout << "Product was validated successfully!\n";
}
Worth noting, the service might have other things it does as well that need to spit back messages or errors to the client. For example, persisting the product on disk somehow, which could succeed or fail for any number of reasons. In the future, I may want to expand my views to use HTML or some type of layout as well, so it seems I'd need to be able to detect certain kinds of messages I'm expecting for positioning.
Am I correct in thinking that my approach to the services and validators is worthwhile? Any suggestions on how to handle the message/warning/error issue discussed above is really appreciated. It's something that's holding me back right now.