DEV Community

nikosst
nikosst

Posted on Edited on

Κατανοώντας τα .NET Assemblies στην Clean Architecture: Από τα Compilation Boundaries στα Architecture Tests

Όταν συζητάμε για Clean Architecture στο .NET, η προσοχή συνήθως στρέφεται σε έννοιες όπως το Domain Layer, το Application Layer, το Infrastructure Layer, το Dependency Inversion Principle, τα repositories και τα use cases.

Όλες αυτές οι έννοιες είναι θεμελιώδεις. Ωστόσο, πίσω από αυτές υπάρχει ένας μηχανισμός του .NET που δίνει στις αρχιτεκτονικές μας αποφάσεις μια πραγματική τεχνική υπόσταση: το Assembly.

Η κατανόηση ενός assembly δεν περιορίζεται στο ότι «το build παράγει ένα .dll αρχείο». Σε μία σωστά σχεδιασμένη εφαρμογή, ένα assembly μπορεί να αποτελέσει ένα πραγματικό architectural boundary, ένα όριο μεταγλώττισης που διαχωρίζει διαφορετικές ευθύνες του συστήματος.

Από τη στιγμή που αντιληφθούμε αυτή τη σχέση, τα Architecture Tests αποκτούν πολύ μεγαλύτερη σημασία. Δεν αποτελούν απλώς tests που ελέγχουν naming conventions ή namespaces. Μπορούν να επαληθεύσουν ότι η πραγματική, compiled δομή του συστήματος εξακολουθεί να ακολουθεί το dependency graph που σχεδιάσαμε.

Σε αυτό το άρθρο θα εξετάσουμε τη σχέση μεταξύ Projects, Assemblies, Reflection, Clean Architecture και Architecture Tests, χρησιμοποιώντας ως παράδειγμα μία εφαρμογή ASP.NET Core.


Από τον C# κώδικα στο Assembly

Ας θεωρήσουμε μία τυπική λύση Clean Architecture:

TestMyApp

  • TestMyApp.Domain
  • TestMyApp.Application
  • TestMyApp.Infrastructure
  • TestMyApp.Api

Μέσα στο Visual Studio βλέπουμε τέσσερα διαφορετικά projects.

Σε πρώτη ανάγνωση θα μπορούσε κανείς να θεωρήσει ότι πρόκειται απλώς για έναν τρόπο καλύτερης οργάνωσης του source code.

Η πραγματικότητα όμως είναι βαθύτερη.

Όταν πραγματοποιούμε build τη solution, κάθε project μεταγλωττίζεται και παράγει συνήθως το δικό του assembly.

Για παράδειγμα:

TestMyApp.Domain

│ Build

TestMyApp.Domain.dll

Αντίστοιχα:

TestMyApp.Application

│ Build

TestMyApp.Application.dll

και:

TestMyApp.Infrastructure

│ Build

TestMyApp.Infrastructure.dll

Επομένως, το assembly δεν αποτελεί απλώς μία σύμβαση οργάνωσης που εισάγει το Visual Studio.

Είναι ένα πραγματικό compilation artifact.

Σε υψηλό επίπεδο, η διαδικασία μπορεί να απεικονιστεί ως:

C# Source Code --> C# Compiler

      Assembly 
         IL  
      Metadata
        Types 
       Members
      Assembly 
     References 
         │ 
         ▼ 
   .NET Runtime
Enter fullscreen mode Exit fullscreen mode

Ένα .NET assembly περιλαμβάνει compiled Intermediate Language, metadata για τους τύπους που περιέχει, πληροφορίες σχετικά με το ίδιο το assembly και references προς άλλα assemblies.

Αυτό ακριβώς το χαρακτηριστικό είναι που το κάνει ιδιαίτερα σημαντικό από αρχιτεκτονικής πλευράς.


Assembly και Namespace δεν είναι το ίδιο πράγμα

Ένα από τα σημεία που δημιουργούν συχνά σύγχυση είναι η διαφορά μεταξύ namespace και assembly.

Ας υποθέσουμε ότι το Domain project έχει την ακόλουθη δομή:

Οι παραπάνω classes μπορεί να ανήκουν στα namespaces:

TestMyApp.Domain.Users
TestMyApp.Domain.Orders
TestMyApp.Domain.Products

Όμως όλες μπορούν να βρίσκονται μέσα στο ίδιο assembly:

TestMyApp.Domain.dll

Το namespace αποτελεί κυρίως έναν λογικό μηχανισμό ονομασίας και οργάνωσης των types.

Το assembly, αντίθετα, αποτελεί πραγματική μονάδα compilation και deployment του .NET.

Η διαφορά αυτή έχει ιδιαίτερη αρχιτεκτονική σημασία.

Θα μπορούσαμε να έχουμε ένα μόνο project:

και να ισχυριστούμε ότι έχουμε Clean Architecture επειδή δημιουργήσαμε τέσσερα folders.

Ωστόσο, μετά το build θα μπορούσαμε να καταλήξουμε σε:

MyApplication.dll

Όλα τα layers θα βρίσκονταν μέσα στο ίδιο assembly.

Τα directories θα μας έδιναν λογική οργάνωση, αλλά όχι πραγματικά compilation boundaries.

Αντίθετα, όταν δημιουργούμε:

Domain.csproj
Application.csproj
Infrastructure.csproj
Api.csproj

δημιουργούμε ξεχωριστές μονάδες compilation.

Μετά το build έχουμε:

Domain.dll
Application.dll
Infrastructure.dll
Api.dll

Επομένως, η αρχιτεκτονική παύει να αποτελεί απλώς μία οργάνωση directories και αρχίζει να αποτυπώνεται στην πραγματική compiled δομή του συστήματος.


Το Assembly ως Architectural Boundary

Μία από τις σημαντικότερες αρχές της Clean Architecture αφορά την κατεύθυνση των dependencies.

Σε μία απλοποιημένη μορφή μπορούμε να έχουμε:

API


Infrastructure


Application


Domain

Η ουσία δεν βρίσκεται απλώς στο ότι έχουμε τέσσερα κουτιά.

Η ουσία βρίσκεται στο ποιος επιτρέπεται να γνωρίζει ποιον.

Το Domain αποτελεί τον πυρήνα της εφαρμογής.

Δεν θα πρέπει να γνωρίζει πώς αποθηκεύονται τα δεδομένα.

Δεν θα πρέπει να γνωρίζει τι είναι HTTP request.

Δεν θα πρέπει να γνωρίζει τι είναι ASP.NET Core Controller.

Δεν θα πρέπει να γνωρίζει τι είναι Microsoft Entra ID.

Δεν θα πρέπει να γνωρίζει τι είναι Entity Framework Core.

Ένα Domain Entity θα πρέπει να μπορεί να είναι κάτι τόσο καθαρό όσο:

