O arhitectură hexagonală simplifică amânarea sau schimbarea deciziilor tehnologice. Vrei să treci la un cadru diferit? Scrieți un adaptor nou. Doriți să utilizați o bază de date, în loc să stocați date în fișiere? Din nou, scrieți un adaptor pentru acesta.

Desenați o graniță în jurul logicii de afaceri. Hexagonul. Orice din interiorul hexagonului trebuie să fie lipsit de preocupări tehnologice.
Exteriorul hexagonului vorbește cu interiorul numai folosind interfețe, numite porturi. La fel invers. Schimbând implementarea unui port, schimbați tehnologia.

Izolarea logicii de afaceri în interiorul hexagonului are un alt avantaj. Permite scrierea unor teste rapide și stabile pentru logica de afaceri. De exemplu, nu depind de tehnologia web.

Iată un exemplu de diagramă. Prezintă tehnologia Spring MVC ca cutii cu linii punctate, porturi și adaptoare ca cutii solide și hexagonul fără elementele sale interne:

Implementarea unei arhitecturi

Un adaptor se traduce între o tehnologie specifică și un port fără tehnologie. PoemController adaptorul din stânga primește cereri și trimite comenzi către IReactToCommands port. PoemController este un controler MVC obișnuit de primăvară. Deoarece folosește în mod activ portul, se numește adaptor pentru driver.

IReactToCommands se numește port driver. Implementarea sa se face în interiorul hexagonului. Nu este afișat pe diagramă.

În partea dreaptă, SpringMvcPublisher adaptorul implementează IWriteLines port. De data aceasta, hexagon apelează adaptorul prin port. De aceea SpringMvcPublisher se numește adaptor acționat. Și IWriteLines se numește port condus.

Vă arăt cum să implementați acea aplicație. Trecem de la o poveste de utilizator la un model de domeniu din interiorul hexagonului. Începem cu o versiune simplă a aplicației care se imprimă pe consolă. Apoi trecem la Spring Boot și Spring MVC.

De la o poveste de utilizator la porturi și adaptoare

Compania FooBars.io decide să construiască o aplicație de poezie. Proprietarul produsului și dezvoltatorii sunt de acord cu următoarea poveste a utilizatorului:

Ca cititor
Vreau să citesc cel puțin o poezie în fiecare zi
În așa fel încât să mă dezvolt ca ființă umană

Ca criterii de acceptare, echipa este de acord asupra:

  • Când utilizatorul solicită o poezie într-o anumită limbă, sistemul afișează o poezie aleatorie în limba respectivă în consolă
  • Este ok să „simulați” utilizatorul la început, adică să nu existe o interacțiune reală a utilizatorului. (Acest lucru se va schimba în versiunile viitoare.)
  • Limbi acceptate: engleză, germană

Dezvoltatorii se întâlnesc și desenează următoarea diagramă:

poem-hexagon

Asa ca SimulatedUser trimite comenzi către IReactToCommands port. Solicită poezii în engleză și germană. Iată codul, este disponibil pe Github.

poem / simplu / driver_adapter /SimulatedUser.java

public class SimulatedUser {
    private IReactToCommands driverPort;

    public SimulatedUser(IReactToCommands driverPort) {
        this.driverPort = driverPort;
    }

    public void run() {
        driverPort.reactTo(new AskForPoem("en"));
        driverPort.reactTo(new AskForPoem("de"));
    }
}

IReactToCommands portul are o singură metodă pentru a primi orice fel de comandă.

poem / hotar / driver_port /IReactToCommands.java

public interface IReactToCommands{
    void reactTo(Object command);
}

AskForPoem este comanda. Instanțele sunt POJO-uri simple, imuabile. Ei poartă limba poeziei solicitate.

poem / comandă /AskForPoem.java

public class AskForPoem {
    private String language;

    public AskForPoem(String language) {
        this.language = language;
    }

    public String getLanguage() {
        return language;
    }
}

Și asta este tot pentru partea stângă, șofer a hexagonului. Pe partea dreaptă, condusă.

poem-hexagon: condus cu fața în sus în continuare

Cand SimulatedUser întreabă IReactToCommands port pentru o poezie, hexagonul:

  1. Contactează IObtainPoems port pentru o colecție de poezii
  2. Alege un poem aleatoriu din colecție
  3. Spune IWriteLines port pentru a scrie poezia pe dispozitivul de ieșire

Nu puteți vedea încă Pasul 2. Se întâmplă în interiorul hexagonului, în modelul de domeniu. Aceasta este logica de afaceri a exemplului. Deci, ne concentrăm mai întâi pe Pasul 1 și Pasul 3.

La Pasul 1, colecția de poezii este o matrice dependentă de limbă, codificată greu. Este furnizat de HardcodedPoemLibrary adaptor care implementează IObtainPoems port.

poem / graniță / port_directat /IObtainPoems.java

