Home > Software design >  when to use design patterns and when to apply which solid principle?
when to use design patterns and when to apply which solid principle?

Time:09-02

I am confused! I'm trying to figure out when to apply Solid Principles and when to use which Design Patterns(Factory Method ,Builder Pattern etc.). I searched a lot but I can't find answer of my question. At the end I posted here. Please explain.

CodePudding user response:

Adapter pattern Imagine you travel to Europe from Asia and you need to connect your laptop using your Asian plug, but you can't because European sockets are not designed to work with your plug. In such a case u use an adapter between your plug and their sockets.

You use the adapter designed pattern when you want to connect your code with a different interface.

An additional benefit is that using an adapter pattern makes your code dependent on your adapter and not on a foreign library, which might increase the maintainability or testability, etc., of your code.

I often use adapters when I don't want my domain code (business logic) dependent on used libraries (f.ex database ORM). Adapter provides me the flexibility of changing libraries without breaking my domain code.


Facade Very similar usage to the adapter. When you want to simplify your code by hiding complex code (several libraries used) behind a simple and easy-to-use interface, then you use a facade. The benefit of this approach is that the code behind the facade could be easily modified without breaking the code using this facade. Using it might increase the readability and testability of the code or make code refactoring simpler.

CodePudding user response:

I use Solid everywhere except special unique cases where using Solid results in a smelly code. Using Solid usually makes the code easier to maintain, read and test.

Static Factory method - A handy pattern for having several constructors while explaining each of them with a meaningful name.

class AClass {

    private final String parameter;
    
    private AClass(String parameter) {
        this.parameter = parameter;
    }
    
    public static AClass fromLong(Long longParameter) {
        return new AClass(longParameter.toString());
    }

    public static AClass fromDecimal(Long longParameter) {
        return new AClass(longParameter.toString());
    }
    
    public static AClass empty() {
        return new AClass(null);
    }
}

Factory method When we need to design a code that constructs an object implementing an interface, but we know the concrete implementation of this object only during runtime, then the factory method comes in handy. Users can easily extend functionality by adding new concrete objects and creating factory methods.

See the example below.

The DocumentService can work with any class implementing Document, and any class extending AbstractDocumentCreator. Current version of example supports Txt and Html documents. If we need a PFG support we simply create a PdfDocument class implementing Document and PdfDocumentCreator extending AbstractDocumentCreator.

During runtime a PdfDocumentCreator (or any other creator class) instance is passed to the DocumentService::create method. The service uses this creator class to create a document, work with it, and store it in the database.

Our library is OPEN/CLOSED (CLOSED for modifying the interfaces, OPENED for adding support of new types of documents when needed).

class UserData {

    private final long userId;


    UserData(long userId) {
        this.userId = userId;
    }

    long getUserId() {
        return userId;
    }
}

interface Document {

    void setUserData(UserData userData);

    byte[] toBytes();

}

class TxtDocument implements Document {


    @Override
    public void setUserData(UserData userData) {
        //TODO: Implement feature
    }

    @Override
    public byte[] toBytes() {
        return new byte[0];
    }
}

class HtmlDocument implements Document {

    @Override
    public void setUserData(UserData userData) {
        //TODO: Implement feature
    }

    @Override
    public byte[] toBytes() {
        //TODO: Implement feature
        return new byte[0];
    }
}

class PdfDocument implements Document {


    @Override
    public void setUserData(UserData userData) {

    }

    @Override
    public byte[] toBytes() {
        return new byte[0];
    }
}

abstract class AbstractDocumentCreator {
    abstract Document create(UserData userData);
}

class TxtDocumentCreator extends AbstractDocumentCreator {

    @Override
    Document create(UserData data) {
        TxtDocument document = new TxtDocument();
        document.setUserData(data);
        return document;
    }
}

class HtmlDocumentCreator extends AbstractDocumentCreator {

    @Override
    Document create(UserData userData) {
        TxtDocument document = new TxtDocument();
        document.setUserData(userData);
        return document;
    }
}

