Pular para o conteúdo principal

Padrões Arquiteturais

Visão Geral

O Acesso ao Emprego implementa padrões arquiteturais modernos e comprovados para garantir escalabilidade, manutenibilidade e confiabilidade.

Backend - Arquitetura Django

Domain-Driven Design (DDD)

O backend segue princípios de DDD com uma clara separação em camadas:

Estrutura de Camadas

app/
├── apps/ # Bounded Contexts
│ ├── accounts/ # Contexto de Contas
│ │ ├── domain/ # Camada de Domínio
│ │ │ ├── models/ # Entidades e Agregados
│ │ │ ├── services/ # Serviços de Domínio
│ │ │ ├── choices/ # Enums e Value Objects
│ │ │ └── permissions/ # Regras de Autorização
│ │ ├── infrastructure/ # Camada de Infraestrutura
│ │ │ └── migrations/ # Migrações do Banco
│ │ └── presentation/ # Camada de Apresentação
│ │ ├── serializers/ # Serialização de Dados
│ │ ├── views/ # Controllers/Views
│ │ └── urls/ # Rotas da API
│ ├── candidates/ # Contexto de Candidatos
│ ├── companies/ # Contexto de Empresas
│ └── job_vacancies/ # Contexto de Vagas

Clean Architecture

Camada de Domínio

# domain/models/candidate.py
class Candidate(models.Model):
"""Entidade de Domínio - Candidato"""
user = models.OneToOneField(CustomUser, on_delete=models.CASCADE)
cpf = models.CharField(max_length=11, unique=True)
birth_date = models.DateField()

class Meta:
db_table = 'candidates'

def can_apply_to_job(self, job):
"""Regra de negócio no domínio"""
return self.is_profile_complete() and job.is_active()

Camada de Serviços

# domain/services/user_service.py
class UserService:
"""Serviço de Domínio - Lógica de Negócio"""

@staticmethod
def create_candidate_user(validated_data):
"""Orquestra a criação de um usuário candidato"""
with transaction.atomic():
user = CustomUser.objects.create_user(
email=validated_data['email'],
password=validated_data['password']
)
candidate = Candidate.objects.create(
user=user,
cpf=validated_data['cpf']
)
return candidate

Camada de Apresentação

# presentation/views/candidate_apiview.py
class CandidateAPIView(APIView):
"""Controller - Expõe a API REST"""
permission_classes = [IsAuthenticated]

def get(self, request):
candidates = CandidateService.get_filtered_candidates(
request.query_params
)
serializer = CandidateSerializer(candidates, many=True)
return Response(serializer.data)

Repository Pattern

# infrastructure/repositories/candidate_repository.py
class CandidateRepository:
"""Abstração do acesso a dados"""

def find_by_cpf(self, cpf: str) -> Optional[Candidate]:
try:
return Candidate.objects.select_related('user').get(cpf=cpf)
except Candidate.DoesNotExist:
return None

def find_by_skills(self, skills: List[str]) -> QuerySet:
return Candidate.objects.filter(
hard_skills__name__in=skills
).distinct()

Service Layer Pattern

# domain/services/selective_process_service.py
class SelectiveProcessService:
"""Serviço de Aplicação - Casos de Uso"""

def __init__(self):
self.candidate_repo = CandidateRepository()
self.job_repo = JobRepository()
self.notification_service = NotificationService()

def apply_to_job(self, candidate_id: int, job_id: int) -> Application:
"""Caso de uso: Candidatar-se a uma vaga"""
candidate = self.candidate_repo.find_by_id(candidate_id)
job = self.job_repo.find_by_id(job_id)

# Validações de negócio
if not candidate.can_apply_to_job(job):
raise BusinessException("Candidato não pode se candidatar")

# Criar candidatura
application = Application.objects.create(
candidate=candidate,
job=job,
status='pending'
)

# Notificar empresa
self.notification_service.notify_new_application(application)

return application

Frontend - Arquitetura React

Component-Based Architecture

Estrutura de Componentes

src/
├── presentation/
│ ├── components/ # Componentes Reutilizáveis
│ │ ├── common/ # Componentes Genéricos
│ │ │ ├── Button/
│ │ │ ├── InputField/
│ │ │ └── Table/
│ │ ├── auth/ # Componentes de Autenticação
│ │ └── layout/ # Componentes de Layout
│ ├── pages/ # Páginas/Containers
│ │ ├── private/ # Páginas Autenticadas
│ │ │ ├── candidate/
│ │ │ └── company/
│ │ └── login/ # Páginas Públicas
│ └── layouts/ # Layouts Base
├── application/ # Camada de Aplicação
│ ├── hooks/ # Custom Hooks
│ └── queries/ # React Query Queries
├── infrastructure/ # Camada de Infraestrutura
│ └── api/ # Chamadas API
├── store/ # Gerenciamento de Estado
└── utils/ # Utilitários

Custom Hooks Pattern

// application/hooks/useLocalStorage.tsx
export function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});

