672 lines
18 KiB
TeX
672 lines
18 KiB
TeX
\section{Service Layer}
|
|
|
|
\begin{frame}[shrink=5]{Mi az a Service Layer?}
|
|
\begin{block}{Definíció}
|
|
A \textbf{Service Layer} (szolgáltatási réteg) egy tervezési minta, amely elválasztja az üzleti logikát a controller-ektől és a data access layer-től.
|
|
\end{block}
|
|
|
|
\begin{itemize}
|
|
\item \textbf{Separation of Concerns:} Felelősségek szétválasztása
|
|
\item \textbf{Reusability:} Újrafelhasználható üzleti logika
|
|
\item \textbf{Testability:} Könnyebb tesztelhetőség
|
|
\item \textbf{Maintainability:} Karbantarthatóság
|
|
\end{itemize}
|
|
|
|
\begin{exampleblock}{MVC architektúrában}
|
|
Controller → \textbf{Service Layer} → Repository/Model → Database
|
|
\end{exampleblock}
|
|
\end{frame}
|
|
|
|
\begin{frame}[shrink=5]{Háromrétegű architektúra}
|
|
\begin{columns}
|
|
\begin{column}{0.3\textwidth}
|
|
\begin{block}{Presentation Layer}
|
|
\begin{itemize}
|
|
\item Controllers
|
|
\item Routes
|
|
\item HTTP kérés/válasz
|
|
\item Validáció
|
|
\end{itemize}
|
|
\end{block}
|
|
\end{column}
|
|
|
|
\begin{column}{0.35\textwidth}
|
|
\begin{block}{Business Logic Layer}
|
|
\begin{itemize}
|
|
\item \textbf{Services}
|
|
\item Üzleti szabályok
|
|
\item Adatmanipuláció
|
|
\item Orchestration
|
|
\end{itemize}
|
|
\end{block}
|
|
\end{column}
|
|
|
|
\begin{column}{0.3\textwidth}
|
|
\begin{block}{Data Access Layer}
|
|
\begin{itemize}
|
|
\item Repository
|
|
\item Models
|
|
\item ORM
|
|
\item Database
|
|
\end{itemize}
|
|
\end{block}
|
|
\end{column}
|
|
\end{columns}
|
|
|
|
\begin{alertblock}{Fontos!}
|
|
A controller \textbf{NEM} tartalmaz üzleti logikát, csak delegál a service-eknek!
|
|
\end{alertblock}
|
|
\end{frame}
|
|
|
|
\begin{frame}[shrink=10]{Service Layer előnyei}
|
|
\begin{enumerate}
|
|
\item \textbf{Separation of Concerns}
|
|
\begin{itemize}
|
|
\item Tiszta felelősségi körök
|
|
\item Controller: HTTP kezelés
|
|
\item Service: Üzleti logika
|
|
\item Repository: Adatelérés
|
|
\end{itemize}
|
|
|
|
\item \textbf{Reusability}
|
|
\begin{itemize}
|
|
\item Több controller használhatja ugyanazt a service-t
|
|
\item Különböző kontextusokban (API, CLI, Background job)
|
|
\end{itemize}
|
|
|
|
\item \textbf{Testability}
|
|
\begin{itemize}
|
|
\item Service-ek könnyebben unit tesztelhetők
|
|
\item Mock-olható függőségek
|
|
\end{itemize}
|
|
|
|
\item \textbf{Maintainability}
|
|
\begin{itemize}
|
|
\item Változások izoláltak
|
|
\item Könnyebb hibakeresés
|
|
\end{itemize}
|
|
\end{enumerate}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile,shrink=5]{AuthService - Authentikációs szolgáltatás}
|
|
\begin{block}{AuthService felelősségei}
|
|
\begin{itemize}
|
|
\item Felhasználó regisztráció
|
|
\item Bejelentkezés (login)
|
|
\item Token generálás és validálás
|
|
\item Jelszó hash-elés
|
|
\item Kijelentkezés (logout)
|
|
\end{itemize}
|
|
\end{block}
|
|
|
|
\begin{exampleblock}{AuthService példa}
|
|
\small
|
|
\begin{verbatim}
|
|
class AuthService {
|
|
async register(userData) { ... }
|
|
async login(email, password) { ... }
|
|
async logout(userId) { ... }
|
|
async refreshToken(refreshToken) { ... }
|
|
async verifyToken(token) { ... }
|
|
async resetPassword(email) { ... }
|
|
}
|
|
\end{verbatim}
|
|
\end{exampleblock}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile,shrink=10]{AuthService implementáció - Register}
|
|
\begin{block}{Regisztráció üzleti logikája}
|
|
\small
|
|
\begin{verbatim}
|
|
class AuthService {
|
|
async register(userData) {
|
|
const existing = await this.userRepository
|
|
.findByEmail(userData.email);
|
|
if (existing) throw new Error('User exists');
|
|
|
|
const hashed = await bcrypt.hash(userData.password, 10);
|
|
const user = await this.userRepository.create({
|
|
...userData,
|
|
password: hashed
|
|
});
|
|
|
|
return { id: user.id, email: user.email };
|
|
}
|
|
}
|
|
\end{verbatim}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{AuthService implementáció - Login}
|
|
\begin{block}{Bejelentkezés üzleti logikája}
|
|
\small
|
|
\begin{verbatim}
|
|
async login(email, password) {
|
|
const user = await this.userRepository.findByEmail(email);
|
|
if (!user) throw new Error('Invalid credentials');
|
|
|
|
const valid = await bcrypt.compare(password, user.password);
|
|
if (!valid) throw new Error('Invalid credentials');
|
|
|
|
const accessToken = jwt.sign(
|
|
{ userId: user.id, email: user.email },
|
|
process.env.JWT_SECRET, { expiresIn: '15m' }
|
|
);
|
|
|
|
const refreshToken = jwt.sign(
|
|
{ userId: user.id },
|
|
process.env.REFRESH_SECRET, { expiresIn: '7d' }
|
|
);
|
|
|
|
return { accessToken, refreshToken };
|
|
}
|
|
\end{verbatim}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Controller használja az AuthService-t}
|
|
\begin{block}{Thin Controller - Fat Service}
|
|
\small
|
|
\begin{verbatim}
|
|
app.post('/api/auth/register', async (req, res) => {
|
|
try {
|
|
const user = await authService.register(req.body);
|
|
res.status(201).json({ user });
|
|
} catch (error) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.post('/api/auth/login', async (req, res) => {
|
|
try {
|
|
const result = await authService.login(
|
|
req.body.email, req.body.password
|
|
);
|
|
res.cookie('refreshToken', result.refreshToken,
|
|
{ httpOnly: true, secure: true });
|
|
res.json({ accessToken: result.accessToken });
|
|
} catch (error) {
|
|
res.status(401).json({ error: error.message });
|
|
}
|
|
});
|
|
\end{verbatim}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{UserService - Felhasználó kezelés}
|
|
\begin{block}{UserService felelősségei}
|
|
\begin{itemize}
|
|
\item Felhasználói profil lekérdezés
|
|
\item Profil módosítás
|
|
\item Jelszó változtatás
|
|
\item Felhasználó törlés
|
|
\item Felhasználó lista (admin)
|
|
\end{itemize}
|
|
\end{block}
|
|
|
|
\vspace{0.3cm}
|
|
|
|
\begin{exampleblock}{UserService példa}
|
|
\small
|
|
\begin{verbatim}
|
|
class UserService {
|
|
constructor(userRepository) {
|
|
this.userRepository = userRepository;
|
|
}
|
|
|
|
async getUserProfile(userId) { ... }
|
|
async updateProfile(userId, data) { ... }
|
|
async changePassword(userId, oldPassword, newPassword) { ... }
|
|
async deleteUser(userId) { ... }
|
|
async getAllUsers(filters) { ... }
|
|
}
|
|
\end{verbatim}
|
|
\end{exampleblock}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile,shrink=5]{UserService implementáció}
|
|
\begin{block}{Jelszó változtatás üzleti logikával}
|
|
\small
|
|
\begin{verbatim}
|
|
class UserService {
|
|
async changePassword(userId, oldPass, newPass) {
|
|
const user = await this.userRepository.findById(userId);
|
|
if (!user) throw new Error('User not found');
|
|
|
|
const valid = await bcrypt.compare(oldPass, user.password);
|
|
if (!valid) throw new Error('Incorrect password');
|
|
|
|
const hashed = await bcrypt.hash(newPass, 10);
|
|
await this.userRepository.update(userId,
|
|
{ password: hashed });
|
|
await this.tokenService.revokeAllTokens(userId);
|
|
|
|
return { success: true };
|
|
}
|
|
}
|
|
\end{verbatim}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{TokenService - Token kezelés}
|
|
\begin{block}{TokenService felelősségei}
|
|
\begin{itemize}
|
|
\item Access token generálás
|
|
\item Refresh token generálás
|
|
\item Token validálás
|
|
\item Token megújítás (refresh)
|
|
\item Token visszavonás (revoke)
|
|
\end{itemize}
|
|
\end{block}
|
|
|
|
\vspace{0.3cm}
|
|
|
|
\begin{exampleblock}{TokenService példa}
|
|
\small
|
|
\begin{verbatim}
|
|
class TokenService {
|
|
constructor(tokenRepository) {
|
|
this.tokenRepository = tokenRepository;
|
|
}
|
|
|
|
generateAccessToken(payload) { ... }
|
|
generateRefreshToken(userId) { ... }
|
|
async verifyAccessToken(token) { ... }
|
|
async refreshAccessToken(refreshToken) { ... }
|
|
async revokeToken(token) { ... }
|
|
async revokeAllTokens(userId) { ... }
|
|
}
|
|
\end{verbatim}
|
|
\end{exampleblock}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile,shrink=5]{TokenService implementáció}
|
|
\begin{block}{Token refresh implementáció}
|
|
\small
|
|
\begin{verbatim}
|
|
class TokenService {
|
|
async refreshAccessToken(refreshToken) {
|
|
const decoded = jwt.verify(
|
|
refreshToken, process.env.REFRESH_SECRET);
|
|
|
|
const stored = await this.tokenRepository
|
|
.findByToken(refreshToken);
|
|
if (!stored || stored.revoked)
|
|
throw new Error('Token revoked');
|
|
|
|
const user = await this.userRepository
|
|
.findById(decoded.userId);
|
|
return {
|
|
accessToken: this.generateAccessToken(user)
|
|
};
|
|
}
|
|
}
|
|
\end{verbatim}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{AuthorizationService - Autorizáció}
|
|
\begin{block}{AuthorizationService felelősségei}
|
|
\begin{itemize}
|
|
\item Jogosultság ellenőrzés
|
|
\item Role-based access control (RBAC)
|
|
\item Permission-based access control (PBAC)
|
|
\item Resource ownership ellenőrzés
|
|
\end{itemize}
|
|
\end{block}
|
|
|
|
\vspace{0.3cm}
|
|
|
|
\begin{exampleblock}{AuthorizationService példa}
|
|
\small
|
|
\begin{verbatim}
|
|
class AuthorizationService {
|
|
async hasRole(userId, role) { ... }
|
|
async hasPermission(userId, permission) { ... }
|
|
async canAccessResource(userId, resourceId, action) { ... }
|
|
async isOwner(userId, resourceId) { ... }
|
|
}
|
|
\end{verbatim}
|
|
\end{exampleblock}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{AuthorizationService implementáció}
|
|
\begin{block}{Jogosultság ellenőrzés}
|
|
\small
|
|
\begin{verbatim}
|
|
class AuthorizationService {
|
|
constructor(userRepository, permissionRepository) {
|
|
this.userRepository = userRepository;
|
|
this.permissionRepository = permissionRepository;
|
|
}
|
|
|
|
async canAccessResource(userId, resourceId, action) {
|
|
const user = await this.userRepository.findById(userId);
|
|
|
|
// 1. Admin mindent csinálhat
|
|
if (user.role === 'admin') {
|
|
return true;
|
|
}
|
|
|
|
// 2. Ownership ellenőrzés
|
|
const resource = await this.resourceRepository.findById(resourceId);
|
|
if (resource.ownerId === userId && action === 'read') {
|
|
return true;
|
|
}
|
|
|
|
// 3. Permission alapú ellenőrzés
|
|
const permissions = await this.permissionRepository.getByUserId(userId);
|
|
return permissions.some(p => p.action === action && p.resource === resourceId);
|
|
}
|
|
}
|
|
\end{verbatim}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Dependency Injection}
|
|
\begin{block}{Service-ek kapcsolata}
|
|
A service-ek más service-eket használnak. Dependency Injection segít a függőségek kezelésében.
|
|
\end{block}
|
|
|
|
\vspace{0.3cm}
|
|
|
|
\begin{columns}
|
|
\begin{column}{0.48\textwidth}
|
|
\textbf{Rossz példa:}
|
|
\small
|
|
\begin{verbatim}
|
|
class AuthService {
|
|
constructor() {
|
|
this.userRepo =
|
|
new UserRepository();
|
|
}
|
|
}
|
|
\end{verbatim}
|
|
\end{column}
|
|
|
|
\begin{column}{0.48\textwidth}
|
|
\textbf{Jó példa (DI):}
|
|
\small
|
|
\begin{verbatim}
|
|
class AuthService {
|
|
constructor(userRepo) {
|
|
this.userRepo = userRepo;
|
|
}
|
|
}
|
|
|
|
const authService =
|
|
new AuthService(userRepo);
|
|
\end{verbatim}
|
|
\end{column}
|
|
\end{columns}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Service Container / DI Container}
|
|
\begin{block}{Automatikus Dependency Injection}
|
|
DI container-ek automatikusan kezelik a szolgáltatások létrehozását.
|
|
\end{block}
|
|
|
|
\vspace{0.3cm}
|
|
|
|
\begin{exampleblock}{Awilix library használata}
|
|
\small
|
|
\begin{verbatim}
|
|
const { createContainer, asClass } = require('awilix');
|
|
|
|
const container = createContainer();
|
|
container.register({
|
|
userRepository: asClass(UserRepository).singleton(),
|
|
tokenRepository: asClass(TokenRepository).singleton(),
|
|
authService: asClass(AuthService).singleton(),
|
|
userService: asClass(UserService).singleton(),
|
|
tokenService: asClass(TokenService).singleton()
|
|
});
|
|
|
|
const authService = container.resolve('authService');
|
|
\end{verbatim}
|
|
\end{exampleblock}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Error Handling a Service Layer-ben}
|
|
\begin{block}{Custom Error osztályok}
|
|
\small
|
|
\begin{verbatim}
|
|
class AuthenticationError extends Error {
|
|
constructor(message) {
|
|
super(message);
|
|
this.statusCode = 401;
|
|
}
|
|
}
|
|
|
|
class AuthorizationError extends Error {
|
|
constructor(message) {
|
|
super(message);
|
|
this.statusCode = 403;
|
|
}
|
|
}
|
|
|
|
class NotFoundError extends Error {
|
|
constructor(resource) {
|
|
super(`${resource} not found`);
|
|
this.statusCode = 404;
|
|
}
|
|
}
|
|
\end{verbatim}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Error Handling - Service használat}
|
|
\begin{block}{Service dobja a custom error-t}
|
|
\small
|
|
\begin{verbatim}
|
|
class AuthService {
|
|
async login(email, password) {
|
|
const user = await this.userRepository
|
|
.findByEmail(email);
|
|
if (!user) {
|
|
throw new AuthenticationError('Invalid credentials');
|
|
}
|
|
const valid = await bcrypt.compare(
|
|
password, user.password
|
|
);
|
|
if (!valid) {
|
|
throw new AuthenticationError('Invalid credentials');
|
|
}
|
|
// ...
|
|
}
|
|
}
|
|
\end{verbatim}
|
|
\end{block}
|
|
|
|
\vspace{0.2cm}
|
|
|
|
\begin{block}{Controller kezeli}
|
|
\small
|
|
\begin{verbatim}
|
|
app.post('/api/auth/login', async (req, res, next) => {
|
|
try {
|
|
const result = await authService.login(
|
|
req.body.email, req.body.password);
|
|
res.json(result);
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
\end{verbatim}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile]{Global Error Handler Middleware}
|
|
\begin{block}{Központi hibaüzenet kezelés}
|
|
\small
|
|
\begin{verbatim}
|
|
app.use((error, req, res, next) => {
|
|
console.error(error);
|
|
|
|
if (error.statusCode) {
|
|
return res.status(error.statusCode)
|
|
.json({ error: error.message });
|
|
}
|
|
|
|
res.status(500).json({
|
|
error: 'Internal Server Error'
|
|
});
|
|
});
|
|
\end{verbatim}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}{Service Layer Testing}
|
|
\begin{block}{Unit Testing}
|
|
A service-ek izoláltan tesztelhetők mock repository-kkal és service-ekkel.
|
|
\end{block}
|
|
|
|
\vspace{0.3cm}
|
|
|
|
\begin{itemize}
|
|
\item \textbf{Előnyök:}
|
|
\begin{itemize}
|
|
\item Gyors tesztek (nincs adatbázis)
|
|
\item Üzleti logika fókusz
|
|
\item Mock-olható függőségek
|
|
\end{itemize}
|
|
|
|
\vspace{0.3cm}
|
|
|
|
\item \textbf{Test framework-ök:}
|
|
\begin{itemize}
|
|
\item Jest
|
|
\item Mocha + Chai
|
|
\item Vitest
|
|
\end{itemize}
|
|
|
|
\vspace{0.3cm}
|
|
|
|
\item \textbf{Mocking library-k:}
|
|
\begin{itemize}
|
|
\item Sinon.js
|
|
\item Jest built-in mocks
|
|
\end{itemize}
|
|
\end{itemize}
|
|
\end{frame}
|
|
|
|
\begin{frame}[fragile,shrink=5]{Service Unit Test példa}
|
|
\begin{block}{AuthService.login() teszt}
|
|
\small
|
|
\begin{verbatim}
|
|
describe('AuthService', () => {
|
|
let authService, mockUserRepo;
|
|
|
|
beforeEach(() => {
|
|
mockUserRepo = { findByEmail: jest.fn() };
|
|
authService = new AuthService(mockUserRepo);
|
|
});
|
|
|
|
test('login fails for invalid user', async () => {
|
|
mockUserRepo.findByEmail.mockResolvedValue(null);
|
|
await expect(
|
|
authService.login('test@test.com', 'pass')
|
|
).rejects.toThrow('Invalid credentials');
|
|
});
|
|
});
|
|
\end{verbatim}
|
|
\end{block}
|
|
\end{frame}
|
|
|
|
\begin{frame}{Service Layer Best Practices}
|
|
\begin{enumerate}
|
|
\item \textbf{Single Responsibility:} Egy service egy felelősségi kör
|
|
\item \textbf{Dependency Injection:} Konstruktorban injektált függőségek
|
|
\item \textbf{Thin Controllers:} Controller csak HTTP kezel
|
|
\item \textbf{Error Handling:} Custom error osztályok használata
|
|
\item \textbf{Async/Await:} Tiszta aszinkron kód
|
|
\item \textbf{Validation:} Input validáció a service-ben is
|
|
\item \textbf{Transaction Management:} Adatbázis tranzakciók
|
|
\item \textbf{Logging:} Strukturált log-olás
|
|
\end{enumerate}
|
|
\end{frame}
|
|
|
|
\begin{frame}{Projekt struktúra Service Layer-rel}
|
|
\begin{columns}
|
|
\begin{column}{0.48\textwidth}
|
|
\textbf{Fájl struktúra:}
|
|
\small
|
|
\begin{itemize}
|
|
\item \texttt{src/}
|
|
\begin{itemize}
|
|
\item \texttt{controllers/}
|
|
\begin{itemize}
|
|
\item \texttt{auth.controller.js}
|
|
\item \texttt{user.controller.js}
|
|
\end{itemize}
|
|
\item \texttt{services/}
|
|
\begin{itemize}
|
|
\item \texttt{auth.service.js}
|
|
\item \texttt{user.service.js}
|
|
\item \texttt{token.service.js}
|
|
\end{itemize}
|
|
\item \texttt{repositories/}
|
|
\begin{itemize}
|
|
\item \texttt{user.repository.js}
|
|
\item \texttt{token.repository.js}
|
|
\end{itemize}
|
|
\item \texttt{models/}
|
|
\item \texttt{middlewares/}
|
|
\item \texttt{utils/}
|
|
\end{itemize}
|
|
\end{itemize}
|
|
\end{column}
|
|
|
|
\begin{column}{0.48\textwidth}
|
|
\textbf{Rétegek felelősségei:}
|
|
\begin{itemize}
|
|
\item \textbf{Controller:}
|
|
\begin{itemize}
|
|
\item HTTP kérés/válasz
|
|
\item Validáció (input)
|
|
\item Service hívás
|
|
\end{itemize}
|
|
|
|
\vspace{0.3cm}
|
|
|
|
\item \textbf{Service:}
|
|
\begin{itemize}
|
|
\item Üzleti logika
|
|
\item Adatmanipuláció
|
|
\item Orchestration
|
|
\end{itemize}
|
|
|
|
\vspace{0.3cm}
|
|
|
|
\item \textbf{Repository:}
|
|
\begin{itemize}
|
|
\item Adatbázis műveletek
|
|
\item Query-k
|
|
\item ORM interaction
|
|
\end{itemize}
|
|
\end{itemize}
|
|
\end{column}
|
|
\end{columns}
|
|
\end{frame}
|
|
|
|
\begin{frame}{Összefoglalás}
|
|
\begin{itemize}
|
|
\item \textbf{Service Layer} = Üzleti logika réteg
|
|
\item \textbf{Separation of Concerns:} Controller, Service, Repository
|
|
\item \textbf{AuthService:} Regisztráció, login, token kezelés
|
|
\item \textbf{UserService:} Felhasználó kezelés, profil, jelszó
|
|
\item \textbf{TokenService:} Token generálás, validálás, refresh
|
|
\item \textbf{AuthorizationService:} Jogosultság ellenőrzés
|
|
\item \textbf{Dependency Injection:} Konstruktor alapú DI
|
|
\item \textbf{Error Handling:} Custom error osztályok
|
|
\item \textbf{Testing:} Unit teszt mock-okkal
|
|
\item \textbf{Best Practices:} Thin controller, fat service
|
|
\end{itemize}
|
|
|
|
\vspace{0.3cm}
|
|
|
|
\begin{exampleblock}{Miért használjuk?}
|
|
Tiszta kód, újrafelhasználhatóság, tesztelhetőség, karbantarthatóság
|
|
\end{exampleblock}
|
|
\end{frame}
|