class DocumentService {

    private final DocumentRepository userRepository;

    DocumentService(DocumentRepository documentRepository) {
        this.documentRepository = documentRepository;
    }

    void saveInDb(AbstractCreator abstractCreator, UserData userData) {
        Document document = abstractCreator.create(userData);
        documentRepository.insert(document);
    }
}

Builder pattern - When users of your class construct your class with different sets of parameters each time, the builder pattern provides them the flexibility of constructing the object the way they like to do it. Often used to build application configuration, SQL queries, HTTP requests, or immutable classes. Provides flexibility for the user, increases the maintainability of code constructing your class, and often is more readable and easy to use than parameter constructors or factory methods, etc.

List<Person> persons = queryFactory.selectFrom(person)
   .where(
        person.firstName.eq("John"),
        person.lastName.eq("Doe"))
   .fetch();
      

or another example

Book book = new Book.Builder("0-12-345678-9", "Moby-Dick")
            .genre(Genre.ADVENTURE_FICTION)
            .author("Herman Melville")
            .published(Year.of(1851))
            .description(
                    "The book is the sailor Ishmael's narrative of the obsessive quest of "
                      "Ahab, captain of the whaling ship Pequod, for revenge on Moby Dick, "
                      "the giant white sperm whale that on the ship's previous voyage bit "
                      "off Ahab's leg at the knee."
            )

There are no strict guidelines on which design patterns to use; hence, the ability to use them comes with experience and experimentation. Play with design patterns in different contexts and talk your code over with other developers, and you will learn when to use which solution.

CodePudding user response:

The other answers are great, but they dive right into the detail, so let me give you some context.

Design principles (like SOLID) are like simple, low-level, generic rules:

  • Simple: they tend to deal with low-level, but fundamental and far reaching concepts.
  • Generic: they apply very broadly, they are not technology specific. In the case of SOLID, the only assumption is that you are doing Object Orientated programming - it doesn't even matter what language.

Design patterns are higher-level, and more specific:

  • Higher-level: like principles, they are technology agnostic (see caveat below), but unlike principles they describe parts of a solution and what it's job is as a part of that pattern.
  • More specific: they describe a solution to a particular design problem.

Caveat: It's common to see generic design patterns described using language specific code - as a way to communicate, not because the fundamental ideas of the pattern are unique to that language. Personally I would say that design patterns that are intentionally technology specific (cannot be readily used in a different technology) are really reference patterns.

CodePudding user response:

SOLID principles are well, principles. That's what they are. They are like litmus tests to see whether the design of a system is good or not. They can be used in architecture too. For example, a class should have a single responsibility. If a class is doing more than one thing, it means there is something wrong with that class. So SOLID principles don't tell you what to do in a direct way, but point you out that something is wrong here - this smell can lead you to a deep rabbit hole, so avoid it, you get the point.

Design patterns on the other hand, are not as general as SOLID principles, but they are refined solutions to common problems. Just as algorithms are refined Solutions to common problems and just like data structures are refined structures to hold different types of data, design patterns are refined solutions to common design problems. Most of the time, that design problem can be traced down to one or more of the SOLID principles. But that doesn't mean that Design Patterns cover the solution to "all" types of SOLID principles. SOLID principles are broader.

Another take on SOLID principles and Design Patterns can be that when you use SOLID principles to clean the code, the resulting code will become refactored, and then you'll be able to spot the application of Design problems easily.

What is a dependency? A class X has a method A which at some point refers to class Y -> X has a dependency on Y. This dependency can be either transparent, or can be hidden beneath bad design, such as patches over a violation of Liskov's Substitution Principle. So once you remove that violation, you see a design problem on which the Design Pattern can be applied. Without using SOLID principle, the dependency of X on Y was still there. The design problem didn't go - it was worse - it was there but it was hidden. Now after using SOLID principle, the hidden design problem becomes exposed, and then you tackle it appropriately. It's just an example.

  • Related