An overview of the

Software design has changed dramatically in the last 20 years, but SOLID principles remain the best practice for software design today.

SOLID principles are a well-tested title for creating high-quality software. But in the age of modern multi-paradigm programming (functional programming, etc.) and the rise of cloud computing, will it hold up? I’ll explain what SOLID stands for, why it still works with modern software, and share some examples to explain.

What is a SOLID

SOLID is a set of principles extracted by Robert C. Martin in 2000. It has been suggested as a special way of thinking about object-oriented (OO) programming quality. In general, SOLID makes suggestions on how code should be shred, how code should be private and exposed, and how code should be called between codes. I’ll delve into the original meaning of each letter (S.O.L.I.D) and extend it beyond object-oriented programming.

What has changed?

In the early 2000s, Java and C++ reigned supreme, and of course many of my college courses used the Java language as training. The popularity of Java has spawned books, courses, and other materials that make the transition from writing code to writing good code.

As a result, the software industry has had a profound impact. There are a few points worth noting:

  • ** Dynamically typed languages such as Python, Ruby, and especially JavaScript have become as popular as Java — and in some industries and companies have surpassed It
  • Non-object-oriented paradigms, most notable for functional programming (FP), are also common in these new languages. Even Java itself has introduced lambdas! Techniques such as metaprogramming (adding and changing methods and features of objects) are also becoming more popular. There is also the “soft object-oriented” Go language, which has static typing but no inheritance. All of this suggests that classes and integrations are less important in modern software than they used to be.
  • The proliferation of Open source software. In the early days, more general-purpose software was closed-source and people used compiled software, now it’s common for people to rely on open source software. As a result, the logic and data hiding that was once essential to writing a library is no longer important.
  • Microservices and software-as-a-service (Saas) have exploded. Rather than deploying an application as a large executable that links all dependencies together, deploy a small service that calls other services (supported by its own living third party).

Overall, many of the things SOLID really cares about — like classes and interfaces, data hiding and polymorphism — are no longer something programmers deal with every day.

What hasn’t changed?

A lot is different in industry now, but some things are still the same. Include:

  • Code is written and modified by humans. Code is written once and read many, many times. So you need good code documentation internally and externally, especially good API documentation.
  • Code is organized into modules. In some languages, these are classes. In other cases, they may be separate source files. In JavaScript, they might be exported objects. However, there is always some way to isolate and organize code into separate, bounded units. Therefore, you always need to decide how best to organize your code together.
  • The code can be internal or external. Some of the code you write will be used by you or your team, and some will be used by other teams or even other customers through the API. This also means that you need some way to decide which code is “visible” and which is hidden.

“Modern” SOLID

In the following articles, I will present each of the principles in SOLID as a more general description, and show how they apply to OO, FP, and multi-paradigm programming, without further explanation. In many cases, these principles can even be applied to an entire service or system.

Note that I’m going to use “module” to refer to a set of code, which could be classes, packages, files, etc.

Single responsibility principle

Original definition: “A class can change for no more than one reason”

If you write a class that has many concerns or “reasons to change,” and any one of those concerns needs to change, you need to change the same code. This increases the possibility that changes to one feature feature will break another feature feature.

As an example, here is a Franken-class that should never be used in production:

class FrankenClass {
  public void savaUserDetail(User user) {
    / /...
  }
  
  public void performOrder(Order order) {
    / /...
  }
  
  public void shipItem(Item item, String address) {
    / /...}}Copy the code

New definition: “Each module should do one thing, and do it well”.

This principle was closely linked to the topic of High Cohesion. By nature, your code should not mix up many roles or uses.

Here is a functional programming (FP) version of the same example using JavaScript:

const saveUserDetails = (user) = >{... }const performOrder = (order) = > { ...}
const shipItem = (item, address) = >{... }export { saveUserDetails, performOrder, shipItem };
import { saveUserDetails, performOrder, shipItem } from "allActions";                   
Copy the code

This also applies to microservice design; If you have a single service that handles all three functions, it will try to do too many things.

The Open-closed principle

Original definition: “Software entities should be open for extension and closed for modification.”

This is part of the Design of the Java language – you can create a subclass to inherit a class, but you can’t modify the original class.

One of the reasons for being “open to extension” is that it limits the dependency on class authors — if you need to change a class, you have to wait for the original author to make the change, or you have to dig into the class to make the change. More importantly, this class takes on too many concerns that break the single responsibility principle.

The reason for “closing to changes” is that we don’t trust downstream users to fully understand all of our “private” private code, and we want to protect it from changes by unskilled people.

