Проблема заключается в том, чтобы создать компилятор кода, известный как CodeRunner, способный работать с 7 различными языками программирования. Это представляет собой серьезную проблему, поскольку каждый язык имеет свой уникальный синтаксис, структуру и требования.

Понимание фабричного шаблона посредством создания приложения-компилятора кода с помощью Nest JS

В этом руководстве мы создадим приложение-компилятор кода с использованием платформы NestJS и реализуем фабричный шаблон для обработки компиляции кода на разных языках программирования. Прежде чем начать, важно иметь общее представление о Node.js, NestJS и модуле ChildProcess. сможете полностью понять и использовать концепции, описанные в этом руководстве, и сможете эффективно реализовать фабричный шаблон в своих собственных проектах.

Начиная

Начнем с настройки нового проекта NestJS и установки необходимых зависимостей. Затем мы перейдем к реализации фабричного шаблона и создадим компилятор кода, который может работать с несколькими языками программирования.

Готовы ли вы погрузиться в мир фабричного шаблона и NestJS? Давайте начнем!

Структура папок

Начнем с создания нового проекта NestJS. Запустите следующую команду в своем терминале:

nest new codeRunnerServer

Это создаст новый проект NestJS с именем «codeRunnerServer». Далее мы создадим ресурс под названием «codeRunner» с помощью интерфейса командной строки NestJS. Запустите следующую команду в своем терминале:

nest g res codeRunner

Это создаст новый модуль и контроллер для ресурса «codeRunner». Затем создайте папку внутри папки запуска кода с именем «codeRunnerFactory». Эта папка будет использоваться для хранения фабричных классов для каждого языка программирования. После этого просто создайте еще одну подпапку внутри codeRunnerFactory под названием «languagesFactory». Здесь мы можем написать языковой класс для каждого языка программирования. Прежде чем создавать фабрики, позвольте мне кратко объяснить, что такое Factory Pattern.

Что такое заводской шаблон?

Фабричный шаблон — это шаблон проектирования, который предоставляет интерфейс для создания объектов в суперклассе, но позволяет подклассам изменять тип создаваемых объектов. Этот шаблон часто используется в ситуациях, когда класс не может предугадать тип объектов, которые он должен создать.

В нашем приложении компилятора кода мы будем использовать фабричный шаблон для компиляции кода на разных языках программирования. Каждый язык будет иметь свой собственный фабричный класс со своей собственной уникальной логикой обработки процесса компиляции. Эти фабричные классы будут реализовывать общий интерфейс, обеспечивая согласованность и простоту использования на всех языках.

С этим пониманием фабричного шаблона мы можем теперь приступить к созданию фабричных классов для каждого языка программирования и реализации шаблона в нашем приложении-компиляторе кода.

Мы можем создавать классы для каждого языка программирования, например:

  • JavaCodeRunner
  • JavaScriptCodeRunner
  • PythonCodeRunner
  • CppCodeRunner
  • DartCodeRunner
  • CCodeRunner
  • МашинописьCodeRunner

И в каждом классе мы можем написать определенную логику для процесса компиляции этого конкретного языка.

Класс CodeRunnerFactory является основным фабричным классом для нашего приложения-компилятора кода. Он отвечает за создание соответствующего фабричного класса для указанного языка и обработку процесса компиляции.

//src/common/languages.ts

export const languages={
JS:{
    name:'javascript',
    extension:'.js',
},
JAVA:{
    name:'java',
    extension:'.java',
},
CPP:{
    name:'c++',
    extension:'.cpp',
},
C:{
    name:'c',
    extension:'.c',
},
PY:{
    name:'python',
    extension:'.py',
},
DART:{
    name:'dart',
    extension:'.dart',
},
TS:{
    name:'typescript',
    extension:'.ts',
},
}
//this is just list of supported languages for our app.