public interface IObtainPoems {
    String[] getMePoems(String language);
}

poem / simplu / driven_adapter /HardcodedPoemLibrary.java

public class HardcodedPoemLibrary implements IObtainPoems {
    public String[] getMePoems(String language) {
        if ("de".equals(language)) {
            return new String[] { /* Omitted for brevity */ };
        } else { 
            return new String[] { /* Omitted for brevity */ };
        }
    }
}

La pasul 3, ConsoleWriter adaptor scrie liniile poeziilor pe dispozitivul de ieșire, adică consola.

poem / frontieră / port_directat /IWriteLines.java

public interface IWriteLines {
    void writeLines(String[] strings);
}

poem / simplu / driven_adapter /ConsoleWriter.java

public class ConsoleWriter implements IWriteLines {
    public void writeLines(String[] lines) {
        Objects.requireNonNull(lines);
        for (String line : lines) {
            System.out.println(line);
        }
        System.out.println("");
    }
}

Am creat toate porturile și o implementare simplă a tuturor adaptoarelor. Până în prezent, interiorul hexagonului a rămas un mister. Urmează.

poem-hexagon: înăuntru în sus alături

Manipulatori de comandă (în interiorul hexagonului)

Când un utilizator cere o poezie, sistemul afișează o poezie aleatorie.
Similar în cod: când IReactToCommands portul primește un AskForPoemcomandă, hexagonul numește a DisplayRandomPoem handler de comandă.

DisplayRandomPoem handler-ul de comandă obține o listă de poezii, alege una aleatorie și o scrie pe dispozitivul de ieșire. Aceasta este exact lista pașilor despre care am vorbit în ultima clauză.

poem / hotar / intern / comandă_handler /DisplayRandomPoem.java

public class DisplayRandomPoem implements Consumer<AskForPoem> {
        /* Omitted for brevity */

    @Override
    public void accept(AskForPoem askForPoem) {
        List<Poem> poems = obtainPoems(askForPoem);
        Optional<Poem> poem = pickRandomPoem(poems);
        writeLines(poem);   
    }

        /* Rest of class omitted for brevity */
}  

Este, de asemenea, sarcina gestionarului de comenzi să traducă între datele modelului de domeniu și datele utilizate în interfețele de port.

Legarea comenzilor pentru gestionarea comenzilor

În implementarea mea a unei arhitecturi hexagonale, există doar un singur port de driver, IReactToCommands. Reacționează la toate tipurile de comenzi.

public interface IReactToCommands{
    void reactTo(Object command);
}

Boundary clasa este implementarea IReactToCommands port. Se creează un model de caz de utilizare folosind un bibliotecă. Modelul de caz de utilizare mapează fiecare tip de comandă la un handler de comandă. A ModelRunner rulează modelul și expediază comenzi bazate pe modelul cazului de utilizare.

poem / hotar /Limita.java

poem/boundary/Boundary.java

public class Boundary implements IReactToCommands {
	private static final Class<AskForPoem> asksForPoem = AskForPoem.class;

	private Model model;

	public Boundary(IObtainPoems poemObtainer, IWriteLines lineWriter) {
		model = buildModel(poemObtainer, lineWriter);
	}

	private Model buildModel(IObtainPoems poemObtainer, IWriteLines lineWriter) {
		// Create the command handler(s)
		DisplayRandomPoem displaysRandomPoem = new DisplayRandomPoem(poemObtainer, lineWriter);

		// With a use case model, map classes of command objects to command handlers.
		Model model = Model.builder()
			.user(asksForPoem).system(displaysRandomPoem)
		.build();
		
		return model;
	}

	@Override
	public void reactTo(Object commandObject) {
		new ModelRunner().run(model).reactTo(commandObject);
	}
}

Modelul domeniului

Modelul de domeniu al exemplului nu are o funcționalitate foarte interesantă. RandomPoemPicker alege o poezie aleatorie dintr-o listă.

A Poem are un constructor care ia un șir care conține separatoare de linii și îl împarte în versuri.

Partea cu adevărat interesantă despre exemplul de model de domeniu: nu se referă la o bază de date sau la orice altă tehnologie, nici măcar prin interfață!

Asta înseamnă că puteți testa modelul de domeniu cu teste unitare simple. Nu trebuie să batjocorești nimic.

Un astfel de model de domeniu pur nu este o proprietate necesară a unei aplicații care implementează o arhitectură hexagonală. Dar îmi place decuplarea și testabilitatea pe care le oferă.

Conectați adaptoare în porturi și atât

Rămâne un ultim pas pentru ca aplicația să funcționeze. Aplicația are nevoie de o clasă principală care creează adaptoarele conduse. Le injectează în graniță.
Apoi creează adaptorul driverului pentru limită și îl rulează.

poem / simplu /Main.java

public class Main {
    public static void main(String[] args) {
        new Main().startApplication();
    }