class Notifier {
  public void notify(String message) {
    //send an e-mail}}class LoggingNotifier extends Notifier {
  public void notify(String message) {
    super.notify(message);//keep parent behaiver
    //also log the message;}}Copy the code

New definition: “should be able to use and add modules without rewriting modules”.

This is free in the object-oriented world (AOP). The Hook point must be explicitly defined in functional programming code to allow modification. Here is an example where not only do you allow the use of before-and-after hooks, but you can even override the basic behavior of your function by passing it to it:

//library code

const saveRecord = (record, save, beforeSave, afterSave) = > {
  const defaultSave = (record) = > {
    //default save function;
  }
  
  if (beforeSave) beforeSave(record);
  if (save) {
    save(record);
  } else {
    defaultSave(record);
  }
  if(afterSave) afterSave(record);
  //calling code
}

//calling code
const customSave = (record) = >{... } saveRecord(myRecord, customSave);Copy the code

Richter’s substitution principle

Original definition: “If type S is a subtype of T, then type T can be replaced with type S without modifying any required properties of the program.”

This is also a fundamental property of object-oriented languages. It means that you can replace their parent class with any subclass. So you can be confident in this convention: you can safely use any object of “is a” type T as if it were T. Here’s an example:

class Vehicle {
  public int getNumberOfWheels(a) {
    return 4; }}class Bicycle extends Vehicle {
  public int getNumberOfWheels(a) {
    return 2; }}// calling code
public static int COST_PER_TIRE = 50;

public int tireCost(Vehicle vehicle) {
  return COST_PER_TIRE * vehicle.getNumberOfWheels();
}

Bicycle bicycle = new Bicycle();
System.out.println(tireCost(bicycle)); / / 100
Copy the code

New definition: You can substitute one thing for another as long as they all behave the same way.

In dynamic languages, the important thing is that if your program promises to do something (such as implementing an interface or function), you need to stand by promises and not give a client something that does not promise.

Many dynamic languages use “duck typing” to achieve this. Essentially, your function formally or informally states that it expects its input to behave in a particular way and to behave based on that assumption.

For example, Ruby:

# @param input [#to_s]
def split_lines(input)
  input.to_s.split("\n")
end
Copy the code

In this case, the function doesn’t care about the input type — just that it has a to_s function that behaves the same way all to_s functions behave: by turning the input into a string. Many dynamic languages have no way to enforce this behavior, so it is more a matter of discipline than a formal technique.

Next comes the TypeScript example of functional programming, where higher-order functions introduce a filter that inputs a number and returns a Boolean value:

const isEven = (x: number) : boolean= > x % 2= =0;
const isOdd = (x: number) : boolean= > x % 2= =1;

const printFiltered = (arr: number[], filterFunc: (int) => boolean) = > {
  arr.forEach((item) = > {
    if (filterFunc(item)) {
      console.log(item); }})}const array = [1.2.3.4.5.6];
printFiltered(array, isEven);
printFiltered(array, isOdd);
Copy the code

Interface Segregation Principle

Original definition: “Many client-specific interfaces are better than one general-purpose interface.”

In an object-oriented language, you can think of it as providing a “view” for your class. Instead of providing you with a big, all-inclusive implementation for your clients, create interfaces on top of them using only methods relevant to that client and require your clients to use those interfaces.

Like the single responsibility principle, the interface isolation principle isolates coupling between systems and ensures that clients do not need to know about the extraneous functions on which they depend.

The following example passes the SPR test:

class PrintRequest {
  public void createRequest(a) {}
  public void deleteRequest(a) {}
  public void workOnRequest(a) {}}Copy the code

This code usually has only one “reason to change” — it’s all related to print requests, they’re all part of the same domain, and all three methods might change the same state. However, the client that creates the request is unlikely to be the client that processes the request. It makes more sense to isolate these interfaces for meetings:

interface PrintRequestModifier {
  public void createRequest(a);
  public void deleteRequest(a);
}

interface PrintRequestWorker {
  public void workOnRequest(a)
}

class PrintRequest implements PrintRequestModifier, PrintRequestWorker {
  public void createRequest(a) {}
  public void deleteRequest(a) {}
  public void workOnRequest(a) {}}Copy the code

New definition: “Don’t show the client something it doesn’t need to see.”

Only record what your client needs to know. This means that using the document generator only inputs “public” function or route, and “private” does not necessarily output.

In the era of microservices, you can add clarity using documents or real isolation. For example, your external customer may only be logged in as a user, but your internal service may get a list of users or other attributes. You can also create a separate “external only” user service to invoke your main service, or you can output specific documents only for external users that hide internal routes.

Dependency Inversion Principle

Original definition: “Rely on abstractions rather than concrete implementations”

In object-oriented languages, this means that clients should rely on interfaces rather than concrete implementation classes whenever possible. This ensures that the code should rely on the smallest possible area — in fact, the client doesn’t have to rely on all the code, just a contract that defines how the code should behave. As with any principle, this reduces the risk of changing one feature and breaking another. Here’s an example:

interface Logger {
  public void write(String message);
}

class FileLogger implements Logger {
  public void write(String message) {
    //write to file}}class StandardOutLogger implements Logger {
  public void write(String message) {
    //write to standard out}}// call
public void doStuff(Logger logger) {
  //do stuff
  logger.write("some message");
}
Copy the code

If you’re writing code that needs a Logger, you don’t want to limit yourself to just writing to a file because you don’t care. You simply call the write method and let the implementation print.

New definition: “Rely on abstractions rather than concrete implementations.”

Yes, this example I will keep the definition as it is! It is still important to keep things dependent on abstraction, even if the abstraction mechanisms in modern code are not as powerful as in the strict object-oriented world.

In particular, this is the same as the Richter substitution principle discussed above. The main difference is that there is no default implementation. Therefore, the discussion of duck types and hook functions in this section applies to dependency inversion in general.

You can also use abstractions for microservices. For example, you can replace direct communication between services with a message bus live queue platform such as Kafka or other messaging middleware. This allows services to send messages to a single generic location, regardless of which particular service will receive the messages and perform its tasks.

conclusion

Rehash the “modern SOLID” one more time:

  • Don’t be surprised if someone reads your code.
  • Don’t be surprised if others use your code.
  • Don’t confuse the people reading your code.
  • Use reasonable boundaries for your code.
  • Use the right level of coupling – dust to dust, soil to soil.

Good code is good code — it never changes, nor does SOLID, and practice is a SOLID foundation.

The original connection: stackoverflow. Blog / 2021/11/01 /…