// src/code-runner/CodeRunnerFactory/codeRunner.factory.ts
export class CodeRunnerFactory {
    create(language) {
      switch (language) {
        case languages.JS.name:
          return new JavaScriptCodeRunner();
        case languages.JAVA.name:
          return new JavaCodeRunner();
        case languages.CPP.name:
          return new CppCodeRunner();
        case languages.C.name:
          return new CCodeRunner();
        case languages.PY.name:
          return new PythonCodeRunner();
        case languages.DART.name:
          return new DartCodeRunner();
        case languages.TS.name:
          return new TypescriptCodeRunner();
        default:
          throw new Error(`Unsupported language: ${language}`);
      }
    }
  }

В этом классе есть единственный метод create(language), который принимает параметр language, указывающий язык, для которого должен быть создан фабричный класс. Затем метод использует оператор switch, чтобы определить, какой класс фабрики создать на основе значения параметра language.

Оператор switch имеет несколько операторов case, каждый из которых проверяет значение параметра language на конкретном языке. Например, если значением параметра language является «javacript», возвращается заводской класс JavaScriptCodeRunner. Если параметр language соответствует любому из случаев, возвращается новый экземпляр соответствующего фабричного класса.

Если параметр language не соответствует ни одному из случаев, выдается ошибка с сообщением «Неподдерживаемый язык: [указанный язык]».

Таким образом, класс CodeRunnerFactory действует как центральное место для создания и возврата соответствующего фабричного класса для указанного языка, что позволяет легко и эффективно выполнять компиляцию кода для нескольких языков.

Написание соответствующего фабричного класса языка

Отлично, теперь, когда мы лучше понимаем класс CodeRunnerFactory и его роль в приложении компилятора кода, давайте посмотрим, как написать соответствующие фабричные классы для каждого языка программирования.

Во-первых, нам нужно создать новый файл для каждого фабричного класса в папке languages. Имя файла должно совпадать с названием соответствующего языка. Например, файл для класса фабрики JavaScript должен называться javascript.factory.ts .

// src/code-runner/CodeRunnerFactory/LanguagesFactory/javascript.factory.ts
import { spawn } from "child_process";
const fs=require('fs')
let path=require('path')
export class JavaScriptCodeRunner {
    async execute(code) {
      return new Promise((resolve, reject) => {
        let inputPath=path.join(__dirname,'code.js')
        fs.writeFileSync(inputPath, code);
        const child = spawn('node', [ inputPath]);
        let output = '';
        child.stdout.on('data', (data) => {
          output += data.toString();
        });
        child.stderr.on('data', (e) => output += e.toString());
        child.on('exit', (code) => {
          if (code === 0) {
            fs.unlinkSync(inputPath);
            resolve(output);
          } else {
            fs.unlinkSync(inputPath);
            reject(output);
          }
        });
      });
    }
  }

Пояснение

Это фабричный класс JavaScript для приложения компилятора кода, которое отвечает за обработку процесса компиляции кода JavaScript. Он содержит единственный метод execute(code), который принимает код для компиляции в качестве параметра и возвращает обещание, разрешающее вывод исполняемого кода или отклонение с ошибкой.

Затем он использует функцию spawn из модуля child_process для выполнения кода, который находится в файле, вызывая node в пути к файлу. Функция spawn возвращает экземпляр дочернего процесса, который хранится в переменной child.

Выходные данные процесса фиксируются путем прослушивания событий stdout и stderr, генерируемых дочерним процессом. Данные, выдаваемые этими событиями, добавляются к переменной output в виде строк.

После завершения процесса код проверяет код выхода. Если код равен 0, это означает, что процесс прошел успешно, затем файл удаляется путем вызова fs.unlinkSync(inputPath), а обещание разрешается с выводом. Если код не 0, это означает, что произошла ошибка, затем файл удаляется путем вызова fs.unlinkSync(inputPath), а обещание отклоняется с сообщением об ошибке. Ну, это все для этого класса и остальных фабричных классов, которые я собираюсь показать здесь сейчас. В этом сила фабричного шаблона, мы можем писать независимые классы для каждого языка, не затрагивая логику другого класса одинаковым образом. Теперь мы можем просто скопировать и вставить то, что мы пишем здесь для javascript, единственное изменение заключается в том, что нам нужно изменить некоторые команды, специфичные для этого конкретного языка.

