One level of abstraction per function - Simplification
Writing code is all about abstractions. Abstraction helps programmers write better, well-designed code.
One of the main principles that every developer needs to follow for reducing code complexity and avoiding code smells is the fundamental principle called “One level of abstraction” or better known as “SLA - Single Level of Abstraction.” It’s a powerful concept, and like all other powerful concepts, it comes with a price. If not used well, it can also cause damage.
The main idea of this principle is to avoid mixing multiple levels of abstraction in one method. As with any other principle, this one is not mandatory for use, but the benefits from following it are enormous. Most importantly, it can help increase the readability and simplicity of the code.
Excellent code readability and simplicity are often the most difficult to achieve, but always striving for them is priceless for the future of programming.
If followed, this principle ensures that functions do one and only one thing, and they do it well.
Even though it sounds simple, in reality, this principle can be hard to understand and follow.
In the examples below, I will explain this principle in detail through practical examples.
“Mixing levels of abstraction within a function is always confusing.” - Robert C. Martin
Simplification
Let’s try to simplify a couple of examples to help break down and understand this principle.
Order processing
In this example, we will explain the principle in the context of order processing, specifically the order shipping feature. Let’s do it:
-
def ship_order if @order.get_payment_state === 'paid' && @order.get_shipment_state != 'shipped' @order.set_as_shipped() _email_template = 'order_shipped' _email_content = @emailTemplateBuilder.build( _email_template, @order ) @emailService.notify( _email_content, Array([@order.get_email()]) ); _tracking_number = @trackingNumberService.fetch_tracking_number( @order ); @order.set_tracking_number(_tracking_number); @order.save(); end end -
public function shipOrder(): void { if ( $this->order->getPaymentState() === 'paid' && $this->order->getShipmentState() !== 'shipped' ) { $this->order->setAsShipped(); $emailTemplate = 'order_shipped'; $emailContent = $this->emailTemplateBuilder->build( $emailTemplate, $this->order ); $this->emailService->notify( $emailContent, [$this->order->getEmail()] ); $trackingNumber = $this->trackingNumberService->fetchTrackingNumber( $this->order ); $this->order->setTrackingNumber($trackingNumber); $this->order->save(); } } -
func ShipOrder(order Order) { if order.GetPaymentState() == "paid" && order.GetShipmentState() != "shipped" { order.SetAsShipped() emailTemplate := "order_shipped" emailContent := emailTemplateBuilder.Build( emailTemplate, order, ) emailService.Notify( emailContent, []string{order.GetEmail()}, ) trackingNumber := trackingNumberService.FetchTrackingNumber( order, ) order.SetTrackingNumber(trackingNumber) order.Save() } } -
def ship_order(self): if (self.order.get_payment_state() == 'paid' and self.order.get_shipment_state() != 'shipped'): self.order.set_as_shipped() _email_template = 'order_shipped' _email_content = self.email_template_builder.build( _email_template, self.order ) self.email_service.notify( _email_content, [self.order.get_email()] ) _tracking_number = self.tracking_number_service.fetch_tracking_number( self.order ) self.order.set_tracking_number(_tracking_number) self.order.save() -
shipOrder() { if ( this.order.getPaymentState() === 'paid' && this.order.getShipmentState() !== 'shipped' ) { this.order.setAsShipped(); const emailTemplate = 'order_shipped'; const emailContent = this.emailTemplateBuilder.build( emailTemplate, this.order ); this.emailService.notify( emailContent, [this.order.getEmail()] ); const trackingNumber = this.trackingNumberService.fetchTrackingNumber( this.order ); this.order.setTrackingNumber(trackingNumber); this.order.save(); } } -
public void shipOrder(Order order) { if ( order.getPaymentState().equals("paid") && !order.getShipmentState().equals("shipped") ) { order.setAsShipped(); String emailTemplate = "order_shipped"; String emailContent = emailTemplateBuilder.build( emailTemplate, order ); emailService.notify( emailContent, order.getEmail() ); String trackingNumber = trackingNumberService.fetchTrackingNumber( order ); order.setTrackingNumber(trackingNumber); order.save(); } } -
void ShipOrder(Order order) { if ( order.GetPaymentState() == "paid" && order.GetShipmentState() != "shipped" ) { order.SetAsShipped(); string emailTemplate = "order_shipped"; string emailContent = emailTemplateBuilder.Build( emailTemplate, order ); emailService.Notify( emailContent, order.GetEmail() ); string trackingNumber = trackingNumberService.FetchTrackingNumber( order ); order.SetTrackingNumber(trackingNumber); order.Save(); } } -
fn ship_order(order: &mut Order) { if order.get_payment_state() == "paid" && order.get_shipment_state() != "shipped" { order.set_as_shipped(); let email_template = "order_shipped"; let email_content = email_template_builder.build( email_template, order ); email_service.notify( &email_content, vec![order.get_email()] ); let tracking_number = tracking_number_service.fetch_tracking_number( order ); order.set_tracking_number(&tracking_number); order.save(); } }
-
def ship_order if @order.get_payment_state === 'paid' && @order.get_shipment_state != 'shipped' @order.set_as_shipped() _email_template = 'order_shipped' _email_content = @emailTemplateBuilder.build( _email_template, @order ) @emailService.notify( _email_content, Array([@order.get_email()]) ); _tracking_number = @trackingNumberService.fetch_tracking_number( @order ); @order.set_tracking_number(_tracking_number); @order.save(); end end -
public function shipOrder(): void { if ( $this->order->getPaymentState() === 'paid' && $this->order->getShipmentState() !== 'shipped' ) { $this->order->setAsShipped(); $emailTemplate = 'order_shipped'; $emailContent = $this->emailTemplateBuilder->build( $emailTemplate, $this->order ); $this->emailService->notify( $emailContent, [$this->order->getEmail()] ); $trackingNumber = $this->trackingNumberService->fetchTrackingNumber( $this->order ); $this->order->setTrackingNumber($trackingNumber); $this->order->save(); } } -
func ShipOrder(order Order) { if order.GetPaymentState() == "paid" && order.GetShipmentState() != "shipped" { order.SetAsShipped() emailTemplate := "order_shipped" emailContent := emailTemplateBuilder.Build( emailTemplate, order, ) emailService.Notify( emailContent, []string{order.GetEmail()}, ) trackingNumber := trackingNumberService.FetchTrackingNumber( order, ) order.SetTrackingNumber(trackingNumber) order.Save() } } -
def ship_order(self): if (self.order.get_payment_state() == 'paid' and self.order.get_shipment_state() != 'shipped'): self.order.set_as_shipped() _email_template = 'order_shipped' _email_content = self.email_template_builder.build( _email_template, self.order ) self.email_service.notify( _email_content, [self.order.get_email()] ) _tracking_number = self.tracking_number_service.fetch_tracking_number( self.order ) self.order.set_tracking_number(_tracking_number) self.order.save() -
shipOrder() { if ( this.order.getPaymentState() === 'paid' && this.order.getShipmentState() !== 'shipped' ) { this.order.setAsShipped(); const emailTemplate = 'order_shipped'; const emailContent = this.emailTemplateBuilder.build( emailTemplate, this.order ); this.emailService.notify( emailContent, [this.order.getEmail()] ); const trackingNumber = this.trackingNumberService.fetchTrackingNumber( this.order ); this.order.setTrackingNumber(trackingNumber); this.order.save(); } } -
public void shipOrder(Order order) { if ( order.getPaymentState().equals("paid") && !order.getShipmentState().equals("shipped") ) { order.setAsShipped(); String emailTemplate = "order_shipped"; String emailContent = emailTemplateBuilder.build( emailTemplate, order ); emailService.notify( emailContent, order.getEmail() ); String trackingNumber = trackingNumberService.fetchTrackingNumber( order ); order.setTrackingNumber(trackingNumber); order.save(); } } -
void ShipOrder(Order order) { if ( order.GetPaymentState() == "paid" && order.GetShipmentState() != "shipped" ) { order.SetAsShipped(); string emailTemplate = "order_shipped"; string emailContent = emailTemplateBuilder.Build( emailTemplate, order ); emailService.Notify( emailContent, order.GetEmail() ); string trackingNumber = trackingNumberService.FetchTrackingNumber( order ); order.SetTrackingNumber(trackingNumber); order.Save(); } } -
fn ship_order(order: &mut Order) { if order.get_payment_state() == "paid" && order.get_shipment_state() != "shipped" { order.set_as_shipped(); let email_template = "order_shipped"; let email_content = email_template_builder.build( email_template, order ); email_service.notify( &email_content, vec![order.get_email()] ); let tracking_number = tracking_number_service.fetch_tracking_number( order ); order.set_tracking_number(&tracking_number); order.save(); } }
The code above makes it crystal clear that the method used for shipping orders is doing a lot. In other words, it has multiple levels of abstractions inside method implementation.
The following points represent different levels of abstraction that need to be extracted from the ship order method:
- The code responsible for checking if the order is ready for shipping.
- Building the email template needed for shipped order notification.
- Sending the notification from inside the method.
- Fetching and attaching the tracking number to the order.
Let’s do the work and apply SLA principle to improve the method.
-
def ship_order if (self.order_not_shippable()) return; end @order.set_as_shipped() self.notify_customer(); self.track_order(); @order.save(); end def order_not_shippable return (@order.get_payment_state != 'paid' || @order.get_shipment_state == 'shipped') end def notify_customer _email_template = 'order_shipped' _email_content = @emailTemplateBuilder.build( _email_template, @order ) @emailService.notify( _email_content, [@order.get_email()] ) end def track_order _tracking_number = @trackingNumberService.fetch_tracking_number( @order ) @order.set_tracking_number(_tracking_number) end -
public function shipOrder(): void { if ($this->orderNotShippable()) { return; } $this->order->setAsShipped(); $this->notifyCustomer(); $this->trackOrder(); $this->order->save(); } private function orderNotShippable(): bool { return $this->order->getPaymentState() !== 'paid' || $this->order->getShipmentState() === 'shipped'; } private function notifyCustomer(): void { $emailTemplate = 'order_shipped'; $emailContent = $this->emailTemplateBuilder->build( $emailTemplate, $this->order ); $this->emailService->notify( $emailContent, [$this->order->getEmail()] ); } private function trackOrder(): void { $trackingNumber = $this->trackingNumberService->fetchTrackingNumber( $this->order ); $this->order->setTrackingNumber($trackingNumber); } -
func ShipOrder(order Order) { if OrderNotShippable(order) { return } order.SetAsShipped() NotifyCustomer(order) TrackOrder(order) order.Save() } func OrderNotShippable(order Order) bool { return order.GetPaymentState() != "paid" || order.GetShipmentState() == "shipped" } func NotifyCustomer(order Order) { emailTemplate := "order_shipped" emailContent := emailTemplateBuilder.Build( emailTemplate, order, ) emailService.Notify( emailContent, []string{order.GetEmail()}, ) } func TrackOrder(order Order) { trackingNumber := trackingNumberService.FetchTrackingNumber( order, ) order.SetTrackingNumber(trackingNumber) } -
def ship_order(self): if self.order_not_shippable(): return self.order.set_as_shipped() self.notify_customer() self.track_order() self.order.save() def order_not_shippable(self): return (self.order.get_payment_state() != 'paid' or self.order.get_shipment_state() == 'shipped') def notify_customer(self): _email_template = 'order_shipped' _email_content = self.email_template_builder.build( _email_template, self.order ) self.email_service.notify( _email_content, [self.order.get_email()] ) def track_order(self): _tracking_number = self.tracking_number_service.fetch_tracking_number( self.order ) self.order.set_tracking_number(_tracking_number) -
shipOrder() { if (this.orderNotShippable()) { return; } this.order.setAsShipped(); this.notifyCustomer(); this.trackOrder(); this.order.save(); } orderNotShippable() { return this.order.getPaymentState() !== 'paid' || this.order.getShipmentState() === 'shipped'; } notifyCustomer() { const emailTemplate = 'order_shipped'; const emailContent = this.emailTemplateBuilder.build( emailTemplate, this.order ); this.emailService.notify( emailContent, [this.order.getEmail()] ); } trackOrder() { const trackingNumber = this.trackingNumberService.fetchTrackingNumber( this.order ); this.order.setTrackingNumber(trackingNumber); } -
public void shipOrder(Order order) { if (orderNotShippable(order)) { return; } order.setAsShipped(); notifyCustomer(order); trackOrder(order); order.save(); } private boolean orderNotShippable(Order order) { return !order.getPaymentState().equals("paid") || order.getShipmentState().equals("shipped"); } private void notifyCustomer(Order order) { String emailTemplate = "order_shipped"; String emailContent = emailTemplateBuilder.build( emailTemplate, order ); emailService.notify( emailContent, order.getEmail() ); } private void trackOrder(Order order) { String trackingNumber = trackingNumberService.fetchTrackingNumber( order ); order.setTrackingNumber(trackingNumber); } -
void ShipOrder(Order order) { if (OrderNotShippable(order)) { return; } order.SetAsShipped(); NotifyCustomer(order); TrackOrder(order); order.Save(); } bool OrderNotShippable(Order order) { return order.GetPaymentState() != "paid" || order.GetShipmentState() == "shipped"; } void NotifyCustomer(Order order) { string emailTemplate = "order_shipped"; string emailContent = emailTemplateBuilder.Build( emailTemplate, order ); emailService.Notify( emailContent, order.GetEmail() ); } void TrackOrder(Order order) { string trackingNumber = trackingNumberService.FetchTrackingNumber( order ); order.SetTrackingNumber(trackingNumber); } -
fn ship_order(order: &mut Order) { if order_not_shippable(order) { return; } order.set_as_shipped(); notify_customer(order); track_order(order); order.save(); } fn order_not_shippable(order: &Order) -> bool { order.get_payment_state() != "paid" || order.get_shipment_state() == "shipped" } fn notify_customer(order: &Order) { let email_template = "order_shipped"; let email_content = email_template_builder.build( email_template, order ); email_service.notify( &email_content, vec![order.get_email()] ); } fn track_order(order: &mut Order) { let tracking_number = tracking_number_service.fetch_tracking_number( order ); order.set_tracking_number(&tracking_number); }
It looks so much better!
Every line in the ship method is now at the same level of detail as the other lines. This means they are all on the same level of abstraction.
User Registration
In this example, we will demonstrate the principle using a user registration feature. This will show how to properly separate concerns when creating a new user account.
-
def register_user if @user_data.get_email != nil && @user_data.get_password != nil && @user_data.get_password.length >= 8 _hashed_password = @passwordHasher.hash(@user_data.get_password) @user.set_password(_hashed_password) @user.set_email(@user_data.get_email) @user.set_name(@user_data.get_name) @user.save() _verification_token = SecureRandom.hex(32) @user.set_verification_token(_verification_token) @user.save() _email_template = 'welcome_email' _email_content = @emailTemplateBuilder.build( _email_template, @user ) @emailService.send( _email_content, @user.get_email() ) _log_message = "User registered: " + @user.get_email() @logger.info(_log_message) end end -
public function registerUser(): void { if ( $this->userData->getEmail() !== null && $this->userData->getPassword() !== null && strlen($this->userData->getPassword()) >= 8 ) { $hashedPassword = $this->passwordHasher->hash( $this->userData->getPassword() ); $this->user->setPassword($hashedPassword); $this->user->setEmail($this->userData->getEmail()); $this->user->setName($this->userData->getName()); $this->user->save(); $verificationToken = bin2hex(random_bytes(16)); $this->user->setVerificationToken($verificationToken); $this->user->save(); $emailTemplate = 'welcome_email'; $emailContent = $this->emailTemplateBuilder->build( $emailTemplate, $this->user ); $this->emailService->send( $emailContent, $this->user->getEmail() ); $logMessage = "User registered: " . $this->user->getEmail(); $this->logger->info($logMessage); } } -
func RegisterUser(userData UserData) { if userData.GetEmail() != "" && userData.GetPassword() != "" && len(userData.GetPassword()) >= 8 { hashedPassword := passwordHasher.Hash( userData.GetPassword(), ) user.SetPassword(hashedPassword) user.SetEmail(userData.GetEmail()) user.SetName(userData.GetName()) user.Save() verificationToken := generateRandomToken() user.SetVerificationToken(verificationToken) user.Save() emailTemplate := "welcome_email" emailContent := emailTemplateBuilder.Build( emailTemplate, user, ) emailService.Send( emailContent, user.GetEmail(), ) logMessage := "User registered: " + user.GetEmail() logger.Info(logMessage) } } -
def register_user(self): if (self.user_data.get_email() is not None and self.user_data.get_password() is not None and len(self.user_data.get_password()) >= 8): hashed_password = self.password_hasher.hash( self.user_data.get_password() ) self.user.set_password(hashed_password) self.user.set_email(self.user_data.get_email()) self.user.set_name(self.user_data.get_name()) self.user.save() verification_token = secrets.token_hex(16) self.user.set_verification_token(verification_token) self.user.save() email_template = 'welcome_email' email_content = self.email_template_builder.build( email_template, self.user ) self.email_service.send( email_content, self.user.get_email() ) log_message = "User registered: " + self.user.get_email() self.logger.info(log_message) -
registerUser() { if ( this.userData.getEmail() !== null && this.userData.getPassword() !== null && this.userData.getPassword().length >= 8 ) { const hashedPassword = this.passwordHasher.hash( this.userData.getPassword() ); this.user.setPassword(hashedPassword); this.user.setEmail(this.userData.getEmail()); this.user.setName(this.userData.getName()); this.user.save(); const verificationToken = crypto.randomBytes(16).toString('hex'); this.user.setVerificationToken(verificationToken); this.user.save(); const emailTemplate = 'welcome_email'; const emailContent = this.emailTemplateBuilder.build( emailTemplate, this.user ); this.emailService.send( emailContent, this.user.getEmail() ); const logMessage = "User registered: " + this.user.getEmail(); this.logger.info(logMessage); } } -
public void registerUser(UserData userData) { if ( userData.getEmail() != null && userData.getPassword() != null && userData.getPassword().length() >= 8 ) { String hashedPassword = passwordHasher.hash( userData.getPassword() ); user.setPassword(hashedPassword); user.setEmail(userData.getEmail()); user.setName(userData.getName()); user.save(); String verificationToken = generateRandomToken(); user.setVerificationToken(verificationToken); user.save(); String emailTemplate = "welcome_email"; String emailContent = emailTemplateBuilder.build( emailTemplate, user ); emailService.send( emailContent, user.getEmail() ); String logMessage = "User registered: " + user.getEmail(); logger.info(logMessage); } } -
void RegisterUser(UserData userData) { if ( userData.GetEmail() != null && userData.GetPassword() != null && userData.GetPassword().Length >= 8 ) { string hashedPassword = passwordHasher.Hash( userData.GetPassword() ); user.SetPassword(hashedPassword); user.SetEmail(userData.GetEmail()); user.SetName(userData.GetName()); user.Save(); string verificationToken = GenerateRandomToken(); user.SetVerificationToken(verificationToken); user.Save(); string emailTemplate = "welcome_email"; string emailContent = emailTemplateBuilder.Build( emailTemplate, user ); emailService.Send( emailContent, user.GetEmail() ); string logMessage = "User registered: " + user.GetEmail(); logger.Info(logMessage); } } -
fn register_user(user_data: &UserData) { if user_data.get_email().is_some() && user_data.get_password().is_some() && user_data.get_password().as_ref().unwrap().len() >= 8 { let hashed_password = password_hasher.hash( user_data.get_password().as_ref().unwrap() ); user.set_password(&hashed_password); user.set_email(user_data.get_email().as_ref().unwrap()); user.set_name(user_data.get_name().as_ref().unwrap()); user.save(); let verification_token = generate_random_token(); user.set_verification_token(&verification_token); user.save(); let email_template = "welcome_email"; let email_content = email_template_builder.build( email_template, &user ); email_service.send( &email_content, user.get_email() ); let log_message = format!("User registered: {}", user.get_email()); logger.info(&log_message); } }
The code above clearly shows that the register user method is handling too many responsibilities.
The following points represent different levels of abstraction that need to be extracted from the register user method:
- Validating user input data.
- Hashing the password securely.
- Creating and saving the user entity.
- Generating and storing the verification token.
- Sending the welcome email.
- Logging the registration event.
Let’s apply the SLA principle to improve the method.
-
def register_user if !self.user_data_valid() return end self.create_user() self.generate_verification_token() self.send_welcome_email() self.log_registration() end def user_data_valid return @user_data.get_email != nil && @user_data.get_password != nil && @user_data.get_password.length >= 8 end def create_user hashed_password = @passwordHasher.hash(@user_data.get_password) @user.set_password(hashed_password) @user.set_email(@user_data.get_email) @user.set_name(@user_data.get_name) @user.save() end def generate_verification_token verification_token = SecureRandom.hex(32) @user.set_verification_token(verification_token) @user.save() end def send_welcome_email email_template = 'welcome_email' email_content = @emailTemplateBuilder.build( email_template, @user ) @emailService.send( email_content, @user.get_email() ) end def log_registration log_message = "User registered: " + @user.get_email() @logger.info(log_message) end -
public function registerUser(): void { if (!$this->userDataValid()) { return; } $this->createUser(); $this->generateVerificationToken(); $this->sendWelcomeEmail(); $this->logRegistration(); } private function userDataValid(): bool { return $this->userData->getEmail() !== null && $this->userData->getPassword() !== null && strlen($this->userData->getPassword()) >= 8; } private function createUser(): void { $hashedPassword = $this->passwordHasher->hash( $this->userData->getPassword() ); $this->user->setPassword($hashedPassword); $this->user->setEmail($this->userData->getEmail()); $this->user->setName($this->userData->getName()); $this->user->save(); } private function generateVerificationToken(): void { $verificationToken = bin2hex(random_bytes(16)); $this->user->setVerificationToken($verificationToken); $this->user->save(); } private function sendWelcomeEmail(): void { $emailTemplate = 'welcome_email'; $emailContent = $this->emailTemplateBuilder->build( $emailTemplate, $this->user ); $this->emailService->send( $emailContent, $this->user->getEmail() ); } private function logRegistration(): void { $logMessage = "User registered: " . $this->user->getEmail(); $this->logger->info($logMessage); } -
func RegisterUser(userData UserData) { if !UserDataValid(userData) { return } user := CreateUser(userData) GenerateVerificationToken(user) SendWelcomeEmail(user) LogRegistration(user) } func UserDataValid(userData UserData) bool { return userData.GetEmail() != "" && userData.GetPassword() != "" && len(userData.GetPassword()) >= 8 } func CreateUser(userData UserData) User { hashedPassword := passwordHasher.Hash( userData.GetPassword(), ) user.SetPassword(hashedPassword) user.SetEmail(userData.GetEmail()) user.SetName(userData.GetName()) user.Save() return user } func GenerateVerificationToken(user User) { verificationToken := generateRandomToken() user.SetVerificationToken(verificationToken) user.Save() } func SendWelcomeEmail(user User) { emailTemplate := "welcome_email" emailContent := emailTemplateBuilder.Build( emailTemplate, user, ) emailService.Send( emailContent, user.GetEmail(), ) } func LogRegistration(user User) { logMessage := "User registered: " + user.GetEmail() logger.Info(logMessage) } -
def register_user(self): if not self.user_data_valid(): return self.create_user() self.generate_verification_token() self.send_welcome_email() self.log_registration() def user_data_valid(self): return (self.user_data.get_email() is not None and self.user_data.get_password() is not None and len(self.user_data.get_password()) >= 8) def create_user(self): hashed_password = self.password_hasher.hash( self.user_data.get_password() ) self.user.set_password(hashed_password) self.user.set_email(self.user_data.get_email()) self.user.set_name(self.user_data.get_name()) self.user.save() def generate_verification_token(self): verification_token = secrets.token_hex(16) self.user.set_verification_token(verification_token) self.user.save() def send_welcome_email(self): email_template = 'welcome_email' email_content = self.email_template_builder.build( email_template, self.user ) self.email_service.send( email_content, self.user.get_email() ) def log_registration(self): log_message = "User registered: " + self.user.get_email() self.logger.info(log_message) -
registerUser() { if (!this.userDataValid()) { return; } this.createUser(); this.generateVerificationToken(); this.sendWelcomeEmail(); this.logRegistration(); } userDataValid() { return this.userData.getEmail() !== null && this.userData.getPassword() !== null && this.userData.getPassword().length >= 8; } createUser() { const hashedPassword = this.passwordHasher.hash( this.userData.getPassword() ); this.user.setPassword(hashedPassword); this.user.setEmail(this.userData.getEmail()); this.user.setName(this.userData.getName()); this.user.save(); } generateVerificationToken() { const verificationToken = crypto.randomBytes(16).toString('hex'); this.user.setVerificationToken(verificationToken); this.user.save(); } sendWelcomeEmail() { const emailTemplate = 'welcome_email'; const emailContent = this.emailTemplateBuilder.build( emailTemplate, this.user ); this.emailService.send( emailContent, this.user.getEmail() ); } logRegistration() { const logMessage = "User registered: " + this.user.getEmail(); this.logger.info(logMessage); } -
public void registerUser(UserData userData) { if (!userDataValid(userData)) { return; } User user = createUser(userData); generateVerificationToken(user); sendWelcomeEmail(user); logRegistration(user); } private boolean userDataValid(UserData userData) { return userData.getEmail() != null && userData.getPassword() != null && userData.getPassword().length() >= 8; } private User createUser(UserData userData) { String hashedPassword = passwordHasher.hash( userData.getPassword() ); User user = new User(); user.setPassword(hashedPassword); user.setEmail(userData.getEmail()); user.setName(userData.getName()); user.save(); return user; } private void generateVerificationToken(User user) { String verificationToken = generateRandomToken(); user.setVerificationToken(verificationToken); user.save(); } private void sendWelcomeEmail(User user) { String emailTemplate = "welcome_email"; String emailContent = emailTemplateBuilder.build( emailTemplate, user ); emailService.send( emailContent, user.getEmail() ); } private void logRegistration(User user) { String logMessage = "User registered: " + user.getEmail(); logger.info(logMessage); } -
void RegisterUser(UserData userData) { if (!UserDataValid(userData)) { return; } User user = CreateUser(userData); GenerateVerificationToken(user); SendWelcomeEmail(user); LogRegistration(user); } bool UserDataValid(UserData userData) { return userData.GetEmail() != null && userData.GetPassword() != null && userData.GetPassword().Length >= 8; } User CreateUser(UserData userData) { string hashedPassword = passwordHasher.Hash( userData.GetPassword() ); User user = new User(); user.SetPassword(hashedPassword); user.SetEmail(userData.GetEmail()); user.SetName(userData.GetName()); user.Save(); return user; } void GenerateVerificationToken(User user) { string verificationToken = GenerateRandomToken(); user.SetVerificationToken(verificationToken); user.Save(); } void SendWelcomeEmail(User user) { string emailTemplate = "welcome_email"; string emailContent = emailTemplateBuilder.Build( emailTemplate, user ); emailService.Send( emailContent, user.GetEmail() ); } void LogRegistration(User user) { string logMessage = "User registered: " + user.GetEmail(); logger.Info(logMessage); } -
fn register_user(user_data: &UserData) { if !user_data_valid(user_data) { return; } let mut user = create_user(user_data); generate_verification_token(&mut user); send_welcome_email(&user); log_registration(&user); } fn user_data_valid(user_data: &UserData) -> bool { user_data.get_email().is_some() && user_data.get_password().is_some() && user_data.get_password().as_ref().unwrap().len() >= 8 } fn create_user(user_data: &UserData) -> User { let hashed_password = password_hasher.hash( user_data.get_password().as_ref().unwrap() ); let mut user = User::new(); user.set_password(&hashed_password); user.set_email(user_data.get_email().as_ref().unwrap()); user.set_name(user_data.get_name().as_ref().unwrap()); user.save(); user } fn generate_verification_token(user: &mut User) { let verification_token = generate_random_token(); user.set_verification_token(&verification_token); user.save(); } fn send_welcome_email(user: &User) { let email_template = "welcome_email"; let email_content = email_template_builder.build( email_template, user ); email_service.send( &email_content, user.get_email() ); } fn log_registration(user: &User) { let log_message = format!("User registered: {}", user.get_email()); logger.info(&log_message); }
Much better! The register user method now clearly shows each step at the same level of abstraction.
Payment Processing
In this example, we will demonstrate the principle using a payment processing feature. This will illustrate how to properly separate concerns when processing a customer payment.
-
def process_payment if @payment.get_amount > 0 && @payment.get_currency != nil && @payment.get_card_number != nil _card_valid = @cardValidator.validate(@payment.get_card_number) if _card_valid _encrypted_card = @encryptionService.encrypt(@payment.get_card_number) _payment_gateway_response = @paymentGateway.charge( @payment.get_amount, @payment.get_currency, _encrypted_card ) if _payment_gateway_response.get_status == 'success' @payment.set_status('completed') @payment.set_transaction_id(_payment_gateway_response.get_transaction_id) @payment.save() _receipt_template = 'payment_receipt' _receipt_content = @emailTemplateBuilder.build( _receipt_template, @payment ) @emailService.send( _receipt_content, @payment.get_customer_email() ) _log_entry = "Payment processed: " + @payment.get_transaction_id @logger.info(_log_entry) else @payment.set_status('failed') @payment.save() end end end end -
public function processPayment(): void { if ( $this->payment->getAmount() > 0 && $this->payment->getCurrency() !== null && $this->payment->getCardNumber() !== null ) { $cardValid = $this->cardValidator->validate( $this->payment->getCardNumber() ); if ($cardValid) { $encryptedCard = $this->encryptionService->encrypt( $this->payment->getCardNumber() ); $paymentGatewayResponse = $this->paymentGateway->charge( $this->payment->getAmount(), $this->payment->getCurrency(), $encryptedCard ); if ($paymentGatewayResponse->getStatus() === 'success') { $this->payment->setStatus('completed'); $this->payment->setTransactionId( $paymentGatewayResponse->getTransactionId() ); $this->payment->save(); $receiptTemplate = 'payment_receipt'; $receiptContent = $this->emailTemplateBuilder->build( $receiptTemplate, $this->payment ); $this->emailService->send( $receiptContent, $this->payment->getCustomerEmail() ); $logEntry = "Payment processed: " . $this->payment->getTransactionId(); $this->logger->info($logEntry); } else { $this->payment->setStatus('failed'); $this->payment->save(); } } } } -
func ProcessPayment(payment Payment) { if payment.GetAmount() > 0 && payment.GetCurrency() != "" && payment.GetCardNumber() != "" { cardValid := cardValidator.Validate( payment.GetCardNumber(), ) if cardValid { encryptedCard := encryptionService.Encrypt( payment.GetCardNumber(), ) paymentGatewayResponse := paymentGateway.Charge( payment.GetAmount(), payment.GetCurrency(), encryptedCard, ) if paymentGatewayResponse.GetStatus() == "success" { payment.SetStatus("completed") payment.SetTransactionId( paymentGatewayResponse.GetTransactionId(), ) payment.Save() receiptTemplate := "payment_receipt" receiptContent := emailTemplateBuilder.Build( receiptTemplate, payment, ) emailService.Send( receiptContent, payment.GetCustomerEmail(), ) logEntry := "Payment processed: " + payment.GetTransactionId() logger.Info(logEntry) } else { payment.SetStatus("failed") payment.Save() } } } } -
def process_payment(self): if (self.payment.get_amount() > 0 and self.payment.get_currency() is not None and self.payment.get_card_number() is not None): card_valid = self.card_validator.validate( self.payment.get_card_number() ) if card_valid: encrypted_card = self.encryption_service.encrypt( self.payment.get_card_number() ) payment_gateway_response = self.payment_gateway.charge( self.payment.get_amount(), self.payment.get_currency(), encrypted_card ) if payment_gateway_response.get_status() == 'success': self.payment.set_status('completed') self.payment.set_transaction_id( payment_gateway_response.get_transaction_id() ) self.payment.save() receipt_template = 'payment_receipt' receipt_content = self.email_template_builder.build( receipt_template, self.payment ) self.email_service.send( receipt_content, self.payment.get_customer_email() ) log_entry = "Payment processed: " + self.payment.get_transaction_id() self.logger.info(log_entry) else: self.payment.set_status('failed') self.payment.save() -
processPayment() { if ( this.payment.getAmount() > 0 && this.payment.getCurrency() !== null && this.payment.getCardNumber() !== null ) { const cardValid = this.cardValidator.validate( this.payment.getCardNumber() ); if (cardValid) { const encryptedCard = this.encryptionService.encrypt( this.payment.getCardNumber() ); const paymentGatewayResponse = this.paymentGateway.charge( this.payment.getAmount(), this.payment.getCurrency(), encryptedCard ); if (paymentGatewayResponse.getStatus() === 'success') { this.payment.setStatus('completed'); this.payment.setTransactionId( paymentGatewayResponse.getTransactionId() ); this.payment.save(); const receiptTemplate = 'payment_receipt'; const receiptContent = this.emailTemplateBuilder.build( receiptTemplate, this.payment ); this.emailService.send( receiptContent, this.payment.getCustomerEmail() ); const logEntry = "Payment processed: " + this.payment.getTransactionId(); this.logger.info(logEntry); } else { this.payment.setStatus('failed'); this.payment.save(); } } } } -
public void processPayment(Payment payment) { if ( payment.getAmount() > 0 && payment.getCurrency() != null && payment.getCardNumber() != null ) { boolean cardValid = cardValidator.validate( payment.getCardNumber() ); if (cardValid) { String encryptedCard = encryptionService.encrypt( payment.getCardNumber() ); PaymentGatewayResponse response = paymentGateway.charge( payment.getAmount(), payment.getCurrency(), encryptedCard ); if (response.getStatus().equals("success")) { payment.setStatus("completed"); payment.setTransactionId( response.getTransactionId() ); payment.save(); String receiptTemplate = "payment_receipt"; String receiptContent = emailTemplateBuilder.build( receiptTemplate, payment ); emailService.send( receiptContent, payment.getCustomerEmail() ); String logEntry = "Payment processed: " + payment.getTransactionId(); logger.info(logEntry); } else { payment.setStatus("failed"); payment.save(); } } } } -
void ProcessPayment(Payment payment) { if ( payment.GetAmount() > 0 && payment.GetCurrency() != null && payment.GetCardNumber() != null ) { bool cardValid = cardValidator.Validate( payment.GetCardNumber() ); if (cardValid) { string encryptedCard = encryptionService.Encrypt( payment.GetCardNumber() ); PaymentGatewayResponse response = paymentGateway.Charge( payment.GetAmount(), payment.GetCurrency(), encryptedCard ); if (response.GetStatus() == "success") { payment.SetStatus("completed"); payment.SetTransactionId( response.GetTransactionId() ); payment.Save(); string receiptTemplate = "payment_receipt"; string receiptContent = emailTemplateBuilder.Build( receiptTemplate, payment ); emailService.Send( receiptContent, payment.GetCustomerEmail() ); string logEntry = "Payment processed: " + payment.GetTransactionId(); logger.Info(logEntry); } else { payment.SetStatus("failed"); payment.Save(); } } } } -
fn process_payment(payment: &mut Payment) { if payment.get_amount() > 0.0 && payment.get_currency().is_some() && payment.get_card_number().is_some() { let card_valid = card_validator.validate( payment.get_card_number().as_ref().unwrap() ); if card_valid { let encrypted_card = encryption_service.encrypt( payment.get_card_number().as_ref().unwrap() ); let payment_gateway_response = payment_gateway.charge( payment.get_amount(), payment.get_currency().as_ref().unwrap(), &encrypted_card ); if payment_gateway_response.get_status() == "success" { payment.set_status("completed"); payment.set_transaction_id( payment_gateway_response.get_transaction_id() ); payment.save(); let receipt_template = "payment_receipt"; let receipt_content = email_template_builder.build( receipt_template, payment ); email_service.send( &receipt_content, payment.get_customer_email() ); let log_entry = format!( "Payment processed: {}", payment.get_transaction_id() ); logger.info(&log_entry); } else { payment.set_status("failed"); payment.save(); } } } }
The code above demonstrates that the process payment method is handling too many responsibilities at different abstraction levels.
The following points represent different levels of abstraction that need to be extracted from the process payment method:
- Validating payment data and card information.
- Encrypting sensitive card data.
- Processing the payment through the gateway.
- Updating payment status based on the result.
- Sending payment receipt email.
- Logging the payment transaction.
Let’s apply the SLA principle to improve the method.
-
def process_payment if !self.payment_valid() return end if !self.card_valid() return end if self.charge_payment() self.complete_payment() self.send_receipt() self.log_payment() else self.fail_payment() end end def payment_valid return @payment.get_amount > 0 && @payment.get_currency != nil && @payment.get_card_number != nil end def card_valid return @cardValidator.validate(@payment.get_card_number) end def charge_payment encrypted_card = @encryptionService.encrypt(@payment.get_card_number) response = @paymentGateway.charge( @payment.get_amount, @payment.get_currency, encrypted_card ) return response.get_status == 'success' end def complete_payment @payment.set_status('completed') @payment.set_transaction_id(@payment_gateway_response.get_transaction_id) @payment.save() end def send_receipt receipt_template = 'payment_receipt' receipt_content = @emailTemplateBuilder.build( receipt_template, @payment ) @emailService.send( receipt_content, @payment.get_customer_email() ) end def log_payment log_entry = "Payment processed: " + @payment.get_transaction_id @logger.info(log_entry) end def fail_payment @payment.set_status('failed') @payment.save() end -
public function processPayment(): void { if (!$this->paymentValid()) { return; } if (!$this->cardValid()) { return; } if ($this->chargePayment()) { $this->completePayment(); $this->sendReceipt(); $this->logPayment(); } else { $this->failPayment(); } } private function paymentValid(): bool { return $this->payment->getAmount() > 0 && $this->payment->getCurrency() !== null && $this->payment->getCardNumber() !== null; } private function cardValid(): bool { return $this->cardValidator->validate( $this->payment->getCardNumber() ); } private function chargePayment(): bool { $encryptedCard = $this->encryptionService->encrypt( $this->payment->getCardNumber() ); $response = $this->paymentGateway->charge( $this->payment->getAmount(), $this->payment->getCurrency(), $encryptedCard ); $this->paymentGatewayResponse = $response; return $response->getStatus() === 'success'; } private function completePayment(): void { $this->payment->setStatus('completed'); $this->payment->setTransactionId( $this->paymentGatewayResponse->getTransactionId() ); $this->payment->save(); } private function sendReceipt(): void { $receiptTemplate = 'payment_receipt'; $receiptContent = $this->emailTemplateBuilder->build( $receiptTemplate, $this->payment ); $this->emailService->send( $receiptContent, $this->payment->getCustomerEmail() ); } private function logPayment(): void { $logEntry = "Payment processed: " . $this->payment->getTransactionId(); $this->logger->info($logEntry); } private function failPayment(): void { $this->payment->setStatus('failed'); $this->payment->save(); } -
func ProcessPayment(payment Payment) { if !PaymentValid(payment) { return } if !CardValid(payment) { return } if ChargePayment(payment) { CompletePayment(payment) SendReceipt(payment) LogPayment(payment) } else { FailPayment(payment) } } func PaymentValid(payment Payment) bool { return payment.GetAmount() > 0 && payment.GetCurrency() != "" && payment.GetCardNumber() != "" } func CardValid(payment Payment) bool { return cardValidator.Validate( payment.GetCardNumber(), ) } func ChargePayment(payment Payment) bool { encryptedCard := encryptionService.Encrypt( payment.GetCardNumber(), ) response := paymentGateway.Charge( payment.GetAmount(), payment.GetCurrency(), encryptedCard, ) payment.SetGatewayResponse(response) return response.GetStatus() == "success" } func CompletePayment(payment Payment) { payment.SetStatus("completed") payment.SetTransactionId( payment.GetGatewayResponse().GetTransactionId(), ) payment.Save() } func SendReceipt(payment Payment) { receiptTemplate := "payment_receipt" receiptContent := emailTemplateBuilder.Build( receiptTemplate, payment, ) emailService.Send( receiptContent, payment.GetCustomerEmail(), ) } func LogPayment(payment Payment) { logEntry := "Payment processed: " + payment.GetTransactionId() logger.Info(logEntry) } func FailPayment(payment Payment) { payment.SetStatus("failed") payment.Save() } -
def process_payment(self): if not self.payment_valid(): return if not self.card_valid(): return if self.charge_payment(): self.complete_payment() self.send_receipt() self.log_payment() else: self.fail_payment() def payment_valid(self): return (self.payment.get_amount() > 0 and self.payment.get_currency() is not None and self.payment.get_card_number() is not None) def card_valid(self): return self.card_validator.validate( self.payment.get_card_number() ) def charge_payment(self): encrypted_card = self.encryption_service.encrypt( self.payment.get_card_number() ) response = self.payment_gateway.charge( self.payment.get_amount(), self.payment.get_currency(), encrypted_card ) self.payment_gateway_response = response return response.get_status() == 'success' def complete_payment(self): self.payment.set_status('completed') self.payment.set_transaction_id( self.payment_gateway_response.get_transaction_id() ) self.payment.save() def send_receipt(self): receipt_template = 'payment_receipt' receipt_content = self.email_template_builder.build( receipt_template, self.payment ) self.email_service.send( receipt_content, self.payment.get_customer_email() ) def log_payment(self): log_entry = "Payment processed: " + self.payment.get_transaction_id() self.logger.info(log_entry) def fail_payment(self): self.payment.set_status('failed') self.payment.save() -
processPayment() { if (!this.paymentValid()) { return; } if (!this.cardValid()) { return; } if (this.chargePayment()) { this.completePayment(); this.sendReceipt(); this.logPayment(); } else { this.failPayment(); } } paymentValid() { return this.payment.getAmount() > 0 && this.payment.getCurrency() !== null && this.payment.getCardNumber() !== null; } cardValid() { return this.cardValidator.validate( this.payment.getCardNumber() ); } chargePayment() { const encryptedCard = this.encryptionService.encrypt( this.payment.getCardNumber() ); const response = this.paymentGateway.charge( this.payment.getAmount(), this.payment.getCurrency(), encryptedCard ); this.paymentGatewayResponse = response; return response.getStatus() === 'success'; } completePayment() { this.payment.setStatus('completed'); this.payment.setTransactionId( this.paymentGatewayResponse.getTransactionId() ); this.payment.save(); } sendReceipt() { const receiptTemplate = 'payment_receipt'; const receiptContent = this.emailTemplateBuilder.build( receiptTemplate, this.payment ); this.emailService.send( receiptContent, this.payment.getCustomerEmail() ); } logPayment() { const logEntry = "Payment processed: " + this.payment.getTransactionId(); this.logger.info(logEntry); } failPayment() { this.payment.setStatus('failed'); this.payment.save(); } -
public void processPayment(Payment payment) { if (!paymentValid(payment)) { return; } if (!cardValid(payment)) { return; } if (chargePayment(payment)) { completePayment(payment); sendReceipt(payment); logPayment(payment); } else { failPayment(payment); } } private boolean paymentValid(Payment payment) { return payment.getAmount() > 0 && payment.getCurrency() != null && payment.getCardNumber() != null; } private boolean cardValid(Payment payment) { return cardValidator.validate( payment.getCardNumber() ); } private boolean chargePayment(Payment payment) { String encryptedCard = encryptionService.encrypt( payment.getCardNumber() ); PaymentGatewayResponse response = paymentGateway.charge( payment.getAmount(), payment.getCurrency(), encryptedCard ); payment.setGatewayResponse(response); return response.getStatus().equals("success"); } private void completePayment(Payment payment) { payment.setStatus("completed"); payment.setTransactionId( payment.getGatewayResponse().getTransactionId() ); payment.save(); } private void sendReceipt(Payment payment) { String receiptTemplate = "payment_receipt"; String receiptContent = emailTemplateBuilder.build( receiptTemplate, payment ); emailService.send( receiptContent, payment.getCustomerEmail() ); } private void logPayment(Payment payment) { String logEntry = "Payment processed: " + payment.getTransactionId(); logger.info(logEntry); } private void failPayment(Payment payment) { payment.setStatus("failed"); payment.save(); } -
void ProcessPayment(Payment payment) { if (!PaymentValid(payment)) { return; } if (!CardValid(payment)) { return; } if (ChargePayment(payment)) { CompletePayment(payment); SendReceipt(payment); LogPayment(payment); } else { FailPayment(payment); } } bool PaymentValid(Payment payment) { return payment.GetAmount() > 0 && payment.GetCurrency() != null && payment.GetCardNumber() != null; } bool CardValid(Payment payment) { return cardValidator.Validate( payment.GetCardNumber() ); } bool ChargePayment(Payment payment) { string encryptedCard = encryptionService.Encrypt( payment.GetCardNumber() ); PaymentGatewayResponse response = paymentGateway.Charge( payment.GetAmount(), payment.GetCurrency(), encryptedCard ); payment.SetGatewayResponse(response); return response.GetStatus() == "success"; } void CompletePayment(Payment payment) { payment.SetStatus("completed"); payment.SetTransactionId( payment.GetGatewayResponse().GetTransactionId() ); payment.Save(); } void SendReceipt(Payment payment) { string receiptTemplate = "payment_receipt"; string receiptContent = emailTemplateBuilder.Build( receiptTemplate, payment ); emailService.Send( receiptContent, payment.GetCustomerEmail() ); } void LogPayment(Payment payment) { string logEntry = "Payment processed: " + payment.GetTransactionId(); logger.Info(logEntry); } void FailPayment(Payment payment) { payment.SetStatus("failed"); payment.Save(); } -
fn process_payment(payment: &mut Payment) { if !payment_valid(payment) { return; } if !card_valid(payment) { return; } if charge_payment(payment) { complete_payment(payment); send_receipt(payment); log_payment(payment); } else { fail_payment(payment); } } fn payment_valid(payment: &Payment) -> bool { payment.get_amount() > 0.0 && payment.get_currency().is_some() && payment.get_card_number().is_some() } fn card_valid(payment: &Payment) -> bool { card_validator.validate( payment.get_card_number().as_ref().unwrap() ) } fn charge_payment(payment: &mut Payment) -> bool { let encrypted_card = encryption_service.encrypt( payment.get_card_number().as_ref().unwrap() ); let response = payment_gateway.charge( payment.get_amount(), payment.get_currency().as_ref().unwrap(), &encrypted_card ); payment.set_gateway_response(response.clone()); response.get_status() == "success" } fn complete_payment(payment: &mut Payment) { payment.set_status("completed"); payment.set_transaction_id( payment.get_gateway_response().get_transaction_id() ); payment.save(); } fn send_receipt(payment: &Payment) { let receipt_template = "payment_receipt"; let receipt_content = email_template_builder.build( receipt_template, payment ); email_service.send( &receipt_content, payment.get_customer_email() ); } fn log_payment(payment: &Payment) { let log_entry = format!( "Payment processed: {}", payment.get_transaction_id() ); logger.info(&log_entry); } fn fail_payment(payment: &mut Payment) { payment.set_status("failed"); payment.save(); }
Excellent! The process payment method now clearly shows each step at the same level of abstraction, making it much easier to understand and maintain.
