Unveiling the Shadows: Mastering the Dark Arts of Object-Oriented Programming

Unveiling the Shadows: Mastering the Dark Arts of Object-Oriented Programming

This title links back to the theme of not getting too attached to one's code (as discussed in "Don’t fall in love with your code "). It continues the exploration of code issues, specifically focusing on object-oriented programming abuses in Dart. It also hints at a deeper dive, similar to the investigative approach in "Taming the Beast " but focused on a different set of programming challenges.

Object-Oriented Abusers are code smells that occur when object-oriented principles and best practices are not properly followed. These code smells often indicate violations of encapsulation, inheritance, or composition principles, leading to code that is less maintainable, less flexible, and more prone to errors.

In object-oriented programming (OOP), developers can sometimes misuse features like inheritance and interfaces, particularly when using languages. This misuse can lead to code that is difficult to maintain or extend. This article will explore common examples of such abuses in Dart and provide practical solutions for refactoring them.

1- The Problem: Misusing Inheritance

Inheritance is a powerful feature intended to model an "is-a" relationship. However, it's often misused to achieve code reuse, leading to inappropriate coupling and fragile designs.

class Vehicle {
  void startEngine() {
    print("Engine started");
  }
}

class ElectricCar extends Vehicle {
  @override
  void startEngine() {
    throw Exception("Electric car doesn't have an engine!");
  }
}        

In this example, ElectricCar inherits from Vehicle but overrides startEngine to throw an exception, indicating a misuse of inheritance since ElectricCar is not a proper subtype of Vehicle.

The Solution: Favor Composition Over Inheritance

Using composition instead of inheritance can solve this problem by allowing more flexible code reuse without tightly coupling classes.

class Engine {
  void start() {
    print("Engine started");
  }
}

class Vehicle {
  Engine? engine;

  void startEngine() {
    engine?.start();
  }
}

class ElectricCar extends Vehicle {
  ElectricCar() {
    engine = null; // Electric cars do not have an engine
  }
}        

In the refactored example, the Vehicle has an Engine that can be null, allowing ElectricCar to indicate the absence of an engine without misusing inheritance.


2- The Problem: Temporary Fields

Temporary fields get created when an object’s fields are only used under certain conditions. This can lead to objects with a lot of optional or nullable fields, complicating the logic for when they are actually needed.

class Order {
  DateTime? deliveryDate;
  void calculateDeliveryDate() {
    if (isExpress) {
      deliveryDate = DateTime.now().add(Duration(days: 1));
    }
  }
}        

Here, deliveryDate is only used when isExpress is true, making it a temporary field that is not always relevant.

The Solution: Introduce a Strategy or State Pattern

Refactor by using a strategy pattern to handle variations in behavior, or a state pattern to manage state-dependent behavior.

abstract class DeliveryStrategy {
  DateTime calculateDeliveryDate();
}

class ExpressDelivery implements DeliveryStrategy {
  @override
  DateTime calculateDeliveryDate() {
    return DateTime.now().add(Duration(days: 1));
  }
}

class StandardDelivery implements DeliveryStrategy {
  @override
  DateTime calculateDeliveryDate() {
    return DateTime.now().add(Duration(days: 3));
  }
}

class Order {
  DeliveryStrategy deliveryStrategy;
  Order(this.deliveryStrategy);
  DateTime getDeliveryDate() {
    return deliveryStrategy.calculateDeliveryDate();
  }
}        

This approach removes the temporary field and encapsulates the delivery date calculation within appropriate strategy classes.


3- The Problem: Refused Bequest

This occurs when a subclass does not use all of the properties and methods inherited from its parent class, indicating that inheritance is not the best relationship.

class Bird {
  void fly() {
    print("Flying");
  }
  void eat() {
    print("Eating");
  }
}

class Ostrich extends Bird {
  @override
  void fly() {
    throw Exception("Ostriches can't fly!");
  }
}        

Here, Ostrich inherits fly from Bird but cannot fly, indicating a misuse of inheritance.

The Solution: Replace Inheritance with Delegation

Refactor by replacing inheritance with delegation, defining more appropriate relationships.

class Bird {
  void eat() {
    print("Eating");
  }
}

class FlyingBird extends Bird {
  void fly() {
    print("Flying");
  }
}

class Ostrich extends Bird {
  // No need to override fly
}        

This refactoring ensures that Ostrich does not inherit unnecessary methods from Bird.


4- The Problem: Alternative Classes with Different Interfaces

This issue arises when similar classes have different interfaces for performing the same tasks, leading to client confusion and duplication of effort.

class EmailSender {
  void sendEmail(String message) {
    print("Sending email: $message");
  }
}

class MessageSender {
  void sendMessage(String text) {
    print("Sending message: $text");
  }
}        

Both classes perform similar functions but with different method names and interfaces.

The Solution: Unify Interfaces

Standardize the interface across similar classes to ensure consistency.

abstract class MessageSender {
  void send(String message);
}

class EmailSender implements MessageSender {
  @override
  void send(String message) {
    print("Sending email: $message");
  }
}

class TextMessageSender implements MessageSender {
  @override
  void send(String message) {
    print("Sending text message: $message");
  }
}        

This refactoring provides a unified interface, making it easier for clients to use different messaging strategies interchangeably.


5- The Problem: Excessive Use of Interfaces

Using too many interfaces, especially when they are not cohesive, can lead to complex and hard-to-understand code.

abstract class Flyer {
  void fly();
}

abstract class Swimmer {
  void swim();
}

class Duck implements Flyer, Swimmer {
  @override
  void fly() {
    print("Duck flying");
  }

  @override
  void swim() {
    print("Duck swimming");
  }
}        

While Duck can fly and swim, implementing multiple interfaces for each capability can lead to bloated designs.

The Solution: Interface Segregation and Role Interfaces

Applying the Interface Segregation Principle (ISP) can help by ensuring that interfaces are specific and not overly broad.

abstract class Animal {}

abstract class Flyer extends Animal {
  void fly();
}

abstract class Swimmer extends Animal {
  void swim();
}

class Duck implements Flyer, Swimmer {
  @override
  void fly() {
    print("Duck flying");
  }

  @override
  void swim() {
    print("Duck swimming");
  }
}        

In this refactored example, Flyer and Swimmer are both subtypes of Animal, clarifying their roles without forcing Duck to implement unnecessary methods.

Identifying and addressing these Object-Oriented Abusers through refactoring techniques can help bring code back in line with object-oriented principles, improving code quality, maintainability, and extensibility.

要查看或添加评论,请登录

社区洞察

其他会员也浏览了