public class Order 
{ 
  public Guid Id { get; private set; } 
  public decimal Total { get; private set; } 
  public void Confirm() { // Business rule } 
}
Enter fullscreen mode Exit fullscreen mode

Δεν θα θέλαμε μέσα στο Domain να εμφανίζονται dependencies όπως:

Microsoft.AspNetCore.*
Microsoft.EntityFrameworkCore.*
Microsoft.Identity.*

Αυτό σημαίνει ότι αρχιτεκτονικά θέλουμε:

Domain X ───> Infrastructure
Domain X ───> API

Το Domain δεν πρέπει να εξαρτάται από αυτά.

Όταν κάθε layer βρίσκεται σε διαφορετικό project, αυτή η αρχιτεκτονική αρχίζει να αποκτά φυσική υπόσταση μέσα από τα assemblies.


Project References και Assembly Dependencies

Ας υποθέσουμε ότι στο Visual Studio προσθέτουμε:

Application


Domain

ως Project Reference.

Αυτό δεν είναι απλώς μία ρύθμιση του Visual Studio.

Δηλώνουμε ότι το Application έχει δικαίωμα να χρησιμοποιεί types του Domain.

Μετά το compilation, η σχέση αυτή αποτυπώνεται στα assemblies:

Application.dll


Domain.dll

Έτσι το Application μπορεί να χρησιμοποιήσει:

using TestMyApp.Domain.Users;

και στη συνέχεια:

public class CreateUserHandler 
{ 
  public User Handle() { // ... } 
}
Enter fullscreen mode Exit fullscreen mode

Η αντίστροφη σχέση όμως δεν πρέπει να υπάρχει.

Δεν θέλουμε το Domain να περιέχει:

using TestMyApp.Application;

Η κατεύθυνση:

Application → Domain

είναι επιτρεπτή.

Η κατεύθυνση:

Domain → Application

παραβιάζει το dependency direction που έχουμε επιλέξει.

Εδώ γίνεται εμφανής μία σημαντική αρχή:

Ένα Project Reference δεν είναι απλώς configuration του IDE. Συμμετέχει άμεσα στη dependency topology του compiled συστήματος.


Τι περιέχει ένα Assembly;

Ένα assembly δεν περιέχει απλώς τον C# source code όπως τον γράψαμε.

Σε υψηλό επίπεδο μπορούμε να το αντιληφθούμε ως:

TestMyApp.Domain.dll

Intermediate Language
Metadata
Types
User

Order

Role

Permission

Members
Methods
Properties
Fields

Assembly References

Τα metadata είναι εξαιρετικά σημαντικά.

Επιτρέπουν στο .NET Runtime αλλά και στον δικό μας κώδικα να ανακαλύψει πληροφορίες σχετικά με τη compiled εφαρμογή.

Αυτό μας οδηγεί σε μία από τις σημαντικότερες δυνατότητες του .NET: το Reflection.


Reflection: Παρατηρώντας τη δομή της εφαρμογής

Ας θεωρήσουμε ότι έχουμε:

public class User { }
Enter fullscreen mode Exit fullscreen mode

Μπορούμε να γράψουμε:

var type = typeof(User);

Το typeof(User) δεν δημιουργεί ένα User object.

Δεν είναι:

new User();

Αντίθετα, ζητάμε από το .NET πληροφορίες σχετικά με τον type User.

Μπορούμε για παράδειγμα να εξετάσουμε:

type.Name

ή:

type.Namespace

ή ακόμη πιο ενδιαφέρον:

type.Assembly

Άρα:

var assembly = typeof(User).Assembly;

σημαίνει ουσιαστικά:

Σε ποιο assembly βρίσκεται ο συγκεκριμένος type;

Αν ο User βρίσκεται στο Domain project, τότε το αποτέλεσμα αντιστοιχεί στο:

TestMyApp.Domain.dll

Έτσι δημιουργείται μία πολύ ενδιαφέρουσα σύνδεση:

Type


Reflection


Assembly


Compiled Architecture

Μπορούμε πλέον να εξετάσουμε προγραμματιστικά τη δομή της εφαρμογής μας.


Το AssemblyReference Pattern

Αν θέλουμε να πάρουμε το Domain assembly, θα μπορούσαμε να χρησιμοποιήσουμε:

typeof(User).Assembly

Τεχνικά αυτό λειτουργεί.

Δεν είναι όμως η καλύτερη δυνατή επιλογή.

Ο User αποτελεί business concept.

Μπορεί κάποια στιγμή να μετονομαστεί, να μετακινηθεί ή ακόμη και να αφαιρεθεί.

Για αυτό χρησιμοποιείται συχνά ένα απλό marker type:

namespace TestMyApp.Domain;

public sealed class AssemblyReference
{
}

Η συγκεκριμένη class φαίνεται αρχικά περίεργη.

Δεν έχει properties.

Δεν έχει methods.

Δεν έχει business logic.

Δεν προορίζεται για:

new AssemblyReference();

Ο ρόλος της είναι διαφορετικός.

Αποτελεί ένα σταθερό σημείο αναφοράς προς το assembly.

Έτσι μπορούμε να γράψουμε:

var assembly = typeof(AssemblyReference).Assembly;

Η λογική είναι:

AssemblyReference

│ βρίσκεται στο

Domain Project

│ γίνεται compile σε

TestMyApp.Domain.dll

Άρα:

typeof(AssemblyReference).Assembly

μας δίνει με σαφή και type-safe τρόπο το Domain assembly.

Η συγκεκριμένη class δεν μοντελοποιεί κάποιο business concept.

Αποτελεί έναν architectural marker.


Γιατί να μην χρησιμοποιήσουμε απλώς ένα string;

Θα μπορούσαμε θεωρητικά να προσπαθήσουμε να φορτώσουμε:

"TestMyApp.Domain"

μέσω Reflection APIs.

Ωστόσο, η χρήση:

typeof(AssemblyReference).Assembly

είναι ιδιαίτερα απλή και ασφαλής.

Δεν βασιζόμαστε σε κάποιο αυθαίρετο business type και δεν χρειάζεται να διαχειριστούμε χειροκίνητα assembly names.

Το ίδιο pattern μπορεί να χρησιμοποιηθεί σε όλα τα layers:

Domain
└── AssemblyReference.cs

Application
└── AssemblyReference.cs

Infrastructure
└── AssemblyReference.cs

Api
└── AssemblyReference.cs

Έτσι κάθε assembly αποκτά ένα σαφές entry point για reflection-based operations.


Από την αρχιτεκτονική σύμβαση στο Architecture Test

Ας υποθέσουμε ότι έχουμε αποφασίσει:

Το Domain δεν επιτρέπεται να εξαρτάται από Application, Infrastructure ή API.

Το να γράψουμε αυτή την πρόταση σε ένα README είναι χρήσιμο.

Το να τη σχεδιάσουμε σε ένα architecture diagram είναι επίσης χρήσιμο.

Όμως τίποτα από αυτά δεν εμποδίζει έναν developer να παραβιάσει τον κανόνα έξι μήνες αργότερα.

Για παράδειγμα, κάποιος μπορεί να εισαγάγει στο Domain μία εξάρτηση προς Infrastructure.

Η εφαρμογή μπορεί να συνεχίσει να λειτουργεί.

Τα business unit tests μπορεί να συνεχίσουν να περνούν.

Το API μπορεί να επιστρέφει κανονικά 200 OK.

Παρόλα αυτά, η αρχιτεκτονική έχει υποβαθμιστεί.

Αυτό μας οδηγεί στη διάκριση μεταξύ δύο διαφορετικών μορφών correctness:

Behavioural Correctness
και
Structural Correctness

Ένα κλασικό Unit Test εξετάζει συνήθως:

Για συγκεκριμένο input,
παράγει η μέθοδος το σωστό output;

Ένα Architecture Test εξετάζει κάτι διαφορετικό:

Η compiled δομή του συστήματος
εξακολουθεί να ακολουθεί
τους αρχιτεκτονικούς κανόνες;


Architecture Testing με NetArchTest

Ένα χαρακτηριστικό architecture test μπορεί να είναι:

`using NetArchTest.Rules;
using TestMyApp.Domain;

namespace TestMyApp.ArchitectureTests;

public class DomainArchitectureTests
{
[Fact]
public void Domain_Should_Not_Have_Dependency_On_Other_Layers()
{
var assembly = typeof(AssemblyReference).Assembly;

    var result = Types
        .InAssembly(assembly)
        .ShouldNot()
        .HaveDependencyOnAny(
            "TestAILoginAuthoAuthed.Application",
            "TestAILoginAuthoAuthed.Infrastructure",
            "TestAILoginAuthoAuthed.Api")
        .GetResult();

    Assert.True(result.IsSuccessful);
}
Enter fullscreen mode Exit fullscreen mode

}`

Παρότι πρόκειται για ελάχιστο κώδικα, το test εκφράζει μία σημαντική αρχιτεκτονική πολιτική.

Ας εξετάσουμε τι πραγματικά συμβαίνει.

Αρχικά:

var assembly = typeof(AssemblyReference).Assembly;

εντοπίζουμε το Domain assembly.

Στη συνέχεια:

Types.InAssembly(assembly)

ζητάμε από το NetArchTest να εξετάσει τους types που υπάρχουν μέσα στο συγκεκριμένο assembly.

Εννοιολογικά:

TestMyApp.Domain.dll


Examine Types

┌───────┼────────┐
▼ ▼ ▼
User Order Role

Στη συνέχεια:

.ShouldNot()
.HaveDependencyOnAny(...)

εκφράζει τον αρχιτεκτονικό περιορισμό.

Ουσιαστικά το test ρωτά:

Για τους types του Domain assembly:

Υπάρχει dependency προς Application;
Υπάρχει dependency προς Infrastructure;
Υπάρχει dependency προς API?