const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error saving to localStorage:`, error);
}
};

return [storedValue, setValue] as const;
}

State Management com Zustand

// store/auth/authStore.ts
interface AuthStore {
user: User | null;
token: string | null;
isAuthenticated: boolean;

login: (credentials: LoginCredentials) => Promise<void>;
logout: () => void;
updateProfile: (data: Partial<User>) => void;
}

export const useAuthStore = create<AuthStore>((set, get) => ({
user: null,
token: localStorage.getItem('token'),
isAuthenticated: !!localStorage.getItem('token'),

login: async (credentials) => {
const response = await authAPI.login(credentials);
localStorage.setItem('token', response.token);
set({
user: response.user,
token: response.token,
isAuthenticated: true
});
},

logout: () => {
localStorage.removeItem('token');
set({
user: null,
token: null,
isAuthenticated: false
});
},

updateProfile: (data) => {
set((state) => ({
user: state.user ? { ...state.user, ...data } : null
}));
}
}));

Query Management com TanStack Query

// application/queries/candidate/candidate.ts
export const useCandidateProfile = () => {
return useQuery({
queryKey: ['candidate', 'profile'],
queryFn: async () => {
const response = await candidateAPI.getProfile();
return response.data;
},
staleTime: 5 * 60 * 1000, // 5 minutos
cacheTime: 10 * 60 * 1000, // 10 minutos
});
};

export const useUpdateCandidate = () => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: candidateAPI.updateProfile,
onSuccess: (data) => {
queryClient.setQueryData(['candidate', 'profile'], data);
toast.success('Perfil atualizado com sucesso!');
},
onError: (error) => {
toast.error('Erro ao atualizar perfil');
}
});
};

Component Composition Pattern

// presentation/components/common/Table/Table.tsx
interface TableProps<T> {
data: T[];
columns: Column<T>[];
onRowClick?: (row: T) => void;
loading?: boolean;
}

export function Table<T>({ data, columns, onRowClick, loading }: TableProps<T>) {
if (loading) return <TableSkeleton />;

return (
<TableContainer>
<MUITable>
<TableHead>
<TableRow>
{columns.map((col) => (
<TableCell key={col.id}>{col.label}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{data.map((row, index) => (
<TableRow
key={index}
onClick={() => onRowClick?.(row)}
hover
>
{columns.map((col) => (
<TableCell key={col.id}>
{col.render ? col.render(row) : row[col.field]}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</MUITable>
</TableContainer>
);
}

Padrões de Integração

API REST Pattern

// infrastructure/api/candidate/candidate-api.ts
class CandidateAPI {
private axios: AxiosInstance;

constructor() {
this.axios = axios.create({
baseURL: process.env.REACT_APP_API_URL,
headers: {
'Content-Type': 'application/json'
}
});

// Interceptor para adicionar token
this.axios.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
}

async getProfile(): Promise<CandidateProfile> {
const response = await this.axios.get('/api/v1/candidates/me');
return response.data;
}

async updateProfile(data: Partial<CandidateProfile>): Promise<CandidateProfile> {
const response = await this.axios.patch('/api/v1/candidates/me', data);
return response.data;
}
}

export const candidateAPI = new CandidateAPI();

Error Handling Pattern

// infrastructure/api/errors.ts
export class APIError extends Error {
constructor(
public status: number,
public code: string,
message: string,
public details?: any
) {
super(message);
this.name = 'APIError';
}
}

export const handleAPIError = (error: any): never => {
if (error.response) {
throw new APIError(
error.response.status,
error.response.data.code || 'UNKNOWN',
error.response.data.message || 'Erro desconhecido',
error.response.data.details
);
}
throw new Error('Erro de conexão');
};

Padrões de Segurança

Authentication & Authorization

# Backend - Django
class IsOwnerPermission(permissions.BasePermission):
"""Permissão customizada - apenas o dono pode acessar"""

def has_object_permission(self, request, view, obj):
return obj.user == request.user

class IsCandidatePermission(permissions.BasePermission):
"""Permissão para candidatos"""

def has_permission(self, request, view):
return hasattr(request.user, 'candidate')
// Frontend - React
const PrivateRoute: React.FC<{ children: ReactNode }> = ({ children }) => {
const { isAuthenticated } = useAuthStore();
const location = useLocation();

if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}

return <>{children}</>;
};

Padrões de Performance

Lazy Loading

// routes/index.tsx
const CandidateRoutes = lazy(() => import('./candidateRoutes'));
const CompanyRoutes = lazy(() => import('./companyRoutes'));

export const AppRoutes = () => (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/candidate/*" element={<CandidateRoutes />} />
<Route path="/company/*" element={<CompanyRoutes />} />
</Routes>
</Suspense>
);

Memoization

// presentation/components/common/JobCard/JobCard.tsx
export const JobCard = memo(({ job, onApply }: JobCardProps) => {
const formattedSalary = useMemo(
() => formatCurrency(job.salary),
[job.salary]
);

return (
<Card>
<CardContent>
<Typography>{job.title}</Typography>
<Typography>{formattedSalary}</Typography>
<Button onClick={() => onApply(job.id)}>Candidatar-se</Button>
</CardContent>
</Card>
);
});

Padrões de Teste

Backend - Django

# tests/candidates/test_candidate_api.py
class CandidateAPITestCase(APITestCase):
def setUp(self):
self.user = User.objects.create_user(
email='test@test.com',
password='testpass'
)
self.candidate = Candidate.objects.create(
user=self.user,
cpf='12345678901'
)
self.client.force_authenticate(user=self.user)

def test_get_candidate_profile(self):
response = self.client.get('/api/v1/candidates/me/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['cpf'], '12345678901')

Frontend - React Testing

// __tests__/components/Button.test.tsx
describe('Button Component', () => {
it('should render correctly', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});

it('should call onClick handler', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);

fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});

Arquitetura Atual

Esta documentação reflete os padrões arquiteturais realmente implementados no projeto Acesso ao Emprego, baseado na análise da estrutura de código do backend Django e frontend React.

Melhores Práticas
  • Mantenha a separação clara entre as camadas
  • Use serviços para lógica de negócio complexa
  • Implemente repositórios para abstração de dados
  • Utilize custom hooks para lógica reutilizável no React
  • Aproveite o cache do TanStack Query para otimizar performance