There are a lot of articles out there that try to explain what is polymorphism. Basically, it is one of OOP pillar that every programmer should know and respect.
This article is an attempt to explain why.
Let say you have a system with a user registration feature. The stakeholder said that the system must
- Validate the user information. Show the error page if the user inputs any invalid data.
- Save the user avatar
- Save the user information
- Response to the user
The most straightforward code that reflects the requirement will look like this
class User {
public User Register(string name, string password, stream avatar) {
// Validate
var user = new User(name, password);
if (!user.valid()) throw new InvalidUserException();
// Uploading
var avatarPath = Uploader.upload(avatar);
user.avatarPath = avatarPath;
// Save
user.save();
// Return response
return user;
}
}
As you can see, the flow of this code reflect the requirement from business stakeholder.
Ok. So what happened if there is an infrastructure requirement. We hate vendor lock-in. We want to be able to switch between two image storage types, Google Storage and Amazon S3?
Let me take another naive approach. We create an uploader class that can upload to each type of storage.
class User {
public User Register(string name, string password, stream avatar) {
// Validate
var user = new User(name, password);
if (!user.valid()) throw new InvalidUserException();
// Upload
String avatarPath = "";
if (Environment.get("STORAGE") == "S3") {
// Setup S3 Uploader
// Setup credentials
// Setup some more
avatarPath = Uploader.uploadS3(avatar);
} else {
// Setup Google Uploader
// Setup credentials
// Setup some more
avatarPath = Uploader.uploadGoogleCloud(avatar);
}
user.avatarPath = avatarPath;
// Save
user.save();
return user;
}
}
But then, this code does not fully reflect the business requirement anymore. The infrastructure related concern is embedded in this chunk of code. There is a credential setup that different for each storage provider. There is a connectivity setup different for each storage provider. And so-on.
Now, any developer who wants to work with this method must understand the system's infrastructure to some degree.
That seems complicated and time-consuming. What if we want to allow developers to work with this chunk of code without understanding those infrastructure details?
We can do this.
// Different uploader
interface IUploader {
public string Upload(Stream fileContent);
}
class S3Uploader: IUploader {
public string Upload(Stream fileContent) { // Something }
}
class GoogleCloudUploader: IUploader {
public string Upload(Stream fileContent) { // Something }
}
// Factory. Create uploader according to environment
class Uploader {
public static IUploader CreateUploaderByEnvironment() {
if (Environment.get("STORAGE") == "S3") {
var result = new S3Uploader();
// Setup S3 Uploader
// Setup credentials
// Setup some more
return result;
} else {
var result = new GoogleCloudUploader();
// Setup Google Uploader
// Setup credentials
// Setup some more
return result;
}
}
}
And by having these Uploader classes with the same interface, we can change a code in the User class to be
class User {
public User Register(string name, string password, stream avatar) {
// Validate
var user = new User(name, password);
if (!user.valid()) throw new InvalidUserException();
// Uploading
var avatarPath = Uploader.CreateByEnvironment().upload(avatar);
user.avatarPath = avatarPath;
// Save
user.save();
return user;
}
}
And now, this code reflects business requirements again.
Developers who don't understand the infrastructure detail can work and modify the code without breaking the infrastructure requirement, as long as they still use Uploader.CreateByEnvironment()
.
Let take a look at what we did.
- We made interchangeable uploader classes, S3Uploader and GoogleCloudUploader. Both adhere to the same interface. This practice is called Polymorphism.
- We created a method that will choose a class implementation according to the environment. We called this a factory pattern.
Using these practices, we create a code that swiftly reflects the business requirement inside the User class while hiding an infrastructure requirement in another class.
Polymorphism allow use to hide some detail and highlight some detail.
For example, in this scenario, in a procedural programming paradigm without Polymorphism, we cannot make a code that swiftly reflects the business requirement.
This technique ultimately allows you to organize your codebase in different ways.
If we look at programming industry history, Polymorphism was promoted in the day where we still design a bunch of classes from the requirement and hand each class with specs over to a big group of programmers to work on, without knowing what how the overall system going is.
And as you can see, how can we make a developer working on the user registration feature without knowing the infrastructure detail in the example above. You can see that this is a perfect tool for this job.
I don't do Polymorphism just for the sake of becoming "a professional developer who deeply understands and respects Object-oriented design".
That has no meaning or value in itself.
And so should you. You should not do Polymorphism just because everyone said so or padding in the resume.
Why do I care about OOP, especially Polymorphism?
Because I know that when I do it right, these things will happen:
From business perspective, I can hide infrastructure detail.
From infrastructure perspective, I can hide business detail.
I can take a notebook to the meeting room with the business stakeholders and listen to what they are talking about. My code and the diagram they display on the Powerpoint presentation will look exactly the same.
I can take a notebook to the meeting room with the infrastructure team. Open another file. And the diagram on the wall will match my code perfectly.
It will be just a different file and a different class.
I can use my code as a source of truth to discuss everything with every related party and department. I don't need any extra documentation to discuss with different teams.
Many programmers hate having a conversation with the user because it feels like they do not understand the system. They talk in different languages, use other terms, and have different wavelengths to programmers who actually work on the codebase.
But what if I say that you can make codebase reflect user thought
With a clean codebase that reflects how user think system work (with some detail hiding in the lower level), you allow programmers and users to speak in the same wavelength with the same understanding.
Programmers will enjoy conversation with the user more and more.
Believe me. I did.
And this is the end game of Polymorphism done right.
Top comments (10)
I think the initial code is the cleanest, because you don't care about environments at all. it is even more polymorphic. The initial code just
Uploader.upload(avatar)
vsUploader.CreateByEnvironment().upload(avatar)
The upload function can hide the environment decision away just as good.
if it internally use use the different classes and
CreateByEnvironment()
does not matter.Did you solved the chicken and egg problem, first there was the chicken factory builder constructor initializer configuration manager.
This might sound very mean, but i mean it. I thought about it. I do not even think this is necessary a bad thing. It is just a different way of working. My point is, if it is cheap to implement the amazon and google uploader in the firat place, then it will later also be cheap to change.
In this OOP world, you worry that you can not handle the uploader logic anymore, because it got to complex, and the solution is (and at this point I need your help to understand it right) to make it overly complex in the first place.
I thank you Valentin to enter this discussion, your points got me a bit further, but I worry to run into an impasse.
Agree
However, now you've mixed all concerns into one big
Uploader
and coupled them together. This means if one concern changes you need to check all of them again. While I get the point I think this post might be misleading :) I've recently blogged about this topic. WDYT? philgiese.com/post/what-is-cohesio...I don't understand what do you mean by mixed all concern into one big Uploader. IMO, the Uploader classes are responsible for uploading to corresponding file storage and Uploader, as a static class, responsible for selecting appropriate uploader implementation. So I don't see how it is mixed together.
Can you clarify it a little bit more?
I have to apologize. I must have misread something. My comment really doesn't make much sense if I read your article again. Excuse my mumbling :D
This should definitely be paired with Factory Pattern to create proper
IUploader
implementation, which as mentioned should be hidden inside theUpload()
function.It could as well be handled by IoT DI container by injecting the proper
IUpload
implementation intoAvatarUploader
class constructor given the fact we read storage provider config at application startup and register its implementation with the DI.I believe what I implemented is already a Factory pattern. Essentially, I have a method that instantiates a proper
IUploader
implementation. This is a factory pattern. I just don't name it "Factory".As a polyglot developer, I can assure you that not every ecosystem name this
Factory
. For example, in the Ruby ecosystem, they prefer to use the termfor
. As in rubyblog.pro/2016/10/factory-metho...I intentionally refrain from using the term Factory because I want the reader to think beyond factory = design pattern = good architecture. I saw many developers moved from C# or Java to another language and start complaining about the lack of design patterns and good OOP practice, which is false. It is just that patterns are named differently.
Also, I don't see why do we need DI container here. I know that it is pretty popular and de facto standard for C# to have DI container inject every configuration into a bunch of classes at startup time and select proper implementation, but is it applicable to this application?
Maybe yes. Maybe no. I would like to hear an argument about it.
The main message here is I want our ecosystem to refrain from blindly following standard practices just because everyone said so or just because everyone is doing it.
Instead of doing Polymorphism just because it is a pillar of OOP and if you are OOP developer it is wrong not to use it, I assert that I do Polymorphism because I want my code to precisely reflect how stakeholders think about our system. The benefit is that it makes communication between developers and stakeholders way easier. It is a tangible benefit.
I am happy to hear your argument toward DI container. But as your comment, I don't see any argument yet. Let's talk about the benefit of using a DI container here, or we can talk about which circumstance we should use a DI container.
Hi,
thank you for your feedback. I didn't want to be too critical. What I see as one weak point is (and it has already been mentioned in previous post) that now you tightly coupled everything together.
Now your factory pattern is strictly part of User class and User is not based on any Interface thus it is not replaceable by any mock/stub implementation for testing.
More test friendly solution (the same applies and also answers your question about DI / IoT) would be to base it on an Interface. This way your method can be safely called from production code and at the same time from a test routine by injecting production code or test stub - to be able to test domain specific behavior without uploading an avatar image to production servers.
I know in the end this makes a lot of boilerplate but if your project ever grows to size when changing one thing might have any impact on other parts of code, especially in a team of multiple developers, you'd rather have tests ready to discover it before deployment.
I hope you find this argument reasonable.