Selamat pagi, siang, sore, dan malam, teman-teman semua, sudah 5 tahun lebih tidak berjumpa. Pada kesempatan kali ini, aku lagi kepikiran pengen menuliskan sesuatu nih.
Nulis apa ya?
Mesti tentang algoritma dan struktur data lagi
Kali ini beda
Alhamdulillah, sejak awal 2024 yang lalu, aku diterima bekerja di salah satu perusahaan besar di Indonesia sebagai software engineer. Di sana, aku belajar tentang Java Programming dan kegunaan Object Oriented Programming.
Dari apa yang aku pelajari, aku mulai tertarik untuk belajar mengenai Принципи програмування SOLID. Якраз я нещодавно подовжив сертифікат Dicoding за цією темою. Хочеш дізнатися, які ж принципи програмування SOLID? Поїхали.
Принцип єдиної відповідальності (SRP)
Отже, Принцип єдиної відповідальності (SRP) стосується розподілу класів для конкретних завдань. Це означає, що кожен клас має бути відповідальним лише за одну специфічну задачу.
Приклад застосування
- Концепція MVC у Springboot
У Springboot кожен об'єкт представлений кількома класами та інтерфейсами, як показано нижче:
- Model → обробляє представлення сутності в базі даних
- Controller → обробляє CRUD-операції та API, що стосуються сутності
- Repository (Interface) → обробляє SQL-запити, що стосуються сутності, або точніше, взаємодіє з базою даних
- Wrapper/DTO → організовує дані, які будуть відображатися з сутності (відрізняється для API та веб-сторінок)
- Service (Interface) → обробляє бізнес-логіку та передачу даних між сутністю (від wrapper до моделі і навпаки)
- ServiceImpl → реалізує service
Принцип відкритості/закритості (OCP)
Принцип відкритості/закритості означає, що клас має бути відкритим для розширення, але закритим для модифікації.
Звучить незрозуміло?
Мені теж
Отже, суть Принципу відкритості/закритості в тому, що клас не має залежати від модифікацій, якщо потрібно додати нові можливості. Замість цього має бути створений інший, більш специфічний клас.
Що це означає?
Це означає, що додавання нової функціональності не повинно виконуватися через зміну існуючих класів, а має бути реалізовано через додавання нових класів, що стосуються нової функціональності/сутності.
Приклад застосування
- Розділення логіки присутності для вчителя та студента
Без OCP
CivitasService.java
@Service
public class CivitasService {
…
@Override
public void presensi(){
if(Civitas.getRole().equals("Guru")){
presensiGuru();
}else if(Civitas.getRole().equals("Siswa")){
presensiSiswa();
}
}
…
}
Як ви можете побачити, тут роль розподіляється за допомогою умов. А що якщо з'явиться нова роль, наприклад, Pegawai TU? Звісно, нам потрібно буде додати нові умови, що означає зміну існуючого коду, що може порушити використання методу presensi() в інших місцях.
Як це виправити?
З OCP
CivitasService.java
@Service
public class CivitasService {
…
public void presensi(){
}
…
}
TeacherService.java
@Service
public TeacherService extends CivitasService {
...
@Override
public void presensi () {
//Логіка присутності вчителя
}
...
}
StudentService.java
@Service
public StudentService extends CivitasService {
...
@Override
public void presensi() {
//Логіка присутності студента
}
...
}
Тут видно, що є розподіл ролей між вчителем (Teacher) та студентом (Student), де і вчитель, і студент можуть розширити сервіси, які надаються академічною одиницею. Реалізація методу для виконання присутності може бути різною для ролей вчителя та студента за допомогою поліморфізму.
Висновок: OCP можна реалізувати за допомогою наслідування та поліморфізму.
З успадкуванням (inheritance), ми можемо використовувати методи з батьківського класу, які є більш загальними, а з поліморфізмом (polymorphism) ми можемо налаштувати реалізацію методів батьківського класу відповідно до потреб дочірнього класу.
Принцип підстановки Ліскова (LSP)
Третім принципом є Принцип підстановки Ліскова (Liskov Substitution Principle). Цей принцип регулює відносини між батьківським класом та його дочірнім класом. Дочірній клас повинен успадковувати всі властивості батьківського класу, не змінюючи їх.
Приклад застосування
- Не додавати специфічні атрибути чи ролі, яких немає у всіх підкласах, наприклад, додавати інформацію про зарплату до CivitasWrapper
Без LSP
CivitasWrapper.java
public class CivitasWrapper {
...
public Long getSalary(){
}
...
}
TeacherWrapper.java
public class TeacherWrapper extends CivitasWrapper {
...
public Long getSalary(){
}
...
}
StudentWrapper.java
public class TeacherWrapper extends CivitasWrapper {
...
public Long getSalary(){
System.out.println("Староста не має зарплати");
return -1;
}
...
}
У цьому випадку, зрозуміло, що студент не має зарплати, що порушує принцип підстановки Ліскова, згідно з яким дочірній клас повинен представляти батьківський клас. Як буде виглядати правильна реалізація?
З LSP
CivitasWrapper.java
public class CivitasWrapper {
...
}
WorkedCivitasWrapper.java
public class WorkedCivitasWrapper {
...
public Long getSalary(){
...
}
...
}
TeacherWrapper.java
public class TeacherWrapper extends WorkedCivitasWrapper {
...
public Long getSalary(){
}
...
}
StudentWrapper.java
public class StudentWrapper extends CivitasWrapper {
...
}
Метод getSalary() можна перенести в клас WorkedCivitasWrapper, щоб розділити академічні одиниці, що працюють в школі (вчителі, співробітники адміністрації) та студентів. Таким чином, не буде реалізовано метод, який не може бути застосований. Іншими словами, не буде жодного дочірнього класу, який не може успадковувати властивості від батьківського класу.
- Не додавати атрибути видалення до даних, для яких не підтримується м'яке видалення (soft delete).
EntityBaseWrapper.java
public class EntityBaseWrapper {
private Long id;
private String description;
...
}
AuditableBaseWrapper.java
public class AuditableBaseWrapper extends EntityBaseWrapper {
private String createdBy;
private Date createdDate;
private String modifiedBy;
private Date modifiedDate;
...
}
ReferenceBaseWrapper.java
public class ReferenceBaseWrapper extends AuditableBaseWrapper {
private Boolean deleted;
private Integer version;
...
public void setDeleted(Boolean deleted){
this.deleted = deleted;
}
...
}
PresensiSiswaWrapper.java
public class PresensiSiswaWrapper extends ReferenceBaseWrapper {
...
public void setDeleted(Boolean deleted){
System.out.println("Не можна видалити за допомогою м'якого видалення");
}
...
}
У цьому випадку, присутність студента — це дані транзакції, які не можна видалити за допомогою м'якого видалення. Тому немає сенсу використовувати ReferenceBaseWrapper, набагато правильніше буде використати AuditableBaseWrapper, і реалізація виглядатиме так:
PresensiSiswaWrapper.java
public class PresensiSiswaWrapper extends AuditableBaseWrapper {
...
}
Принцип розподілу інтерфейсів (Interface Segregation Principle)
Принцип розподілу інтерфейсів (Interface Segregation Principle) — це принцип, згідно з яким сутність не повинна бути змушена використовувати методи, які їй не потрібні.
Іншими словами, сутність повинна реалізовувати інтерфейс відповідно до того, що їй потрібно.
Приклад застосування
- Розподіл CrudService для сутностей, які можуть виконувати всі методи CRUD, і тих, які не можуть.
Без ISP
CrudService.java
public interface CrudService{
//Create
T save(T wrapper);
//Read
T getById(Z id);
Page getPageableList(String keyword, int startPage, int pageSize,
Sort sort);
List getAll();
Long count();
//Update
T update(T oldData, T newData);
T updateById(Z id, T wrapper);
//Delete
T deleteById(Z id);
T deleteAll();
}
PresensiSiswaService.java
public interface PresensiSiswaService extends CrudService {
...
}
PresensiSiswaServiceImpl.java
@Service
public class PresensiSiswaServiceImpl implements PresensiSiswaService {
...
@Override
public Boolean deleteById(Long id){
System.out.println("Не можна видалити");
return false;
}
...
}
У цій реалізації видно, що сутність PresensiSiswaServiceImpl не підтримує deleteById. Це тому, що присутність студентів — це дані транзакцій, які не можна видалити в цьому випадку.
Тому потрібно створити більш специфічні сервіси для розмежування даних, які можна оновлювати або видаляти, будь то через жорстке видалення (hard delete) або м'яке видалення (soft delete).
З ISP
CrudService.java
public interface CrudService{
//Create
T save(T wrapper);
//Read
T getById(Z id);
Page getPageableList(String keyword, int startPage, int pageSize,
Sort sort);
List getAll();
Long count();
}
UpdateableService.java
public interface Updateable extends CrudService{
//Update
T update(T oldData, T newData);
T updateById(Z id, T wrapper);
}
MasterService.java
public interface MasterService extends CrudService, Updateable{
//Soft Delete
T deleteById(Z id, T wrapper);
T deleteAll();
}
TransactionService.java
public interface TransactionService extends CrudService{
//Hard Delete
Boolean hardDelete(Z id);
Boolean hardDeleteByDate(Date date);
Boolean drop();
}
UpdatableTransactionService.java
public interface UpdatableTransactionService extends CrudService, Updateable{
...
}
З цією реалізацією ми можемо розрізняти операції CRUD, які виконуються для кожної сутності, залежно від того, чи можна цю сутність оновлювати або видаляти. Реалізація класу CrudService.java виконує тільки створення (insertion) і читання (searching), що доступні для всіх сутностей, тоді як процеси оновлення та видалення виконуються в інших специфічних сервісах, оскільки не всі сутності можуть це робити.
Принцип інверсії залежностей (Dependency Inversion Principle)
Принцип інверсії залежностей (Dependency Inversion Principle) вказує на те, що модулі високого рівня не повинні залежати від модулів низького рівня, натомість вони повинні залежати від абстракцій. Крім того, абстракції не повинні залежати від деталей, а деталі повинні залежати від абстракцій.
Приклад застосування
- Розподіл Service і ServiceImpl у Spring Boot
Без DIP
TeacherService.java
@Service
public TeacherService extends CivitasService {
...
}
TeacherController.java
@Controller
@RequestMapping("/teacher")
public TeacherController {
...
TeacherService teacherService = new TeacherService();
//Виведення списку вчителів
@GetMapping("/edit")
public String edit(Map mapParam, @RequestParam Long id) {
...
mapParam.put("model", teacherService.getById(id));
...
return "teacher/edit";
}
...
}
## З DIP
З DIP, Service і ServiceImpl розділені, тому кожен модуль, що використовує сервіс, буде залежати від сервісу, а не від його реалізації.
TeacherService.java
public TeacherService extends CivitasService, MasterService{
...
}
```
TeacherServiceImpl.java
@Service
public TeacherServiceImpl implements TeacherService {
...
@Override
public TeacherWrapper getById(Long id){
Optional optCode = teacherRepository.findById(id);
return optCode.map(this::toWrapper).orElse(null);
}
}
TeacherController.java
@Controller
@RequestMapping("/teacher")
public TeacherController {
...
@Autowired
TeacherService teacherService;
//Виведення списку вчителів
@GetMapping("/edit")
public String edit(Map mapParam, @RequestParam Long id) {
...
mapParam.put("model", teacherService.getById(id));
...
return "teacher/edit";
}
...
}
З цією реалізацією ми можемо використовувати сервіс TeacherService без залежності від його детальної реалізації. Крім того, у Springboot є анотація @Autowired, що дозволяє знайти відповідну реалізацію TeacherService для редагування даних вчителя.
Ну що, як вам? Чи зрозуміли ви принципи програмування SOLID і їхнє застосування у проекті Springboot? Якщо є питання, можна написати їх у коментарях. Дякую, до зустрічі в наступному пості.
Перекладено з: Prinsip Pemrograman SOLID