    private void startApplication() {
        // Instantiate driven, right-side adapters
        HardcodedPoemLibrary poemLibrary = new HardcodedPoemLibrary();
        ConsoleWriter consoleWriter = new ConsoleWriter();

        // Inject driven adapters into boundary
        Boundary boundary = new Boundary(poemLibrary, consoleWriter);

        // Start the driver adapter for the application
        new SimulatedUser(boundary).run();
    }
}

Si asta e! Echipa arată rezultatul proprietarului produsului. Și este mulțumită de progres. E timpul pentru o mică sărbătoare.

hexagon-poem cererea completată

Trecerea la primăvară

Echipa decide să transforme aplicația poem într-o aplicație web. Și pentru a stoca poezii într-o bază de date reală. Sunt de acord să utilizeze cadrul de primăvară pentru a-l implementa.
Înainte de a începe codificarea, echipa se întâlnește și desenează următoarea diagramă:

grafik 1

În loc de un SimulatedUser, este un PoemController acum, asta trimite comenzi către hexagon.

poem / springboot / driver_adapter /PoemController.java

@Controller
public class PoemController {
    private SpringMvcBoundary springMvcBoundary;

    @Autowired
    public PoemController(SpringMvcBoundary springMvcBoundary) {
        this.springMvcBoundary = springMvcBoundary;
    }

    @GetMapping("/askForPoem")
    public String askForPoem(@RequestParam(name = "lang", required = false, defaultValue = "en") String language,
            Model webModel) {
        springMvcBoundary.basedOn(webModel).reactTo(new AskForPoem(language));

        return "poemView";
    }
}

Când primiți o comandă, PoemController apeluri springMvcBoundary.basedOn(webModel). Acest lucru creează un nou Boundary exemplu, bazat pe webModel cererii:

poem / primăvară / hotar /SpringMvcBoundary.java

public class SpringMvcBoundary {
    private final IObtainPoems poemObtainer;

    public SpringMvcBoundary(IObtainPoems poemObtainer) {
        this.poemObtainer = poemObtainer;
    }

    public IReactToCommands basedOn(Model webModel) {
        SpringMvcPublisher webPublisher = new SpringMvcPublisher(webModel);
        IReactToCommands boundary = new Boundary(poemObtainer, webPublisher);
        return boundary;
    }
}

Apelul la reactTo() trimite comanda la graniță, ca înainte. În partea dreaptă a hexagonului, SpringMvcPublisher adaugă un atribut lines la modelul Spring MVC. Aceasta este valoarea pe care o folosește Thymeleaf pentru a insera liniile în pagina web.

poem / springboot / driven_adapter /SpringMvcPublisher.java

public class SpringMvcPublisher implements IWriteLines {
    static final String LINES_ATTRIBUTE = "lines";

    private Model webModel;

    public SpringMvcPublisher(Model webModel) {
        this.webModel = webModel;
    }

    public void writeLines(String[] lines) {
        Objects.requireNonNull(lines);
        webModel.addAttribute(LINES_ATTRIBUTE, lines);
    }
}

Echipa implementează, de asemenea, un PoemRepositoryAdapter pentru a accesa PoemRepository. Adaptorul primește Poem obiecte din baza de date. Întoarce textele tuturor poeziilor sub formă de matrice de șiruri.

poem / springboot / driven_adapter /PoemRepositoryAdapter.java

public class PoemRepositoryAdapter implements IObtainPoems {
    private PoemRepository poemRepository;

    public PoemRepositoryAdapter(PoemRepository poemRepository) {
        this.poemRepository = poemRepository;
    }

    @Override
    public String[] getMePoems(String language) {
        Collection<Poem> poems = poemRepository.findByLanguage(language);
        final String[] poemsArray = poems.stream()
            .map(p -> p.getText())
            .collect(Collectors.toList())
            .toArray(new String[0]);
        return poemsArray;
    }
}

În cele din urmă, echipa implementează Cerere clasă care configurează un exemplu de depozit și conectează adaptoarele la porturi.

Si asta e. Trecerea la Spring este completă.

Concluzie

Există multe modalități de a implementa o arhitectură hexagonală. Ți-am arătat o abordare simplă, care oferă un API ușor de utilizat, comandat de comenzi pentru hexagon. Reduce numărul de interfețe pe care trebuie să le implementați. Și duce la un model de domeniu pur.

Dacă doriți să obțineți mai multe informații despre acest subiect, citiți Articolul original al lui Alistair Cockburn pe această temă.

Exemplul din acest articol este inspirat dintr-o serie de trei părți vorbește de Alistair Cockburn pe această temă.

Ultima actualizare la 13 aprilie 2020. Dacă doriți să țineți pasul cu ceea ce fac sau să trimiteți o notă, urmați-mă dev.to, LinkedIn sau Stare de nervozitate. Sau vizitați-o pe a mea Proiectul GitHub. Pentru a afla despre dezvoltarea software-ului agil, vizitați cursul meu online.