I have a class named Product
, which takes a product model number and number of wheels for that product as part of its creation information, and then it instantiates the product by doing a computation on the number of wheels and on various base parameters
For example, Product(5, 22)
creates a model 5 product with 22 wheels on it, where that particular product’s weight and dimensions are computed from the number of wheels and from the base specifications for that model number. Right now base specifications are part of the class itself and are hardcoded into the class, along with the computation formulas.
My Issue: in the (legacy) code I am working with, I cannot always use the model number directly. It may not be available. Instead, an unrelated product_id
is given, where the link between product_id
and model_number
is in the database.
I want to keep my class as simple as possible. As such I do not want to introduce another constructor that allows product_id
as an input parameter, in addition to the existing model_number
, as it will be duplication of code. Also, I am not too keen on putting database logic inside the class, but maybe I can, as thinking about it now, this may be a good candidate for a database-wrapper class, i.e. active record (where my base specifications can be moved off from the code and into the database).
Question: How do I create and return the object while following good object oriented principles when the model_number
I typically use for its creation is not available, but another parameter is available instead (product_id
in this case), which links 1:1 to model_number
?
Sample solutions that I don’t quite like:
- Do not involve the database — since I am working with just a few Products at this time, and Products do not change often, I can create a “conversion function” that serves as a map between
product_id
andmodel_number
and not touch the database. Similarly, create aConvert
object that has same functionality and use it before creating the object. Problem: duplicating DB functionality in the code. - Put DB functionality inside code and add alternate constructor. Problem: multiple entry-points of Product creation for the class create code duplication.
Update:
There basically are 3 aspects here:
- base data (whether hardcoded or part of the database)
- finding base data by various parameters be it model number or other identifying information
- doing computation using base data as per model number
I’d like to build upon the solution @TMN is presenting and take it a little further to answer your SRP problem. Putting database code inside the ProductFactory
would be non-sensical so to encapsulate this responsibility you can create a separate class ProductModelNumberProvider
with just one method findByProductId(int productId)
.
The ProductFactory
should provide a setter setProductModelNumberProvider(ProductModelNumberProvider lookupProvider)
which will assign the provider internally and call findByProductId()
when appropriate.
You will have a ProductFactory
which is responsible for building the product and a ProductModelNumberProvider
which will do the database interaction if needed.
Have a look at your StackOverflow question for a more concrete example.
4
Maybe create a ProductFactory
, with methods to create a Product
given a number of wheels and either a product ID or a model number. Given a model number it defers to the class’ constructor, and if given a product ID it performs the DB lookup, gets the model number, then defers to the class constructor.
2
You don’t want to duplicate the DB mapping product_id -> model_number
by hardcoding the logic, that makes sense. So the first thing you need is a function like
int mapProductIdToModelNumber(int product_id)
which does this mapping by getting the information from the database (consider caching when this function is used very often).
What remains is how to make this function available to your Product
class without tight coupling. You have the following alternatives:
-
pass the function directly to the code blocks whereever
Product
objects are created (depends on your programming language how to accomplish that) -
implement the function in a
ProductFactory
class. DeriveProductFactory
from an interfaceIProductFactory
and pass an object of this type to the code areas where you currently want anew Product()
. Use the factory the way @TMN suggested.
That way, your legacy code stays decoupled from the database, since it won’t depend directly from a database-bound ProductFactory
, only from IProductFactory
, which can be easily mocked out (for example, for testing purposes).
9
Maybe this — give up the idea of a Product
class and create a DB-wrapper where I will say $pf = ProductFinder()
, then do $pf->findById()
and $pf->findByModel()
as needed. Problem: Where do I put my computation code? It can’t be in the same place, as it it will violate Single Responsibility Principle. aka, computation and data handling should be separate. Then maybe Product
class can be the computational part, and ProductFinder
be the database pulling part.
What I actually ended up doing:
At first I was wondering if I am better off with such complexity.
Simpler way was:
There is another pattern here that I could use that keeps things in one class: https://stackoverflow.com/a/2175213/2883328. Breaks SRP.
$product = Product::fromProductId($productId, $wheels);
SRP way (chosen)
ProductData
class that just has the data (can be swapped by database later)ProductFactory
class that uses Provider class to look up the model numberProductNumberProvider
class that provides model number when given product idProduct
class that does loads ProductData and does computations
Code:
$provider = new ProductModelNumberProvider();
$factory = new ProductFactory($provider);
$product = $factory->constructFromProductId($productId, $wheels);