     │
┌────┴────┐
│         │
Enter fullscreen mode Exit fullscreen mode

ΝΑΙ ΟΧΙ
│ │
▼ ▼
FAIL PASS

Έτσι ένας κανόνας που προηγουμένως υπήρχε μόνο ως αρχιτεκτονική πρόθεση μετατρέπεται σε εκτελέσιμο specification.


Unit Tests και Architecture Tests προστατεύουν διαφορετικά πράγματα

Ας θεωρήσουμε ένα Unit Test:

[Fact]
public void Order_Should_Calculate_Total()
{
// ...
}

Το συγκεκριμένο test προστατεύει μία επιχειρησιακή συμπεριφορά.

Θέλουμε να γνωρίζουμε ότι το Order υπολογίζει σωστά το total.

Αντίθετα:

[Fact]
public void Domain_Should_Not_Depend_On_Infrastructure()
{
// ...
}

δεν ενδιαφέρεται για το αποτέλεσμα κάποιου business calculation.

Προστατεύει τη δομή του συστήματος.

Έχουμε επομένως:

              Software Correctness
                      │
           ┌──────────┴──────────┐
           │                     │
           ▼                     ▼
   Behavioural Rules       Structural Rules
           │                     │
           ▼                     ▼
      Unit Tests          Architecture Tests
Enter fullscreen mode Exit fullscreen mode

Αυτές οι δύο κατηγορίες tests είναι συμπληρωματικές.

Το γεγονός ότι μία εφαρμογή παράγει σωστά αποτελέσματα δεν σημαίνει ότι διαθέτει σωστή αρχιτεκτονική.

Και αντίστροφα, μία άψογη dependency structure δεν σημαίνει ότι το business logic λειτουργεί σωστά.


Η έννοια του Architectural Fitness Function

Ένας ιδιαίτερα χρήσιμος τρόπος να αντιληφθούμε τα Architecture Tests είναι ως Architectural Fitness Functions.

Κατά τον σχεδιασμό του συστήματος αποφασίζουμε ότι ορισμένες ιδιότητες πρέπει να παραμένουν αληθείς όσο εξελίσσεται η εφαρμογή.

Για παράδειγμα:

Το Domain πρέπει να παραμένει ανεξάρτητο.

Το Application δεν πρέπει να εξαρτάται από Infrastructure.

Οι Controllers δεν πρέπει να χρησιμοποιούν απευθείας DbContext.

Το Domain δεν πρέπει να γνωρίζει το Entity Framework Core.

Το Domain δεν πρέπει να γνωρίζει το ASP.NET Core.

Το persistence implementation πρέπει να παραμένει εκτός Domain.

Τα Architecture Tests μπορούν να αξιολογούν συνεχώς αυτές τις ιδιότητες.

Έτσι μετακινούμαστε από ένα μοντέλο:

Architecture
=
Documentation
+
Developer Discipline

σε ένα ισχυρότερο μοντέλο:

Architecture
=
Documentation
+
Compilation Boundaries
+
Automated Verification

Αυτό είναι ιδιαίτερα σημαντικό σε συστήματα που πρόκειται να συντηρηθούν για χρόνια.


Το πραγματικό νόημα ενός failing Architecture Test

Ένα Architecture Test γίνεται ακόμη πιο κατανοητό όταν το δούμε να αποτυγχάνει.

Ας υποθέσουμε ότι κάποιος δημιουργεί dependency:

Domain

└────────────► Infrastructure

ενώ η αρχιτεκτονική απαιτεί:

Domain
X────────────► Infrastructure

Το application μπορεί να εξακολουθεί να κάνει compile.

Ένα endpoint μπορεί να συνεχίζει να επιστρέφει:

200 OK

Όμως το architecture test θα αποτύχει.

Αυτό δεν σημαίνει απαραίτητα ότι έχουμε functional bug.

Σημαίνει ότι έχουμε architectural regression.

Η διάκριση είναι σημαντική.

Ένα failing business test μπορεί να μας πει:

Η εφαρμογή δεν συμπεριφέρεται όπως πρέπει.

Ένα failing architecture test μπορεί να μας πει:

Η εφαρμογή εξακολουθεί ίσως να συμπεριφέρεται σωστά, αλλά η εσωτερική της δομή έχει αρχίσει να αποκλίνει από την αρχιτεκτονική που συμφωνήσαμε.

Σε ένα CI/CD pipeline αυτό είναι εξαιρετικά ισχυρό.

Ένα Pull Request που εισάγει απαγορευμένο dependency μπορεί να αποτύχει πριν γίνει merge.

Η αρχιτεκτονική αποκτά επομένως έναν βαθμό αυτοπροστασίας.


Γιατί έχει μεγαλύτερη σημασία σε μεγάλα συστήματα

Σε μία εφαρμογή δύο χιλιάδων γραμμών κώδικα, ένας developer μπορεί ίσως να γνωρίζει ολόκληρο το dependency graph.

Σε ένα production σύστημα εκατοντάδων χιλιάδων γραμμών αυτό δεν είναι ρεαλιστικό.

Οι ομάδες αλλάζουν.

Νέοι developers προστίθενται.

Τα requirements μεταβάλλονται.

Νέα integrations εμφανίζονται.

Τα deadlines δημιουργούν πίεση.

Το Infrastructure μεγαλώνει.

Χωρίς μηχανισμούς ελέγχου, μία αρχικά καθαρή αρχιτεκτονική μπορεί σταδιακά να μετατραπεί σε:

Domain ─────────► Infrastructure
▲ │
│ ▼
Application ◄──────── API
│ ▲
└───────────────────┘

Το σύστημα μπορεί να εξακολουθεί να λειτουργεί, αλλά η δυνατότητα ανεξάρτητης εξέλιξης των components έχει περιοριστεί σημαντικά.

Αυτό το φαινόμενο συχνά περιγράφεται ως architectural erosion.

Τα compilation boundaries και τα architecture tests δεν εξαφανίζουν το πρόβλημα, αλλά δημιουργούν έναν σημαντικό μηχανισμό αντίστασης απέναντί του.


Τα Assemblies δεν δημιουργούν από μόνα τους Clean Architecture

Υπάρχει, ωστόσο, μία σημαντική παγίδα.

Το γεγονός ότι δημιουργήσαμε:

Domain
Application
Infrastructure
Api

ως τέσσερα διαφορετικά projects δεν σημαίνει αυτομάτως ότι έχουμε Clean Architecture.

Μπορούμε κάλλιστα να δημιουργήσουμε τέσσερα assemblies με λανθασμένα dependencies.

Για παράδειγμα:

Domain ───────► Infrastructure
Application ──► API
Infrastructure ► Application
API ──────────► Domain

Τα projects από μόνα τους δεν λύνουν το πρόβλημα.

Παρέχουν όμως έναν μηχανισμό πάνω στον οποίο μπορούμε να επιβάλουμε πραγματικά boundaries.

Η αρχιτεκτονική απαιτεί πρώτα συνειδητές αποφάσεις σχετικά με τα dependencies.

Στη συνέχεια τα assemblies και τα tests μπορούν να βοηθήσουν στην επιβολή αυτών των αποφάσεων.


Architecture Tests χωρίς αρχιτεκτονικό λόγο είναι απλώς ceremony

Το ίδιο ισχύει και για τα Architecture Tests.

Δεν έχει ιδιαίτερη αξία να δημιουργούμε δεκάδες tests μόνο και μόνο επειδή μπορούμε.

Κάθε architectural rule θα πρέπει να απαντά στην ερώτηση:

Ποια σημαντική ιδιότητα του συστήματος προστατεύει αυτός ο κανόνας;

Για παράδειγμα:

Domain must not depend on Infrastructure

έχει ξεκάθαρη αιτιολόγηση.

Προστατεύει την ανεξαρτησία των business rules από τεχνολογικές λεπτομέρειες.

Αντίθετα, ένας αυθαίρετος κανόνας naming χωρίς πραγματικό architectural impact μπορεί να δημιουργήσει περισσότερο θόρυβο παρά αξία.

Τα Architecture Tests είναι περισσότερο χρήσιμα όταν προστατεύουν:

Dependency Direction
Coupling
Separation of Concerns
Replaceability
Testability
Technology Independence

και όχι όταν χρησιμοποιούνται απλώς ως μηχανισμός επιβολής προσωπικών προτιμήσεων.


Η συνολική σχέση

Μπορούμε πλέον να ενώσουμε όλες τις έννοιες σε μία ακολουθία:

Architectural Decision


Project Boundary


Compilation Boundary


Assembly


Assembly Metadata / Types


Reflection


NetArchTest


Automated Architecture Test


CI/CD Validation

Αυτή είναι ίσως η σημαντικότερη ιδέα.

Ξεκινάμε από μία αφηρημένη αρχιτεκτονική απόφαση:

Το Domain πρέπει να είναι ανεξάρτητο από το Infrastructure.

Τη μετατρέπουμε σε project boundaries.

Τα projects μετατρέπονται σε assemblies.

Τα assemblies διαθέτουν metadata.

Το Reflection μας επιτρέπει να εξετάσουμε αυτά τα metadata.

Το NetArchTest μπορεί να εφαρμόσει κανόνες πάνω στους types των assemblies.

Το xUnit μπορεί να εκτελέσει αυτούς τους κανόνες ως tests.

Και τελικά ένα CI/CD pipeline μπορεί να εμποδίσει την εισαγωγή ενός architectural regression στην κύρια έκδοση του συστήματος.

Έτσι μία αφηρημένη αρχιτεκτονική αρχή καταλήγει να είναι machine-verifiable.


Να θυμάσαι..

Η κατανόηση των assemblies αλλάζει σημαντικά τον τρόπο με τον οποίο μπορούμε να αντιληφθούμε την Clean Architecture στο .NET.

Η αρχιτεκτονική δεν είναι πλέον απλώς:

Domain
Application
Infrastructure
API

σχεδιασμένη ως τέσσερα κουτιά σε ένα διάγραμμα.

Τα κουτιά αυτά μπορούν να αντιστοιχούν σε πραγματικές compilation units:

Domain.dll
Application.dll
Infrastructure.dll
Api.dll

με σαφείς dependency relationships μεταξύ τους.

Το assembly αποτελεί επομένως κάτι περισσότερο από το αποτέλεσμα του compiler. Σε μία συνειδητά σχεδιασμένη εφαρμογή μπορεί να αποτελέσει τεχνική αναπαράσταση ενός architectural boundary.

Το Reflection μας επιτρέπει να εξετάσουμε αυτή την αναπαράσταση.

Ένας απλός marker type:

public sealed class AssemblyReference
{
}

μας παρέχει ένα σταθερό σημείο πρόσβασης:

typeof(AssemblyReference).Assembly

και από εκεί μπορούμε να εξετάσουμε το compiled Domain:

Assembly

Types

Dependencies

Architectural Rules

Με εργαλεία όπως το NetArchTest μπορούμε στη συνέχεια να μετατρέψουμε μία αρχιτεκτονική αρχή όπως:

Domain must not depend on Infrastructure

σε executable test.

Αυτό αποτελεί μία ουσιαστική μετάβαση από τη documented architecture προς την executable architecture.

Σε ένα ώριμο software system, η αρχιτεκτονική δεν θα πρέπει να εξαρτάται αποκλειστικά από τη μνήμη των developers, από ένα παλιό διάγραμμα ή από την προσοχή του reviewer σε κάθε Pull Request.

Οι σημαντικότεροι κανόνες της μπορούν, όπου αυτό είναι τεχνικά εφικτό και αρχιτεκτονικά χρήσιμο, να αποτελούν μέρος του ίδιου του μηχανισμού επαλήθευσης του συστήματος.

Και εκεί βρίσκεται ίσως η μεγαλύτερη χρησιμότητα των assemblies στο συγκεκριμένο πλαίσιο:

μετατρέπουν τα αρχιτεκτονικά όρια από αφηρημένες γραμμές ενός διαγράμματος σε πραγματικά όρια του compiled software — όρια που μπορούμε να παρατηρήσουμε, να εξετάσουμε και τελικά να ελέγξουμε αυτοματοποιημένα.

Top comments (0)