// src/code-runner/CodeRunnerFactory/LanguagesFactory/java.factory.ts
import { spawn } from "child_process";
const fs=require('fs')
let path=require('path')
export class JavaCodeRunner {
    async execute(code) {
      return new Promise((resolve, reject) => {
        let inputPath=path.join(__dirname,'code.java')
        let classPath=path.join(__dirname,'main.class')
        fs.writeFileSync(inputPath, code);
        const child = spawn('java', ['-cp', classPath, inputPath]);
        let output = '';
        child.stdout.on('data', (data) => {
          output += data.toString();
        });
        child.stderr.on('data', (e) => output += e.toString());
        child.on('exit', (code) => {
          if (code === 0) {
            // fs.unlinkSync(classPath);
            fs.unlinkSync(inputPath);
            resolve(output);
          } else {
            // fs.unlinkSync(classPath);
            fs.unlinkSync(inputPath);
            reject(output);
          }
        });
      });
    }
  }
// src/code-runner/CodeRunnerFactory/LanguagesFactory/cpp.factory.ts
import { exec, spawn } from "child_process";
const fs=require('fs')
let path=require('path')
export class CppCodeRunner {
    async execute(code) {
      return new Promise((resolve, reject) => {
        let inputPath=path.join(__dirname,'code.cpp')
        let outPutPath=path.join(__dirname,'a.out')
        fs.writeFileSync(inputPath, code);
        const child = spawn('g++', ['-o', outPutPath, inputPath]);
        let output = '';
        child.stdout.on('data', (data) => {
          output += data.toString();
        });
        child.stderr.on('data', (e) => output += e.toString());
        child.on('exit', (code) => {
          if (code === 0) {
        const subchild = spawn(outPutPath);
        let out=''
        subchild.stderr.on("data",(data)=>{
           out+=data.toString() 
        })
        subchild.stdout.on("data",(data)=>{
            out+=data.toString() 

        })
        subchild.on("exit",(code)=>{
            if(code==0){
                fs.unlinkSync(outPutPath);
                fs.unlinkSync(inputPath);
                resolve(out);

            }else{
                fs.unlinkSync(outPutPath);
                fs.unlinkSync(inputPath);
                reject(out)
            }
        })
          } else {
            reject(output);
          }
        });
      });
    }
  }

Вы можете подумать, почему мы пишем здесь subChild, верно? Ну, это просто потому, что в cpp после компиляции кода с помощью g++ вывод создается как новый файл с расширением .out. Таким образом, после компиляции кода нам нужно отобразить вывод в виде отдельного дочернего процесса после успешной компиляции кода.
Этот очень гибкий фабричный шаблон позволяет нам просто заглянуть в основную логику, не беспокоясь о других языках. Теперь переходим к другим языкам. 4 класса.
все остальные аналогичны тому, что мы делали ранее, поэтому для экономии времени я поместил ссылку на исходный код git hub здесь https://github.com/Amalreji111/coderunner.git
дать ставь звезду, если тебе нравится работа :)

// src/code-runner/CodeRunnerFactory/LanguagesFactory/c.factory.ts
import { exec, spawn } from "child_process";
const fs=require('fs')
let path=require('path')
export class CCodeRunner {
    async execute(code) {
      return new Promise((resolve, reject) => {
        let inputPath=path.join(__dirname,'code.c')
        let outPutPath=path.join(__dirname,'a.out')
        fs.writeFileSync(inputPath, code);
        const child = spawn('gcc', ['-o', outPutPath, inputPath]);
        let output = '';
        child.stdout.on('data', (data) => {
          output += data.toString();
        });
        child.stderr.on('data', (e) => output += e.toString());
        child.on('exit', (code) => {
          if (code === 0) {
            const subchild = spawn(outPutPath);
            let out=''
            subchild.stderr.on("data",(data)=>{
               out+=data.toString() 
            })
            subchild.stdout.on("data",(data)=>{
                out+=data.toString() 
    
            })
            subchild.on("exit",(code)=>{
                if(code==0){
                    fs.unlinkSync(outPutPath);
                    fs.unlinkSync(inputPath);
                    resolve(out);
    
                }else{
                    fs.unlinkSync(outPutPath);
                    fs.unlinkSync(inputPath);
                    reject(out)
                }
            })
          } else {
            reject(output);
          }
        });
      });
    }
  }
// src/code-runner/CodeRunnerFactory/LanguagesFactory/py.factory.ts
import { exec, spawn } from "child_process";
const fs=require('fs')
let path=require('path')
export class PythonCodeRunner {
    async execute(code) {
        let inputPath=path.join(__dirname,'code.py')
        // let outPutPath=path.join(__dirname,'a.out')
      return new Promise((resolve, reject) => {
        fs.writeFileSync(inputPath, code);
        const child = spawn('python', [inputPath]);
        let output = '';
        child.stdout.on('data', (data) => {
          output += data.toString();
        });
        child.stderr.on('data', (e) => output += e.toString());
        child.on('exit', (code) => {
          if (code === 0) {
            fs.unlinkSync(inputPath);
            resolve(output);
          } else {
            fs.unlinkSync(inputPath);
            reject(output);
          }
        });
      });
    }
  }
// src/code-runner/CodeRunnerFactory/LanguagesFactory/dart.factory.ts
import { exec, spawn } from "child_process";
const fs=require('fs')
export class DartCodeRunner {
    async execute(code) {
      return new Promise((resolve, reject) => {
        fs.writeFileSync('code.dart', code);
        const child = spawn('dart', ['code.dart']);
        let output = '';
        child.stdout.on('data', (data) => {
          output += data.toString();
        });
        child.stderr.on('data', (e) => output += e.toString());
        child.on('exit', (code) => {
          if (code === 0) {
            resolve(output);
          } else {
            reject(output);
          }
        });
      });
    }
  }
// src/code-runner/CodeRunnerFactory/LanguagesFactory/typescript.factory.ts
import { exec, spawn } from "child_process";
const fs=require('fs')
export class TypescriptCodeRunner {
    async execute(code) {
      return new Promise((resolve, reject) => {
        fs.writeFileSync('code.ts', code);
        const child = spawn('tsc', ['code.ts']);
        let output = '';
        child.stdout.on('data', (data) => {
          output += data.toString();
        });
        child.stderr.on('data', (e) => output += e.toString());
        child.on('exit', (code) => {
          if (code === 0) {
            fs.unlinkSync('code.ts');
            resolve(output);
          } else {
            reject(output);
          }
        });
      });
    }
  }

Теперь перейдите к части «Контроллер и сервис», чтобы узнать, как мы можем использовать преимущества этих фабрик. В этом разделе вы узнаете, как это использовать.

//Controller
// src/code-runner/code-runner.controller.ts
import {
  Controller,
  Post,
  Body
} from '@nestjs/common';
import { CodeRunnerService } from './code-runner.service';
import { CreateCodeRunnerDto } from './dto/create-code-runner.dto';
import { UpdateCodeRunnerDto } from './dto/update-code-runner.dto';

@Controller('run')
export class CodeRunnerController {
  constructor(private readonly codeRunnerService: CodeRunnerService) {}
  @Post()
  async runCode(@Body() body: CreateCodeRunnerDto) {
    return this.codeRunnerService.run(body);
  }
}

Здесь, когда мы нажмем путь /run ниже, служба будет выполняться.

import { HttpException, HttpStatus, Injectable, UnauthorizedException } from '@nestjs/common';
import { exec, spawn } from 'child_process';
import { CreateCodeRunnerDto } from './dto/create-code-runner.dto';
import { UpdateCodeRunnerDto } from './dto/update-code-runner.dto';
import { CodeRunnerFactory } from './CodeRunnerFactory/codeRunner.factory';
import { CodeAnalyserFactory } from './CodeAnalyserFactory/codeAnalyser.factory';
const fs=require('fs')
@Injectable()
export class CodeRunnerService {
  private factory:CodeRunnerFactory;
  constructor(){
    this.factory=new CodeRunnerFactory()
  }

  async run(codeRequest: CreateCodeRunnerDto) {
    try {
      let {code,language}=codeRequest
      const regex = new RegExp(/(ngrok|os|Process|process|runtime|GetrunTime|Process\.runSync|Process|node|g\+\+|javac|gpp|java|tsc|dart|nodemon|python|install|run|execute|system|systemctl|exec|spawn|fork|execfile|fs|filesystem|shutdown|reboot|restart|sudo|admin|cd|cat|nano|ls|docker|pull|jenkins|ctl|\.sh|\.bash|kubectl|minicube|helm|cron|vim|vi|git|origin|master|-b|yay|pamac|pacman|apt-get|apt|-S|-Syu|-Rns)\b/)

      if(regex.test(code)){
        throw new HttpException("This code may contain malicious words please check before running..!",400)

      }
      // return regex.test(code)
      const runner=this.factory.create(language)
      const output=await runner.execute(code)
      // console.log('oust',output)
      return output
    } catch (error) {
      console.log(error.message)
      throw new HttpException(error.message,400)

    }
  }
  }

Пояснение

Это служебный класс для приложения компилятора кода, которое отвечает за обработку процесса выполнения кода. Класс использует фабричный шаблон для создания экземпляров классов для разных языков и имеет единственный метод run(codeRequest: CreateCodeRunnerDto).

Входной DTO codeRequest содержит код и язык. Затем мы используем регулярное выражение для проверки кода на наличие вредоносного кода.
Сейчас мы проверяем только системные команды, которые могут напрямую взаимодействовать с ПК. Но это недостаточно безопасно для защиты от опасных кодов. Возможно, нам придется использовать некоторые инструменты, такие как sonarQube, для проверки уязвимости кода, которые не входят в предмет этой статьи, возможно, я объясню это в будущем.

Затем он создает экземпляр CodeRunnerFactory и вызывает метод create этой фабрики, передавая ему язык. Это вернет экземпляр соответствующего языкового класса, созданного фабрикой.
(Помните случаи переключения, которые мы писали ранее? :-P). Затем он вызывает метод execute этого класса, передавая код как аргумент и ждет, пока обещание разрешится.

Если обещание разрешено, оно возвращает вывод. Если обещание отклонено, оно выдает исключение с сообщением об ошибке.

Таким образом, сервисный класс использует фабричный шаблон для создания экземпляров языковых классов и запускает код, упрощая добавление новых языков и поддержку кодовой базы.

Заключение

В заключение мы рассмотрели реализацию шаблона проектирования factory в приложении-компиляторе кода, созданном с помощью NestJS. Мы увидели, как шаблон фабрики можно использовать для управления выполнением кода для нескольких языков программирования. Создавая экземпляры языковых классов с использованием фабричного шаблона, мы смогли упростить добавление новых языков и поддержку кодовой базы.
Я надеюсь, что эта статья оказалась полезной и теперь у вас есть четкое представление о том, как фабричный шаблон может быть реализован на практике. Обладая этими знаниями, вы сможете создать свой собственный мощный серверный клон jdoodle, используя шаблон проектирования factory.

Исходный код:
https://github.com/Amalreji111/coderunner.git